From 6f3965286ba3651aa8f70dfc15c6bbdabdd63436 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:14:13 +0100 Subject: [PATCH 01/41] Update InputConfigDialog.h --- src/frontend/qt_sdl/InputConfig/InputConfigDialog.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index 0dd384429f..933fd2f31f 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -61,6 +61,7 @@ static constexpr std::initializer_list hk_general = HK_SlowMoToggle, HK_FrameLimitToggle, HK_FullscreenToggle, + HK_MenuBarToggle, HK_Lid, HK_Mic, HK_SwapScreens, @@ -81,6 +82,7 @@ static constexpr std::initializer_list hk_general_labels = "Toggle slow mo", "Toggle FPS limit", "Toggle fullscreen", + "Toggle MenuBar", "Close/open lid", "Microphone", "Swap screens", From fc2b34d30b0c8b1d61d4856fd510b79fd8d1ca07 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:13:11 +0100 Subject: [PATCH 02/41] This should work I guess --- src/frontend/qt_sdl/Config.cpp | 2 ++ src/frontend/qt_sdl/EmuInstanceInput.cpp | 3 ++- src/frontend/qt_sdl/InputConfig/InputConfigDialog.h | 2 +- src/frontend/qt_sdl/Window.cpp | 13 +++++++++++++ src/frontend/qt_sdl/Window.h | 2 ++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index f767fcc92d..39cdd7bd1b 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -162,6 +162,7 @@ LegacyEntry LegacyFile[] = {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, + {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, {"HKKey_SolarSensorDecrease", 0, "Keyboard.HK_SolarSensorDecrease", true}, @@ -182,6 +183,7 @@ LegacyEntry LegacyFile[] = {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, + {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, {"HKJoy_SolarSensorDecrease", 0, "Joystick.HK_SolarSensorDecrease", true}, diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index 866eb29ab5..12afa59f6a 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -66,7 +66,8 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_GuitarGripGreen", "HK_GuitarGripRed", "HK_GuitarGripYellow", - "HK_GuitarGripBlue" + "HK_GuitarGripBlue", + "HK_MenuBarToggle" }; std::shared_ptr EmuInstance::joyMutexGlobal = nullptr; diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index 933fd2f31f..dcefc76d3d 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -82,7 +82,7 @@ static constexpr std::initializer_list hk_general_labels = "Toggle slow mo", "Toggle FPS limit", "Toggle fullscreen", - "Toggle MenuBar", + "Toggle menu bar", "Close/open lid", "Microphone", "Swap screens", diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 9b3ab95c2d..10083ee82e 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -612,6 +612,14 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actShowOSD = menu->addAction("Show OSD"); actShowOSD->setCheckable(true); connect(actShowOSD, &QAction::triggered, this, &MainWindow::onChangeShowOSD); + + menu->addSeparator(); // Separate from existing items for clarity + + actMenuBarToggle = menu->addAction(tr("Show &Menu Bar")); + actMenuBarToggle->setCheckable(true); + actMenuBarToggle->setChecked(true); // Always start visible + actMenuBarToggle->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_M)); + connect(actMenuBarToggle, &QAction::toggled, this, &MainWindow::onToggleMenubar); } { QMenu * menu = menubar->addMenu("Config"); @@ -2227,6 +2235,11 @@ void MainWindow::onFullscreenToggled() toggleFullscreen(); } +void MainWindow::onToggleMenubar(bool visible) +{ + menuBar()->setVisible(visible); +} + void MainWindow::onScreenEmphasisToggled() { int currentSizing = windowCfg.GetInt("ScreenSizing"); diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index 678be7c9e9..3af49ddd21 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -240,6 +240,7 @@ private slots: void onFullscreenToggled(); void onScreenEmphasisToggled(); + void onToggleMenubar(bool visible); private: virtual void closeEvent(QCloseEvent* event) override; @@ -283,6 +284,7 @@ private slots: bool hasMenu; + QAction* actToggleMenubar; QAction* actOpenROM; QAction* actBootFirmware; QAction* actCurrentCart; From fcf6a8fa26f6142ee39888f52ca516949cd89586 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:39:08 +0100 Subject: [PATCH 03/41] =?UTF-8?q?G=C3=B3wno=20fix=20usuwaj=C4=85cy=20featu?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/qt_sdl/Config.cpp | 2 -- src/frontend/qt_sdl/EmuInstanceInput.cpp | 1 - src/frontend/qt_sdl/InputConfig/InputConfigDialog.h | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 39cdd7bd1b..f767fcc92d 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -162,7 +162,6 @@ LegacyEntry LegacyFile[] = {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, - {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, {"HKKey_SolarSensorDecrease", 0, "Keyboard.HK_SolarSensorDecrease", true}, @@ -183,7 +182,6 @@ LegacyEntry LegacyFile[] = {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, - {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, {"HKJoy_SolarSensorDecrease", 0, "Joystick.HK_SolarSensorDecrease", true}, diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index 12afa59f6a..34064056a8 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -67,7 +67,6 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_GuitarGripRed", "HK_GuitarGripYellow", "HK_GuitarGripBlue", - "HK_MenuBarToggle" }; std::shared_ptr EmuInstance::joyMutexGlobal = nullptr; diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index dcefc76d3d..0dd384429f 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -61,7 +61,6 @@ static constexpr std::initializer_list hk_general = HK_SlowMoToggle, HK_FrameLimitToggle, HK_FullscreenToggle, - HK_MenuBarToggle, HK_Lid, HK_Mic, HK_SwapScreens, @@ -82,7 +81,6 @@ static constexpr std::initializer_list hk_general_labels = "Toggle slow mo", "Toggle FPS limit", "Toggle fullscreen", - "Toggle menu bar", "Close/open lid", "Microphone", "Swap screens", From 5dcc694df41383d8d214a63d5e249e9a5118065e Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:44:11 +0100 Subject: [PATCH 04/41] =?UTF-8?q?fix=20g=C3=B3wno=20fixa=20i=20guess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/qt_sdl/Window.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index 3af49ddd21..baa133fa2e 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -284,7 +284,7 @@ private slots: bool hasMenu; - QAction* actToggleMenubar; + QAction* actMenuBarToggle; QAction* actOpenROM; QAction* actBootFirmware; QAction* actCurrentCart; From 29a92d3d416f292ec2f75c268f8b10fd7ce28327 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:03:05 +0100 Subject: [PATCH 05/41] Epic update, i hope it works XD --- src/frontend/qt_sdl/Config.cpp | 2 ++ src/frontend/qt_sdl/EmuInstance.h | 1 + src/frontend/qt_sdl/EmuInstanceInput.cpp | 12 ++++++++++++ src/frontend/qt_sdl/InputConfig/InputConfigDialog.h | 1 + src/frontend/qt_sdl/Window.cpp | 1 - 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index f767fcc92d..39cdd7bd1b 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -162,6 +162,7 @@ LegacyEntry LegacyFile[] = {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, + {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, {"HKKey_SolarSensorDecrease", 0, "Keyboard.HK_SolarSensorDecrease", true}, @@ -182,6 +183,7 @@ LegacyEntry LegacyFile[] = {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, + {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, {"HKJoy_SolarSensorDecrease", 0, "Joystick.HK_SolarSensorDecrease", true}, diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index f143496573..dce0d9cdec 100755 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -55,6 +55,7 @@ enum HK_GuitarGripRed, HK_GuitarGripYellow, HK_GuitarGripBlue, + HK_MenuBarToggle, HK_MAX }; diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index 34064056a8..b34d913dd7 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -52,6 +52,7 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_FastForward", "HK_FrameLimitToggle", "HK_FullscreenToggle", + "HK_MenuBarToggle" "HK_SwapScreens", "HK_SwapScreenEmphasis", "HK_SolarSensorDecrease", @@ -452,6 +453,17 @@ void EmuInstance::inputProcess() hotkeyPress = hotkeyMask & ~lastHotkeyMask; hotkeyRelease = lastHotkeyMask & ~hotkeyMask; lastHotkeyMask = hotkeyMask; + + if (hotkeyPress & (1 << HK_MenuBarToggle)) + { + doOnAllWindows([](MainWindow* win) + { + bool visible = !win->menuBar()->isVisible(); + win->menuBar()->setVisible(visible); + if (win->actToggleMenubar) + win->actToggleMenubar->setChecked(visible); + }); + } SDL_UnlockMutex(joyMutex.get()); } diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index 0dd384429f..e5605f508d 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -81,6 +81,7 @@ static constexpr std::initializer_list hk_general_labels = "Toggle slow mo", "Toggle FPS limit", "Toggle fullscreen", + "Toggle menu bar", "Close/open lid", "Microphone", "Swap screens", diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 10083ee82e..fda967593d 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -618,7 +618,6 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actMenuBarToggle = menu->addAction(tr("Show &Menu Bar")); actMenuBarToggle->setCheckable(true); actMenuBarToggle->setChecked(true); // Always start visible - actMenuBarToggle->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_M)); connect(actMenuBarToggle, &QAction::toggled, this, &MainWindow::onToggleMenubar); } { From 1dc324aac113d766243fc22c184a612bffa80d4a Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:21:12 +0100 Subject: [PATCH 06/41] fix better buildu --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 7 ++++--- src/frontend/qt_sdl/InputConfig/InputConfigDialog.h | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index b34d913dd7..858dfba230 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include "Platform.h" #include "SDL_gamecontroller.h" @@ -454,14 +456,13 @@ void EmuInstance::inputProcess() hotkeyRelease = lastHotkeyMask & ~hotkeyMask; lastHotkeyMask = hotkeyMask; - if (hotkeyPress & (1 << HK_MenuBarToggle)) +if (hotkeyPress & (1 << HK_MenuBarToggle)) { doOnAllWindows([](MainWindow* win) { bool visible = !win->menuBar()->isVisible(); win->menuBar()->setVisible(visible); - if (win->actToggleMenubar) - win->actToggleMenubar->setChecked(visible); + win->onToggleMenubar(visible); }); } SDL_UnlockMutex(joyMutex.get()); diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index e5605f508d..1516514512 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -61,6 +61,7 @@ static constexpr std::initializer_list hk_general = HK_SlowMoToggle, HK_FrameLimitToggle, HK_FullscreenToggle, + HK_MenuBarToggle HK_Lid, HK_Mic, HK_SwapScreens, From e4a2672166749eea2901cc85a1c5521e6a0b630f Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:25:22 +0100 Subject: [PATCH 07/41] fix fixa XD --- src/frontend/qt_sdl/InputConfig/InputConfigDialog.h | 2 +- src/frontend/qt_sdl/Window.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index 1516514512..dcefc76d3d 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -61,7 +61,7 @@ static constexpr std::initializer_list hk_general = HK_SlowMoToggle, HK_FrameLimitToggle, HK_FullscreenToggle, - HK_MenuBarToggle + HK_MenuBarToggle, HK_Lid, HK_Mic, HK_SwapScreens, diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index baa133fa2e..7d13406080 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -105,6 +105,7 @@ class MainWindow : public QMainWindow Q_OBJECT public: + void onToggleMenubar(bool visible); explicit MainWindow(int id, EmuInstance* inst, QWidget* parent = nullptr); ~MainWindow(); @@ -240,7 +241,6 @@ private slots: void onFullscreenToggled(); void onScreenEmphasisToggled(); - void onToggleMenubar(bool visible); private: virtual void closeEvent(QCloseEvent* event) override; From bfb988b4dd171de7bcc4846f1c599c2224d0c21c Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:34:04 +0100 Subject: [PATCH 08/41] i am so fucking stupid fix --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index 858dfba230..2c727939bf 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -54,7 +54,7 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_FastForward", "HK_FrameLimitToggle", "HK_FullscreenToggle", - "HK_MenuBarToggle" + "HK_MenuBarToggle", "HK_SwapScreens", "HK_SwapScreenEmphasis", "HK_SolarSensorDecrease", From 814ad811fbb2b06f211acca177f45a7c8cd902f3 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:40:30 +0100 Subject: [PATCH 09/41] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index db919b498b..ed2f26d80b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@

DS emulator, sorta +MelonDs fork made to implement hotkey to hide menubar +As a disclosure I must say that I am shitty at programming and I made this while vibecoding(Ai Grok) so it isn't fully human but it does what it says so have fun :D. The goal is to do things right and fast, akin to blargSNES (but hopefully better). But also to, you know, have a fun challenge :)
From d03f875e31cf08fc7067b0fbe5a893f99de658b0 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:40:40 +0100 Subject: [PATCH 10/41] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed2f26d80b..5300dbbe13 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

-DS emulator, sorta + MelonDs fork made to implement hotkey to hide menubar As a disclosure I must say that I am shitty at programming and I made this while vibecoding(Ai Grok) so it isn't fully human but it does what it says so have fun :D. From baf8bfa732cc5f4f74e5f9f1da1b811c2444eca4 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:00:04 +0100 Subject: [PATCH 11/41] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5300dbbe13..68d8724dff 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@

-MelonDs fork made to implement hotkey to hide menubar +MelonDS fork made to implement hotkey to hide menubar +I made it because I just got myself a Grip for switch that changes switch playability from horizontal to vertical which is great for ds games, but when using moonlight to stream pc melonds to my switch menubar was the last thing holding me back, so I wanted to make it work myself. As a disclosure I must say that I am shitty at programming and I made this while vibecoding(Ai Grok) so it isn't fully human but it does what it says so have fun :D. The goal is to do things right and fast, akin to blargSNES (but hopefully better). But also to, you know, have a fun challenge :) From 1690b03508ea8a027449d7a09b53de2182372c8e Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:03:16 +0100 Subject: [PATCH 12/41] Cosmetic changes to ReadMe --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 68d8724dff..bfdcf9a606 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-

melonDS

+

melonDS-menubartoggle

@@ -13,11 +13,12 @@

-MelonDS fork made to implement hotkey to hide menubar -I made it because I just got myself a Grip for switch that changes switch playability from horizontal to vertical which is great for ds games, but when using moonlight to stream pc melonds to my switch menubar was the last thing holding me back, so I wanted to make it work myself. -As a disclosure I must say that I am shitty at programming and I made this while vibecoding(Ai Grok) so it isn't fully human but it does what it says so have fun :D. +

MelonDS fork made to implement hotkey to hide menubar

+

I made it because I just got myself a Grip for switch that changes switch playability from horizontal to vertical which is great for ds games.

+

But when using moonlight to stream pc melonds to my switch menubar was the last thing holding me back, so I wanted to make it work myself.

+

As a disclosure I must say that I am shitty at programming and I made this while vibecoding(Ai Grok) so it isn't fully human but it does what it says so have fun :D.

+ -The goal is to do things right and fast, akin to blargSNES (but hopefully better). But also to, you know, have a fun challenge :)
## How to use From e37fa6873f86bc65526a2c5c06cf9c9deac1877e Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:30:47 +0100 Subject: [PATCH 13/41] my stupid ass added button traping users --- src/frontend/qt_sdl/Window.cpp | 7 ------- src/frontend/qt_sdl/Window.h | 2 -- 2 files changed, 9 deletions(-) diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index fda967593d..a2fc8a7ee9 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -612,13 +612,6 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actShowOSD = menu->addAction("Show OSD"); actShowOSD->setCheckable(true); connect(actShowOSD, &QAction::triggered, this, &MainWindow::onChangeShowOSD); - - menu->addSeparator(); // Separate from existing items for clarity - - actMenuBarToggle = menu->addAction(tr("Show &Menu Bar")); - actMenuBarToggle->setCheckable(true); - actMenuBarToggle->setChecked(true); // Always start visible - connect(actMenuBarToggle, &QAction::toggled, this, &MainWindow::onToggleMenubar); } { QMenu * menu = menubar->addMenu("Config"); diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index 7d13406080..678be7c9e9 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -105,7 +105,6 @@ class MainWindow : public QMainWindow Q_OBJECT public: - void onToggleMenubar(bool visible); explicit MainWindow(int id, EmuInstance* inst, QWidget* parent = nullptr); ~MainWindow(); @@ -284,7 +283,6 @@ private slots: bool hasMenu; - QAction* actMenuBarToggle; QAction* actOpenROM; QAction* actBootFirmware; QAction* actCurrentCart; From 14805c1f67de8208a113447bb1e232b0b070ea04 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:35:45 +0100 Subject: [PATCH 14/41] fix because unused code --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index 2c727939bf..f50eee7e04 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -457,15 +457,12 @@ void EmuInstance::inputProcess() lastHotkeyMask = hotkeyMask; if (hotkeyPress & (1 << HK_MenuBarToggle)) +{ + doOnAllWindows([](MainWindow* win) { - doOnAllWindows([](MainWindow* win) - { - bool visible = !win->menuBar()->isVisible(); - win->menuBar()->setVisible(visible); - win->onToggleMenubar(visible); - }); - } - SDL_UnlockMutex(joyMutex.get()); + bool visible = !win->menuBar()->isVisible(); + win->menuBar()->setVisible(visible); + }); } void EmuInstance::touchScreen(int x, int y) From 1a768450bb2a43f4b51f49a147db9142cc04c805 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:55:53 +0100 Subject: [PATCH 15/41] even stupider case for a fix --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index f50eee7e04..f1c89cd9ca 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -465,6 +465,9 @@ if (hotkeyPress & (1 << HK_MenuBarToggle)) }); } +SDL_UnlockMutex(joyMutex.get()); +} + void EmuInstance::touchScreen(int x, int y) { touchX = x; From 0c04ac3e012e7ebccdc30b429a697db2f140ffea Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:01:55 +0100 Subject: [PATCH 16/41] more unused code --- src/frontend/qt_sdl/Window.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index a2fc8a7ee9..9b3ab95c2d 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -2227,11 +2227,6 @@ void MainWindow::onFullscreenToggled() toggleFullscreen(); } -void MainWindow::onToggleMenubar(bool visible) -{ - menuBar()->setVisible(visible); -} - void MainWindow::onScreenEmphasisToggled() { int currentSizing = windowCfg.GetInt("ScreenSizing"); From e389058c7ecd917cc0e1bc874c30a4c3961055e5 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:12:08 +0100 Subject: [PATCH 17/41] revert on readme --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bfdcf9a606..db919b498b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-

melonDS-menubartoggle

+

melonDS

@@ -12,13 +12,9 @@

+DS emulator, sorta -

MelonDS fork made to implement hotkey to hide menubar

-

I made it because I just got myself a Grip for switch that changes switch playability from horizontal to vertical which is great for ds games.

-

But when using moonlight to stream pc melonds to my switch menubar was the last thing holding me back, so I wanted to make it work myself.

-

As a disclosure I must say that I am shitty at programming and I made this while vibecoding(Ai Grok) so it isn't fully human but it does what it says so have fun :D.

- - +The goal is to do things right and fast, akin to blargSNES (but hopefully better). But also to, you know, have a fun challenge :)
## How to use From 8bdda09f847f95fbb228ef6434c4452035dea1e6 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:00:42 +0100 Subject: [PATCH 18/41] Reworked EmuInstanceInput.cpp to not touch existing code and made it look on par, + deletion of unused include. I think that is last thing that was missing from that fork and probably last revision. --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index f1c89cd9ca..3c438f7627 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include "Platform.h" #include "SDL_gamecontroller.h" @@ -69,7 +68,7 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_GuitarGripGreen", "HK_GuitarGripRed", "HK_GuitarGripYellow", - "HK_GuitarGripBlue", + "HK_GuitarGripBlue" }; std::shared_ptr EmuInstance::joyMutexGlobal = nullptr; @@ -455,17 +454,16 @@ void EmuInstance::inputProcess() hotkeyPress = hotkeyMask & ~lastHotkeyMask; hotkeyRelease = lastHotkeyMask & ~hotkeyMask; lastHotkeyMask = hotkeyMask; + SDL_UnlockMutex(joyMutex.get()); if (hotkeyPress & (1 << HK_MenuBarToggle)) -{ - doOnAllWindows([](MainWindow* win) { + doOnAllWindows([](MainWindow* win) + { bool visible = !win->menuBar()->isVisible(); win->menuBar()->setVisible(visible); - }); -} - -SDL_UnlockMutex(joyMutex.get()); + }); + } } void EmuInstance::touchScreen(int x, int y) From f93c230fad3248b28051df53eae3c60e1c2ae1cb Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:02:15 +0100 Subject: [PATCH 19/41] if function shift to look more clean --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index 3c438f7627..f333863cf0 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -456,7 +456,7 @@ void EmuInstance::inputProcess() lastHotkeyMask = hotkeyMask; SDL_UnlockMutex(joyMutex.get()); -if (hotkeyPress & (1 << HK_MenuBarToggle)) + if (hotkeyPress & (1 << HK_MenuBarToggle)) { doOnAllWindows([](MainWindow* win) { From 3b75b1e57baf8f27b209bf49f1de35833f5905a3 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:27:20 +0100 Subject: [PATCH 20/41] Full Rewrite to fit more into melonds code --- src/frontend/qt_sdl/EmuInstanceInput.cpp | 10 ---------- src/frontend/qt_sdl/EmuThread.cpp | 3 +++ src/frontend/qt_sdl/EmuThread.h | 1 + src/frontend/qt_sdl/Window.cpp | 14 ++++++++++++++ src/frontend/qt_sdl/Window.h | 1 + 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index f333863cf0..c26d0e6e1f 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -18,7 +18,6 @@ #include #include -#include #include "Platform.h" #include "SDL_gamecontroller.h" @@ -455,15 +454,6 @@ void EmuInstance::inputProcess() hotkeyRelease = lastHotkeyMask & ~hotkeyMask; lastHotkeyMask = hotkeyMask; SDL_UnlockMutex(joyMutex.get()); - - if (hotkeyPress & (1 << HK_MenuBarToggle)) - { - doOnAllWindows([](MainWindow* win) - { - bool visible = !win->menuBar()->isVisible(); - win->menuBar()->setVisible(visible); - }); - } } void EmuInstance::touchScreen(int x, int y) diff --git a/src/frontend/qt_sdl/EmuThread.cpp b/src/frontend/qt_sdl/EmuThread.cpp index e754d982aa..06cf5ae155 100755 --- a/src/frontend/qt_sdl/EmuThread.cpp +++ b/src/frontend/qt_sdl/EmuThread.cpp @@ -77,6 +77,7 @@ void EmuThread::attachWindow(MainWindow* window) connect(this, SIGNAL(windowEmuReset()), window, SLOT(onEmuReset())); connect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); connect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); + connect(this, SIGNAL(windowMenuBarToggle()), window, SLOT(onMenuBarToggled())); connect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); if (window->winHasMenu()) @@ -95,6 +96,7 @@ void EmuThread::detachWindow(MainWindow* window) disconnect(this, SIGNAL(windowEmuReset()), window, SLOT(onEmuReset())); disconnect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); disconnect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); + disconnect(this, SIGNAL(windowMenuBarToggle()), window, SLOT(onMenuBarToggled())); disconnect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); if (window->winHasMenu()) @@ -165,6 +167,7 @@ void EmuThread::run() if (emuInstance->hotkeyPressed(HK_FrameStep)) emuFrameStep(); if (emuInstance->hotkeyPressed(HK_FullscreenToggle)) emit windowFullscreenToggle(); + if (emuInstance->hotkeyPressed(HK_MenuBarToggle)) emit windowMenuBarToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreens)) emit swapScreensToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreenEmphasis)) emit screenEmphasisToggle(); diff --git a/src/frontend/qt_sdl/EmuThread.h b/src/frontend/qt_sdl/EmuThread.h index 7a7f060052..8462ee91f5 100755 --- a/src/frontend/qt_sdl/EmuThread.h +++ b/src/frontend/qt_sdl/EmuThread.h @@ -157,6 +157,7 @@ class EmuThread : public QThread void autoScreenSizingChange(int sizing); void windowFullscreenToggle(); + void windowMenuBarToggle(); void swapScreensToggle(); void screenEmphasisToggle(); diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 9b3ab95c2d..1c98b7e5a3 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -2227,6 +2227,20 @@ void MainWindow::onFullscreenToggled() toggleFullscreen(); } +void MainWindow::onMenuBarToggled() +{ + if (!hasMenu) return; + if (menuBar()->maximumHeight() != 0) + { + menuBar()->setFixedHeight(0); + } + else + { + int menuBarHeight = menuBar()->sizeHint().height(); + menuBar()->setFixedHeight(menuBarHeight); + } +} + void MainWindow::onScreenEmphasisToggled() { int currentSizing = windowCfg.GetInt("ScreenSizing"); diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index 678be7c9e9..fd9acf657c 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -239,6 +239,7 @@ private slots: void onUpdateVideoSettings(bool glchange); void onFullscreenToggled(); + void onMenuBarToggled(); void onScreenEmphasisToggled(); private: From 3e19c526e013c2a68f9f647a8d7b82294dfccb85 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:01:36 +0100 Subject: [PATCH 21/41] Update Config.h --- src/frontend/qt_sdl/Config.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/frontend/qt_sdl/Config.h b/src/frontend/qt_sdl/Config.h index d83463f343..354aaa1f4a 100644 --- a/src/frontend/qt_sdl/Config.h +++ b/src/frontend/qt_sdl/Config.h @@ -29,6 +29,15 @@ namespace Config { +extern bool RA_Enabled; + extern bool RA_HardcoreMode; + extern std::string RA_Username; + extern std::string RA_Token; + + void SyncRAConfig(); + void SaveRAConfig(); + + void Save(); // Upewnij się, że to tu jest struct LegacyEntry { @@ -135,7 +144,5 @@ void Save(); Table GetLocalTable(int instance); inline Table GetGlobalTable() { return GetLocalTable(-1); } - } - #endif // CONFIG_H From c55230e448613f56e8e25c8692ff4735543ca375 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:01:53 +0100 Subject: [PATCH 22/41] Update Config.cpp --- src/frontend/qt_sdl/Config.cpp | 119 +++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 41 deletions(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 39cdd7bd1b..b24f520b1e 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -33,7 +33,6 @@ using namespace std::string_literals; - namespace Config { using namespace melonDS; @@ -46,6 +45,12 @@ const char* kLegacyUniqueConfigFile = "melonDS.%d.ini"; toml::value RootTable; +// --- Zmienne RetroAchievements --- +bool RA_Enabled; +bool RA_HardcoreMode; +std::string RA_Username; +std::string RA_Token; + DefaultList DefaultInts = { {"Instance*.Keyboard", -1}, @@ -155,47 +160,47 @@ LegacyEntry LegacyFile[] = {"Joy_X", 0, "Joystick.X", true}, {"Joy_Y", 0, "Joystick.Y", true}, - {"HKKey_Lid", 0, "Keyboard.HK_Lid", true}, - {"HKKey_Mic", 0, "Keyboard.HK_Mic", true}, - {"HKKey_Pause", 0, "Keyboard.HK_Pause", true}, - {"HKKey_Reset", 0, "Keyboard.HK_Reset", true}, - {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, - {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, - {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, - {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, - {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, - {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, + {"HKKey_Lid", 0, "Keyboard.HK_Lid", true}, + {"HKKey_Mic", 0, "Keyboard.HK_Mic", true}, + {"HKKey_Pause", 0, "Keyboard.HK_Pause", true}, + {"HKKey_Reset", 0, "Keyboard.HK_Reset", true}, + {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, + {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, + {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, + {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, + {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, + {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, {"HKKey_SolarSensorDecrease", 0, "Keyboard.HK_SolarSensorDecrease", true}, {"HKKey_SolarSensorIncrease", 0, "Keyboard.HK_SolarSensorIncrease", true}, - {"HKKey_FrameStep", 0, "Keyboard.HK_FrameStep", true}, - {"HKKey_PowerButton", 0, "Keyboard.HK_PowerButton", true}, - {"HKKey_VolumeUp", 0, "Keyboard.HK_VolumeUp", true}, - {"HKKey_VolumeDown", 0, "Keyboard.HK_VolumeDown", true}, - {"HKKey_GuitarGripGreen", 0, "Keyboard.HK_GuitarGripGreen", true}, - {"HKKey_GuitarGripRed", 0, "Keyboard.HK_GuitarGripRed", true}, - {"HKKey_GuitarGripYellow", 0, "Keyboard.HK_GuitarGripYellow", true}, - {"HKKey_GuitarGripBlue", 0, "Keyboard.HK_GuitarGripBlue", true}, - - {"HKJoy_Lid", 0, "Joystick.HK_Lid", true}, - {"HKJoy_Mic", 0, "Joystick.HK_Mic", true}, - {"HKJoy_Pause", 0, "Joystick.HK_Pause", true}, - {"HKJoy_Reset", 0, "Joystick.HK_Reset", true}, - {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, - {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, - {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, - {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, - {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, - {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, + {"HKKey_FrameStep", 0, "Keyboard.HK_FrameStep", true}, + {"HKKey_PowerButton", 0, "Keyboard.HK_PowerButton", true}, + {"HKKey_VolumeUp", 0, "Keyboard.HK_VolumeUp", true}, + {"HKKey_VolumeDown", 0, "Keyboard.HK_VolumeDown", true}, + {"HKKey_GuitarGripGreen", 0, "Keyboard.HK_GuitarGripGreen", true}, + {"HKKey_GuitarGripRed", 0, "Keyboard.HK_GuitarGripRed", true}, + {"HKKey_GuitarGripYellow", 0, "Keyboard.HK_GuitarGripYellow", true}, + {"HKKey_GuitarGripBlue", 0, "Keyboard.HK_GuitarGripBlue", true}, + + {"HKJoy_Lid", 0, "Joystick.HK_Lid", true}, + {"HKJoy_Mic", 0, "Joystick.HK_Mic", true}, + {"HKJoy_Pause", 0, "Joystick.HK_Pause", true}, + {"HKJoy_Reset", 0, "Joystick.HK_Reset", true}, + {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, + {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, + {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, + {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, + {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, + {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, {"HKJoy_SolarSensorDecrease", 0, "Joystick.HK_SolarSensorDecrease", true}, {"HKJoy_SolarSensorIncrease", 0, "Joystick.HK_SolarSensorIncrease", true}, - {"HKJoy_FrameStep", 0, "Joystick.HK_FrameStep", true}, - {"HKJoy_PowerButton", 0, "Joystick.HK_PowerButton", true}, - {"HKJoy_VolumeUp", 0, "Joystick.HK_VolumeUp", true}, - {"HKJoy_VolumeDown", 0, "Joystick.HK_VolumeDown", true}, - {"HKJoy_GuitarGripGreen", 0, "Joystick.HK_GuitarGripGreen", true}, - {"HKJoy_GuitarGripRed", 0, "Joystick.HK_GuitarGripRed", true}, - {"HKJoy_GuitarGripYellow", 0, "Joystick.HK_GuitarGripYellow", true}, - {"HKJoy_GuitarGripBlue", 0, "Joystick.HK_GuitarGripBlue", true}, + {"HKJoy_FrameStep", 0, "Joystick.HK_FrameStep", true}, + {"HKJoy_PowerButton", 0, "Joystick.HK_PowerButton", true}, + {"HKJoy_VolumeUp", 0, "Joystick.HK_VolumeUp", true}, + {"HKJoy_VolumeDown", 0, "Joystick.HK_VolumeDown", true}, + {"HKJoy_GuitarGripGreen", 0, "Joystick.HK_GuitarGripGreen", true}, + {"HKJoy_GuitarGripRed", 0, "Joystick.HK_GuitarGripRed", true}, + {"HKJoy_GuitarGripYellow", 0, "Joystick.HK_GuitarGripYellow", true}, + {"HKJoy_GuitarGripBlue", 0, "Joystick.HK_GuitarGripBlue", true}, {"JoystickID", 0, "JoystickID", true}, @@ -339,6 +344,27 @@ LegacyEntry LegacyFile[] = {"", -1, "", false} }; +// --- NOWE FUNKCJE RETROACHIEVEMENTS --- + +void SyncRAConfig() +{ + Table tbl = GetGlobalTable(); + RA_Enabled = tbl.GetBool("RetroAchievements.Enabled"); + RA_HardcoreMode = tbl.GetBool("RetroAchievements.HardcoreMode"); + RA_Username = tbl.GetString("RetroAchievements.Username"); + RA_Token = tbl.GetString("RetroAchievements.Token"); +} + +void SaveRAConfig() +{ + Table tbl = GetGlobalTable(); + tbl.SetBool("RetroAchievements.Enabled", RA_Enabled); + tbl.SetBool("RetroAchievements.HardcoreMode", RA_HardcoreMode); + tbl.SetString("RetroAchievements.Username", RA_Username); + tbl.SetString("RetroAchievements.Token", RA_Token); + Save(); +} + static std::string GetDefaultKey(std::string path) { @@ -794,11 +820,16 @@ bool Load() RootTable = toml::value(); if (!Platform::FileExists(cfgpath)) - return LoadLegacy(); + { + bool ret = LoadLegacy(); + SyncRAConfig(); + return ret; + } try { RootTable = toml::parse(std::filesystem::u8path(cfgpath)); + SyncRAConfig(); } catch (toml::syntax_error& err) { @@ -810,9 +841,15 @@ bool Load() void Save() { + Table tbl = GetGlobalTable(); + tbl.SetBool("RetroAchievements.Enabled", RA_Enabled); + tbl.SetBool("RetroAchievements.HardcoreMode", RA_HardcoreMode); + tbl.SetString("RetroAchievements.Username", RA_Username); + tbl.SetString("RetroAchievements.Token", RA_Token); + auto cfgpath = Platform::GetLocalFilePath(kConfigFile); if (!Platform::CheckFileWritable(cfgpath)) - return; + return; std::ofstream file; file.open(std::filesystem::u8path(cfgpath), std::ofstream::out | std::ofstream::trunc); @@ -834,4 +871,4 @@ Table GetLocalTable(int instance) return Table(tbl, key); } -} +} // namespace Config From 71ddddc83ff0192ee9d84a2cc4454d3eaf38eeaf Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:03:12 +0100 Subject: [PATCH 23/41] Update EmuSettingsDialog.cpp --- src/frontend/qt_sdl/EmuSettingsDialog.cpp | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.cpp b/src/frontend/qt_sdl/EmuSettingsDialog.cpp index c442289c70..ba88408cb3 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.cpp +++ b/src/frontend/qt_sdl/EmuSettingsDialog.cpp @@ -16,6 +16,12 @@ with melonDS. If not, see http://www.gnu.org/licenses/. */ +#include +#include +#include +#include +#include + #include #include @@ -160,6 +166,36 @@ EmuSettingsDialog::EmuSettingsDialog(QWidget* parent) : QDialog(parent), ui(new SET_ORIGVAL(QComboBox, currentIndex); SET_ORIGVAL(QCheckBox, isChecked); + // --- Sekcja RetroAchievements --- + QWidget* raTab = new QWidget(); + QVBoxLayout* tabLayout = new QVBoxLayout(raTab); + + groupRA = new QGroupBox("RetroAchievements Settings", raTab); + QFormLayout* raForm = new QFormLayout(groupRA); + + cbRAEnabled = new QCheckBox("Enable RetroAchievements"); + cbRAHardcore = new QCheckBox("Hardcore Mode (No Savestates)"); + leRAUsername = new QLineEdit(); + leRAToken = new QLineEdit(); + leRAToken->setEchoMode(QLineEdit::Password); // Ukrywa token + leRAToken->setPlaceholderText("Enter your API Token from RA website"); + + raForm->addRow(cbRAEnabled); + raForm->addRow(cbRAHardcore); + raForm->addRow("Username:", leRAUsername); + raForm->addRow("Login Token:", leRAToken); + + tabLayout->addWidget(groupRA); + tabLayout->addStretch(); // Spycha formularz do góry + + // Dodanie zakładki do głównego widgetu okna + ui->tabWidget->addTab(raTab, "RA"); + + // Wczytanie obecnych wartości z konfiguracji + cbRAEnabled->setChecked(Config::RA_Enabled); + cbRAHardcore->setChecked(Config::RA_HardcoreMode); + leRAUsername->setText(QString::fromStdString(Config::RA_Username)); + leRAToken->setText(QString::fromStdString(Config::RA_Token)); #undef SET_ORIGVAL } @@ -224,6 +260,13 @@ void EmuSettingsDialog::done(int r) if (r == QDialog::Accepted) { + // Zapis ustawień RetroAchievements + Config::RA_Enabled = cbRAEnabled->isChecked(); + Config::RA_HardcoreMode = cbRAHardcore->isChecked(); + Config::RA_Username = leRAUsername->text().toStdString(); + Config::RA_Token = leRAToken->text().toStdString(); + + Config::SaveRAConfig(); // Wywołanie Twojej funkcji zapisu z Config.cpp bool modified = false; #define CHECK_ORIGVAL(type, val) \ From ec2c4e92445c1a8e4fbb88fa7f13b04a2c62b364 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:29:46 +0100 Subject: [PATCH 24/41] First batch of changes. could be wrong --- src/CMakeLists.txt | 66 +++++++ src/NDS.cpp | 28 ++- src/NDS.h | 5 + src/NDSCart.cpp | 18 ++ src/NDSCart.h | 2 + src/frontend/qt_sdl/Config.cpp | 13 +- src/frontend/qt_sdl/Config.h | 5 +- src/frontend/qt_sdl/EmuInstance.cpp | 2 + src/frontend/qt_sdl/EmuThread.cpp | 85 ++++++++- src/frontend/qt_sdl/Window.cpp | 256 ++++++++++++++++++++++++++-- src/frontend/qt_sdl/Window.h | 6 + src/frontend/qt_sdl/main.cpp | 26 ++- 12 files changed, 479 insertions(+), 33 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e85dd42958..a5cc1f1e67 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -225,6 +225,72 @@ if (ENABLE_JIT_PROFILING) target_link_libraries(core PRIVATE "${VTUNE_LIBRARY}") endif() +# --- RetroAchievements --- +message(STATUS "Adding RetroAchievements integration sources") + +target_sources(core PRIVATE + RetroAchievements/RAClient.cpp + RetroAchievements/RAClient.h + RetroAchievements/RAFunctions.h +) + +target_include_directories(core PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/RetroAchievements" + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +message(STATUS "RetroAchievements support enabled") + +# --- CURL & SSL Configuration --- +target_compile_definitions(core PRIVATE HAVE_CURL CURL_STATICLIB) + +if(DEFINED ENV{MSYSTEM_PREFIX}) + set(CURL_LDIR "$ENV{MSYSTEM_PREFIX}/lib") +else() + set(CURL_LDIR "C:/msys64/ucrt64/lib") +endif() + +set(NGTCP2_CRYPTO "${CURL_LDIR}/libngtcp2_crypto_ossl.a") + +target_link_libraries(core PRIVATE + "${CURL_LDIR}/libcurl.a" + "${CURL_LDIR}/libnghttp2.a" + "${CURL_LDIR}/libnghttp3.a" + "${CURL_LDIR}/libngtcp2.a" + "${NGTCP2_CRYPTO}" + "${CURL_LDIR}/libssh2.a" + "${CURL_LDIR}/libpsl.a" + "${CURL_LDIR}/libssl.a" + "${CURL_LDIR}/libcrypto.a" + "${CURL_LDIR}/libbrotlidec.a" + "${CURL_LDIR}/libbrotlicommon.a" + "${CURL_LDIR}/libbrotlienc.a" + "${CURL_LDIR}/libzstd.a" + "${CURL_LDIR}/libidn2.a" + "${CURL_LDIR}/libunistring.a" + "${CURL_LDIR}/libiconv.a" + "${CURL_LDIR}/libz.a" + ws2_32 wldap32 crypt32 normaliz advapi32 secur32 bcrypt +) + +# --- rcheevos --- +file(GLOB_RECURSE RCH_ALL + "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src/*.c" +) + +list(FILTER RCH_ALL EXCLUDE REGEX + "rc_libretro.c|rc_client_external.c|rhash/aes.c" +) + +target_sources(core PRIVATE ${RCH_ALL}) + +target_include_directories(core PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include" + "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src" +) + +target_compile_definitions(core PRIVATE RETROACHIEVEMENTS_ENABLED) + #if(CMAKE_BUILD_TYPE MATCHES "Debug") # set( # CMAKE_C_FLAGS diff --git a/src/NDS.cpp b/src/NDS.cpp index 77c48183dd..2dccc7aa62 100644 --- a/src/NDS.cpp +++ b/src/NDS.cpp @@ -44,9 +44,17 @@ #include "DSi_DSP.h" #include "ARMJIT.h" #include "ARMJIT_Memory.h" +#include "RetroAchievements/RAClient.h" +#include namespace melonDS { + + namespace Config { + std::string RA_Username = ""; + std::string RA_Token = ""; + } + using namespace Platform; const s32 kMaxIterationCycles = 64; @@ -543,6 +551,9 @@ void NDS::Reset() SPI.Reset(); RTC.Reset(); Wifi.Reset(); + memset(MainRAM, 0, MainRAMMask + 1); + memset(SharedWRAM, 0, 0x8000); + memset(ARM7WRAM, 0, 0x10000); } void NDS::Start() @@ -755,6 +766,7 @@ bool NDS::DoSavestate(Savestate* file) if (!file->Saving) { + ::RAContext::Get().DisableHardcore("Load state"); GPU.SetPowerCnt(PowerControl9); SPU.SetPowerCnt(PowerControl7 & 0x0001); @@ -773,9 +785,21 @@ bool NDS::DoSavestate(Savestate* file) void NDS::SetNDSCart(std::unique_ptr&& cart) { NDSCartSlot.SetCart(std::move(cart)); - // The existing cart will always be ejected; + // The existing cart will always be ejected; // if cart is null, then that's equivalent to ejecting a cart // without inserting a new one. + + if (NDSCartSlot.GetCart()) { + #ifdef RETROACHIEVEMENTS_ENABLED + auto cart = NDSCartSlot.GetCart(); + if (cart) + { + const char* h = cart->GetRAHash(); + if (h && h[0]) + RAContext::Get().SetPendingGameHash(h); + } +#endif + } } void NDS::SetNDSSave(const u8* savedata, u32 savelen) @@ -922,6 +946,7 @@ void NDS::RunSystemSleep(u64 timestamp) template u32 NDS::RunFrame() { + RAContext::Get().DoFrame(); Current = this; FrameStartTimestamp = SysTimestamp; @@ -1597,6 +1622,7 @@ void NDS::MonitorARM9Jump(u32 addr) { Log(LogLevel::Info, "Game is now booting\n"); RunningGame = true; + RAContext::Get().AttachNDS(this); } } } diff --git a/src/NDS.h b/src/NDS.h index 17e656e9e7..aa67567f80 100644 --- a/src/NDS.h +++ b/src/NDS.h @@ -48,6 +48,9 @@ // with this enabled, to make sure it doesn't desync //#define DEBUG_CHECK_DESYNC +class RAContext; +extern RAContext* g_RAContext; + namespace melonDS { struct NDSArgs; @@ -248,6 +251,8 @@ class NDS #endif public: // TODO: Encapsulate the rest of these members + bool IsGameRunning() const { return RunningGame; } + RAContext* ra = nullptr; void* UserData; int ConsoleType; diff --git a/src/NDSCart.cpp b/src/NDSCart.cpp index bfc07f9394..610b3806a5 100644 --- a/src/NDSCart.cpp +++ b/src/NDSCart.cpp @@ -26,6 +26,12 @@ #include "melonDLDI.h" #include "FATStorage.h" #include "Utils.h" +#include "RetroAchievements/RAClient.h" +#include + +namespace Config { + extern bool RA_Enabled; +} namespace melonDS { @@ -202,6 +208,18 @@ CartCommon::CartCommon(std::unique_ptr&& rom, u32 len, u32 chipid, bool ba CartType(type), UserData(userdata) { +if (ROM && ROMLength > 0) { + if (rc_hash_generate_from_buffer( + this->ra_hash, + RC_CONSOLE_NINTENDO_DS, + ROM.get(), + ROMLength)) + { + //printf("RetroAchievements: Hash gry to: %s\n", this->ra_hash); + + RAContext::Get().SetPendingGameHash(this->ra_hash); + } +} memcpy(&Header, ROM.get(), sizeof(Header)); IsDSi = Header.IsDSi() && !badDSiDump; DSiBase = Header.DSiRegionStart << 19; diff --git a/src/NDSCart.h b/src/NDSCart.h index 7e53e62e91..ae682ce901 100644 --- a/src/NDSCart.h +++ b/src/NDSCart.h @@ -76,6 +76,8 @@ struct NDSCartArgs class CartCommon { public: + const char* GetRAHash() const { return ra_hash; } + char ra_hash[33] = {0}; CartCommon(const u8* rom, u32 len, u32 chipid, bool badDSiDump, ROMListEntry romparams, CartType type, void* userdata); CartCommon(std::unique_ptr&& rom, u32 len, u32 chipid, bool badDSiDump, ROMListEntry romparams, CartType type, void* userdata); virtual ~CartCommon(); diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index b24f520b1e..0d53e7bb78 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -37,6 +37,11 @@ namespace Config { using namespace melonDS; +std::string RA_Username = ""; +std::string RA_Token = ""; +std::string RA_Password = ""; +bool RA_Enabled; +bool RA_HardcoreMode; const char* kConfigFile = "melonDS.toml"; @@ -45,11 +50,6 @@ const char* kLegacyUniqueConfigFile = "melonDS.%d.ini"; toml::value RootTable; -// --- Zmienne RetroAchievements --- -bool RA_Enabled; -bool RA_HardcoreMode; -std::string RA_Username; -std::string RA_Token; DefaultList DefaultInts = { @@ -344,7 +344,6 @@ LegacyEntry LegacyFile[] = {"", -1, "", false} }; -// --- NOWE FUNKCJE RETROACHIEVEMENTS --- void SyncRAConfig() { @@ -352,6 +351,7 @@ void SyncRAConfig() RA_Enabled = tbl.GetBool("RetroAchievements.Enabled"); RA_HardcoreMode = tbl.GetBool("RetroAchievements.HardcoreMode"); RA_Username = tbl.GetString("RetroAchievements.Username"); + RA_Password = tbl.GetString("RetroAchievements.Password"); RA_Token = tbl.GetString("RetroAchievements.Token"); } @@ -361,6 +361,7 @@ void SaveRAConfig() tbl.SetBool("RetroAchievements.Enabled", RA_Enabled); tbl.SetBool("RetroAchievements.HardcoreMode", RA_HardcoreMode); tbl.SetString("RetroAchievements.Username", RA_Username); + tbl.SetString("RetroAchievements.Password", RA_Password); tbl.SetString("RetroAchievements.Token", RA_Token); Save(); } diff --git a/src/frontend/qt_sdl/Config.h b/src/frontend/qt_sdl/Config.h index 354aaa1f4a..451bd69c09 100644 --- a/src/frontend/qt_sdl/Config.h +++ b/src/frontend/qt_sdl/Config.h @@ -29,15 +29,16 @@ namespace Config { -extern bool RA_Enabled; + extern bool RA_Enabled; extern bool RA_HardcoreMode; extern std::string RA_Username; extern std::string RA_Token; + extern std::string RA_Password; void SyncRAConfig(); void SaveRAConfig(); - void Save(); // Upewnij się, że to tu jest + void Save(); struct LegacyEntry { diff --git a/src/frontend/qt_sdl/EmuInstance.cpp b/src/frontend/qt_sdl/EmuInstance.cpp index d6d662fa93..30c0f8d2aa 100755 --- a/src/frontend/qt_sdl/EmuInstance.cpp +++ b/src/frontend/qt_sdl/EmuInstance.cpp @@ -47,6 +47,7 @@ #include "DSi_I2C.h" #include "FreeBIOS.h" #include "main.h" +#include "../../RetroAchievements/RAClient.h" using std::make_unique; using std::pair; @@ -158,6 +159,7 @@ EmuInstance::~EmuInstance() emuThread->emuExit(); emuThread->wait(); + RAContext::Get().Shutdown(); delete emuThread; net.UnregisterInstance(instanceID); diff --git a/src/frontend/qt_sdl/EmuThread.cpp b/src/frontend/qt_sdl/EmuThread.cpp index 06cf5ae155..fa8caeff38 100755 --- a/src/frontend/qt_sdl/EmuThread.cpp +++ b/src/frontend/qt_sdl/EmuThread.cpp @@ -51,6 +51,7 @@ #include "GPU3D_Soft.h" #include "GPU3D_OpenGL.h" #include "GPU3D_Compute.h" +#include "../../RetroAchievements/RAClient.h" #include "Savestate.h" @@ -344,11 +345,34 @@ void EmuThread::run() winUpdateCount = 0; } - if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) emuInstance->fastForwardToggled = !emuInstance->fastForwardToggled; - if (emuInstance->hotkeyPressed(HK_SlowMoToggle)) emuInstance->slowmoToggled = !emuInstance->slowmoToggled; + if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) { + if (Config::RA_Enabled && Config::RA_HardcoreMode) { + emuInstance->osdAddMessage(0xFFA0A0, "HARDCORE: Fast Forward is blocked!"); + } else { + emuInstance->fastForwardToggled = !emuInstance->fastForwardToggled; + } + } - bool enablefastforward = emuInstance->hotkeyDown(HK_FastForward) | emuInstance->fastForwardToggled; - bool enableslowmo = emuInstance->hotkeyDown(HK_SlowMo) | emuInstance->slowmoToggled; + if (emuInstance->hotkeyPressed(HK_SlowMoToggle)) { + if (Config::RA_Enabled && Config::RA_HardcoreMode) { + emuInstance->osdAddMessage(0xFFA0A0, "HARDCORE: Slow-mo is blocked!"); + } else { + emuInstance->slowmoToggled = !emuInstance->slowmoToggled; + + } + } + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + if (emuInstance->hotkeyPressed(HK_FastForward) || emuInstance->hotkeyPressed(HK_SlowMo)) + { + emuInstance->osdAddMessage(0xFFA0A0, "Speed manipulation is disabled in Hardcore Mode"); + } + } + bool enablefastforward = (emuInstance->hotkeyDown(HK_FastForward) | emuInstance->fastForwardToggled) + && !(Config::RA_Enabled && Config::RA_HardcoreMode); + + bool enableslowmo = (emuInstance->hotkeyDown(HK_SlowMo) | emuInstance->slowmoToggled) + && !(Config::RA_Enabled && Config::RA_HardcoreMode); if (useOpenGL) { @@ -627,20 +651,40 @@ void EmuThread::handleMessages() emuInstance->ejectGBACart(); break; - case msg_SaveState: + case msg_SaveState: + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Save states are disabled"); + break; + } msgResult = emuInstance->saveState(msg.param.value().toStdString()); break; - case msg_LoadState: + case msg_LoadState: + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Load states are disabled"); + break; + } msgResult = emuInstance->loadState(msg.param.value().toStdString()); break; - case msg_UndoStateLoad: + case msg_UndoStateLoad: + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Undo load is disabled"); + break; + } emuInstance->undoStateLoad(); msgResult = 1; break; - case msg_ImportSavefile: + case msg_ImportSavefile: + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Importing savefiles is disabled"); + break; + } { msgResult = 0; auto f = Platform::OpenFile(msg.param.value().toStdString(), Platform::FileMode::Read); @@ -660,7 +704,12 @@ void EmuThread::handleMessages() } break; - case msg_EnableCheats: + case msg_EnableCheats: + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + emuInstance->osdAddMessage(0xFFA0A0, "HARDCORE: Cheats are forbidden!"); + break; + } emuInstance->enableCheats(msg.param.value()); break; } @@ -715,6 +764,19 @@ void EmuThread::emuRun() void EmuThread::emuPause(bool broadcast) { + if (Config::RA_Enabled && Config::RA_HardcoreMode) + { + uint32_t frames_left = 0; + if (!RAContext::Get().CanPause(&frames_left)) + { + if (frames_left > 0) + { + float seconds = frames_left / 60.0f; + emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Wait %.1f seconds to pause", seconds); + } + return; + } + } sendMessage(msg_EmuPause); waitMessage(); @@ -753,6 +815,11 @@ void EmuThread::emuExit() void EmuThread::emuFrameStep() { + if (Config::RA_HardcoreMode) + { + emuInstance->osdAddMessage(0xFFA0A0, "Frame step is disabled in Hardcore Mode"); + return; + } if (emuPauseStack < emuPauseStackPauseThreshold) sendMessage(msg_EmuPause); sendMessage(msg_EmuFrameStep); diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 1c98b7e5a3..6da0d4a1ae 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -27,6 +27,12 @@ #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -81,6 +87,11 @@ #include "CameraManager.h" #include "Window.h" #include "AboutDialog.h" +#include "RetroAchievements/RAClient.h" +#include "toast/ToastManager.h" +#include "toast/BadgeCache.h" +//#include "retroachievements/AchievementsDialog.h" +//#include "../../RetroAchievements/RAClient.h" using namespace melonDS; @@ -236,7 +247,8 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : enabledSaved(false), focused(true) { -#ifndef _WIN32 + m_oldRAEnabled = Config::RA_Enabled; + #ifndef _WIN32 if (!parent) { if (socketpair(AF_UNIX, SOCK_STREAM, 0, signalFd)) @@ -255,7 +267,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : sa.sa_flags |= SA_RESTART; sigaction(SIGINT, &sa, 0); } -#endif + #endif showOSD = windowCfg.GetBool("ShowOSD"); @@ -264,12 +276,12 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : setAcceptDrops(true); setFocusPolicy(Qt::ClickFocus); -#if QT_VERSION_MAJOR == 6 && WIN32 + #if QT_VERSION_MAJOR == 6 && WIN32 // The "windows11" theme has pretty massive padding around menubar items, this makes Config and Help not fit in a window at 1x screen sizing // So let's reduce the padding a bit. if (QApplication::style()->name() == "windows11") setStyleSheet("QMenuBar::item { padding: 4px 8px; }"); -#endif + #endif //hasMenu = (!parent); hasMenu = true; @@ -619,11 +631,11 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actEmuSettings = menu->addAction("Emu settings"); connect(actEmuSettings, &QAction::triggered, this, &MainWindow::onOpenEmuSettings); -#ifdef __APPLE__ + #ifdef __APPLE__ actPreferences = menu->addAction("Preferences..."); connect(actPreferences, &QAction::triggered, this, &MainWindow::onOpenEmuSettings); actPreferences->setMenuRole(QAction::PreferencesRole); -#endif + #endif actInputConfig = menu->addAction("Input and hotkeys"); connect(actInputConfig, &QAction::triggered, this, &MainWindow::onOpenInputConfig); @@ -686,12 +698,12 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actMPNewInstance->setText("Fart"); } -#ifdef Q_OS_MAC + #ifdef Q_OS_MAC QPoint screenCenter = screen()->availableGeometry().center(); QRect frameGeo = frameGeometry(); frameGeo.moveCenter(screenCenter); move(frameGeo.topLeft()); -#endif + #endif std::string geom = windowCfg.GetString("Geometry"); if (!geom.empty()) @@ -707,6 +719,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : panel = nullptr; createScreenPanel(); + ToastManager::Get().Init(this); if (hasMenu) { @@ -789,11 +802,50 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actWifiSettings->setEnabled(false); actInterfaceSettings->setEnabled(false); -#ifdef __APPLE__ + #ifdef __APPLE__ actPreferences->setEnabled(false); -#endif // __APPLE__ + #endif // __APPLE__ } + RAContext::Get().SetOnGameLoadedCallback([this](){ + ShowGameLoadToast(); + }); + + RAContext::Get().onLoginResponse = [this](bool success, const std::string& message) { + ShowRALoginToast(success, message); + }; + + QTimer::singleShot(1000, this, []() + { + if (!Config::RA_Enabled) + return; + + ToastManager::Get().ShowAchievement( + "RetroAchievements", + "Enabled", + QPixmap(":/ra/icons/ra-icon.png") + ); + }); + + RAContext::Get().SetOnAchievementUnlocked( + [this](const char* title, + const char* desc, + const char* badgeUrl) + { + QMetaObject::invokeMethod( + this, + [this, + t = QString::fromUtf8(title), + d = QString::fromUtf8(desc), + b = QString::fromUtf8(badgeUrl)]() + { + OnAchievementUnlocked(t, d, b); + }, + Qt::QueuedConnection + ); + } + ); + if (emuThread->emuIsActive()) onEmuStart(); } @@ -1566,6 +1618,11 @@ void MainWindow::onEjectGBACart() void MainWindow::onSaveState() { + if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + { + emuInstance->osdAddMessage(0xFFA0A0, "Savestates states is disabled in Hardcore Mode"); + return; + } int slot = ((QAction*)sender())->data().toInt(); QString filename; @@ -1601,6 +1658,11 @@ void MainWindow::onSaveState() void MainWindow::onLoadState() { + if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + { + emuInstance->osdAddMessage(0xFFA0A0, "Savestates disabled in Hardcore Mode"); + return; + } int slot = ((QAction*)sender())->data().toInt(); QString filename; @@ -1613,9 +1675,9 @@ void MainWindow::onLoadState() // TODO: specific 'last directory' for savestate files? emuThread->emuPause(); filename = QFileDialog::getOpenFileName(this, - "Load state", - globalCfg.GetQString("LastROMFolder"), - "melonDS savestates (*.ml*);;Any file (*.*)"); + "Load state", + globalCfg.GetQString("LastROMFolder"), + "melonDS savestates (*.ml*);;Any file (*.*)"); emuThread->emuUnpause(); if (filename.isEmpty()) return; @@ -1651,6 +1713,11 @@ void MainWindow::onUndoStateLoad() void MainWindow::onImportSavefile() { + if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + { + QMessageBox::critical(this, "melonDS", "Importing savefiles is disabled in Hardcore Mode."); + return; + } QString path = QFileDialog::getOpenFileName(this, "Select savefile", globalCfg.GetQString("LastROMFolder"), @@ -1742,6 +1809,12 @@ void MainWindow::onOpenPowerManagement() void MainWindow::onEnableCheats(bool checked) { + if (checked && Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + { + emuInstance->osdAddMessage(0xFFA0A0, "Cheats are disabled in Hardcore Mode"); + actEnableCheats->setChecked(false); + return; + }- localCfg.SetBool("EnableCheats", checked); emuThread->enableCheats(checked); @@ -1753,8 +1826,13 @@ void MainWindow::onEnableCheats(bool checked) void MainWindow::onSetupCheats() { + if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + { + emuInstance->osdAddMessage(0xFFA0A0, "Cheat menu is disabled in Hardcore Mode"); + return; + } emuThread->emuPause(); - + CheatsDialog* dlg = CheatsDialog::openDlg(this); connect(dlg, &CheatsDialog::finished, this, &MainWindow::onCheatsDialogFinished); } @@ -1882,6 +1960,42 @@ void MainWindow::onEmuSettingsDialogFinished(int res) if (!emuThread->emuIsActive()) actTitleManager->setEnabled(!globalCfg.GetString("DSi.NANDPath").empty()); + const bool newRAEnabled = Config::RA_Enabled; + + if (m_oldRAEnabled != newRAEnabled) + { + if (newRAEnabled) + { + + std::string user = globalCfg.GetString("RetroAchievements.Username"); + std::string pass = globalCfg.GetString("RetroAchievements.Password"); + bool hardcore = globalCfg.GetBool("RetroAchievements.Hardcore"); + + RAContext::Get().SetCredentials( + user.c_str(), + pass.c_str(), + hardcore + ); + + RAContext::Get().Enable(); + ToastManager::Get().ShowAchievement( + "RetroAchievements", + "Enabled", + QPixmap(":/ra/icons/ra-icon.png") + ); + } + else + { + ToastManager::Get().ShowAchievement( + "RetroAchievements", + "Disabled", + QPixmap(":/ra/icons/ra-icon.png") + ); + RAContext::Get().Disable(); + } + m_oldRAEnabled = newRAEnabled; + } + emuThread->emuUnpause(); } @@ -2317,6 +2431,120 @@ void MainWindow::onEmuReset() actUndoStateLoad->setEnabled(false); } +void MainWindow::OnAchievementUnlocked( + const QString& title, + const QString& desc, + const QString& badgeUrl) +{ + BadgeCache::Get().DownloadBadge(badgeUrl, + [title, desc](const QPixmap& pix) + { + ToastManager::Get().ShowAchievement(title, desc, pix); + }); +} + +void MainWindow::ShowRALoginToast(bool success, const std::string& message) +{ + QString title = "RetroAchievements"; + QString toastMsg; + + if (success) { + toastMsg = "Logged In!"; + } else { + toastMsg = QString("Login Error: %1").arg(QString::fromStdString(message)); + } + + QMetaObject::invokeMethod(QApplication::instance(), [title, toastMsg]() { + ToastManager::Get().ShowAchievement( + title, + toastMsg, + QPixmap(":/ra/icons/ra-icon.png") + ); + }); +} + +void MainWindow::ShowGameLoadToast() +{ + + if (RAContext::Get().pendingLoadFailed) + { + QString errorMsg = QString::fromStdString(RAContext::Get().pendingLoadError); + + QTimer::singleShot(100, this, [errorMsg]() + { + QMetaObject::invokeMethod(QApplication::instance(), [errorMsg]() + { + ToastManager::Get().ShowAchievement( + "RetroAchievements", + QString("ERROR: %1").arg(errorMsg), + QPixmap(":/ra/icons/ra-icon.png") + ); + }); + }); + + return; + } + + const rc_client_game_t* game = RAContext::Get().GetCurrentGameInfo(); + if (!game) + return; + + QString title = QString::fromUtf8(game->title); + QString badgeUrl = QString::fromUtf8(game->badge_url); + + int totalAchievements = 0; + int unlockedAchievements = 0; + + rc_client_achievement_list_t* achList = + rc_client_create_achievement_list( + RAContext::Get().client, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + + if (achList) + { + for (uint32_t b = 0; b < achList->num_buckets; ++b) + { + const rc_client_achievement_bucket_t* bucket = + &achList->buckets[b]; + + for (uint32_t i = 0; i < bucket->num_achievements; ++i) + { + const rc_client_achievement_t* ach = + bucket->achievements[i]; + + ++totalAchievements; + + if (ach && ach->unlocked) + ++unlockedAchievements; + } + } + + rc_client_destroy_achievement_list(achList); + } + + //stupid function to subtract 1 from achiv because they are showing up incorectly + if (totalAchievements > 0) totalAchievements -= 1; + if (unlockedAchievements > 0) unlockedAchievements -= 1; + + QString desc = QString("Game loaded! (%1/%2 Achievements)") + .arg(unlockedAchievements) + .arg(totalAchievements); + + QTimer::singleShot(100, this, [title, desc, badgeUrl]() + { + BadgeCache::Get().DownloadBadge(badgeUrl, + [title, desc](const QPixmap& pix) + { + QMetaObject::invokeMethod( + QApplication::instance(), + [title, desc, pix]() + { + ToastManager::Get().ShowAchievement(title, desc, pix); + }); + }); + }); +} void MainWindow::onUpdateVideoSettings(bool glchange) { diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index fd9acf657c..ac8d64a439 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -22,6 +22,7 @@ #include "glad/glad.h" #include "ScreenLayout.h" #include "duckstation/gl/context.h" +#include "toast/ToastManager.h" #include #include @@ -105,6 +106,9 @@ class MainWindow : public QMainWindow Q_OBJECT public: + void ShowRALoginToast(bool success, const std::string& message); + bool m_oldRAEnabled; + void showRALoginToast(); explicit MainWindow(int id, EmuInstance* inst, QWidget* parent = nullptr); ~MainWindow(); @@ -243,6 +247,8 @@ private slots: void onScreenEmphasisToggled(); private: + void ShowGameLoadToast(); + void OnAchievementUnlocked(const QString& title, const QString& desc, const QString& badgeUrl); virtual void closeEvent(QCloseEvent* event) override; QStringList currentROM; diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index ad6eca91f8..4e4bfa7dd2 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -64,6 +64,7 @@ #include "Net_PCap.h" #include "Net_Slirp.h" +#include "RetroAchievements/RAClient.h" using namespace melonDS; @@ -265,6 +266,23 @@ bool MelonApplication::event(QEvent *event) return QApplication::event(event); } +void MyFrontendLoginHandler() +{ + if (!Config::RA_Enabled) + return; + + EmuInstance* inst = emuInstances[0]; + if (!inst) return; + + NDS* nds = inst->getNDS(); + RAContext::Get().Init(nds); + RAContext::Get().SetCredentials( + Config::RA_Username.c_str(), + Config::RA_Password.c_str(), + Config::RA_HardcoreMode + ); + RAContext::Get().LoginNow(); +} int main(int argc, char** argv) { @@ -365,7 +383,8 @@ int main(int argc, char** argv) NetInit(); createEmuInstance(); - + MyFrontendLoginHandler(); + { MainWindow* win = emuInstances[0]->getMainWindow(); bool memberSyntaxUsed = false; @@ -410,3 +429,8 @@ int main(int argc, char** argv) SDL_Quit(); return ret; } + +extern "C" { + void SSL_set_quic_tls_transport_params(void* a, void* b, size_t c) {} + void SSL_set_quic_tls_cbs(void* a, void* b) {} +} \ No newline at end of file From ee8b670a9b7ebc279db82c579167098fd6a4930d Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:10:47 +0100 Subject: [PATCH 25/41] added new files and batch2 --- src/frontend/qt_sdl/CMakeLists.txt | 9 +++ src/frontend/qt_sdl/EmuSettingsDialog.cpp | 88 +++++++++++++++++++---- src/frontend/qt_sdl/EmuSettingsDialog.h | 16 +++++ src/frontend/qt_sdl/Screen.cpp | 12 ++-- 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index b8fa40cecc..9b07b13b8d 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -7,6 +7,14 @@ set(SOURCES_QT_SDL main_shaders.h Screen.cpp Window.cpp + toast/ToastManager.cpp + toast/ToastManager.h + toast/AchievementToast.cpp + toast/AchievementToast.h + toast/ToastOverlay.cpp + toast/ToastOverlay.h + toast/BadgeCache.h + toast/BadgeCache.cpp EmuInstance.cpp EmuInstanceAudio.cpp EmuInstanceInput.cpp @@ -18,6 +26,7 @@ set(SOURCES_QT_SDL EmuSettingsDialog.cpp PowerManagement/PowerManagementDialog.cpp PowerManagement/resources/battery.qrc + retroachievements/resources/ra.qrc InputConfig/InputConfigDialog.cpp InputConfig/MapButton.h InputConfig/resources/ds.qrc diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.cpp b/src/frontend/qt_sdl/EmuSettingsDialog.cpp index ba88408cb3..d6ed716c89 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.cpp +++ b/src/frontend/qt_sdl/EmuSettingsDialog.cpp @@ -32,6 +32,8 @@ #include "EmuSettingsDialog.h" #include "ui_EmuSettingsDialog.h" #include "main.h" +#include +#include "RetroAchievements/RAClient.h" using namespace melonDS::Platform; using namespace melonDS; @@ -176,26 +178,89 @@ EmuSettingsDialog::EmuSettingsDialog(QWidget* parent) : QDialog(parent), ui(new cbRAEnabled = new QCheckBox("Enable RetroAchievements"); cbRAHardcore = new QCheckBox("Hardcore Mode (No Savestates)"); leRAUsername = new QLineEdit(); - leRAToken = new QLineEdit(); - leRAToken->setEchoMode(QLineEdit::Password); // Ukrywa token - leRAToken->setPlaceholderText("Enter your API Token from RA website"); + leRAPassword = new QLineEdit(); + leRAPassword->setEchoMode(QLineEdit::Password); + leRAPassword->setPlaceholderText("Enter your RA password"); + + btnRALogin = new QPushButton(); + lblRAStatus = new QLabel(); + lblRAStatus->setStyleSheet("font-weight: bold; color: #2ecc71;"); raForm->addRow(cbRAEnabled); raForm->addRow(cbRAHardcore); raForm->addRow("Username:", leRAUsername); - raForm->addRow("Login Token:", leRAToken); + raForm->addRow("Password:", leRAPassword); + raForm->addRow("", btnRALogin); + raForm->addRow("", lblRAStatus); tabLayout->addWidget(groupRA); - tabLayout->addStretch(); // Spycha formularz do góry - - // Dodanie zakładki do głównego widgetu okna + tabLayout->addStretch(); + + QLabel* trademarkLabel = new QLabel("Integration added by PanMenel™"); + + trademarkLabel->setStyleSheet( + "QLabel {" + " color: #3498db;" + " font-weight: bold;" + " font-family: 'Segoe UI', sans-serif;" + " font-size: 11px;" + " letter-spacing: 2px;" + " padding: 10px;" + " border-top: 1px solid #333;" + "}" + ); + + trademarkLabel->setAlignment(Qt::AlignRight); + tabLayout->addWidget(trademarkLabel); ui->tabWidget->addTab(raTab, "RA"); - // Wczytanie obecnych wartości z konfiguracji + auto UpdateRAUI = [this]() { + if (RAContext::Get().IsLoggedIn()) { + btnRALogin->setText("Logout"); + const rc_client_user_t* user = rc_client_get_user_info(RAContext::Get().client); + if (user && user->display_name) + lblRAStatus->setText(QString("Logged in as: %1").arg(user->display_name)); + else + lblRAStatus->setText("Logged in"); + } else { + btnRALogin->setText("Login Now"); + lblRAStatus->setText("Not logged in"); + lblRAStatus->setStyleSheet("color: gray;"); + } + }; + + UpdateRAUI(); + + connect(btnRALogin, &QPushButton::clicked, this, [this, UpdateRAUI]() { + if (RAContext::Get().IsLoggedIn()) { + // LOGOUT + RAContext::Get().SetLoggedIn(false); + RAContext::Get().SetToken(""); + UpdateRAUI(); + } else { + // LOGIN + std::string user = leRAUsername->text().toStdString(); + std::string pass = leRAPassword->text().toStdString(); + if (user.empty() || pass.empty()) return; + RAContext::Get().LoginWithPassword(user.c_str(), pass.c_str(), cbRAHardcore->isChecked()); + } + }); + + MainWindow* mainWin = qobject_cast(this->parent()); + + RAContext::Get().onLoginResponse = [this, UpdateRAUI, mainWin](bool success, const std::string& msg) { + QMetaObject::invokeMethod(this, [this, UpdateRAUI, mainWin, success, msg]() { + UpdateRAUI(); + if (mainWin) { + mainWin->ShowRALoginToast(success, msg); + } + }); + }; + cbRAEnabled->setChecked(Config::RA_Enabled); cbRAHardcore->setChecked(Config::RA_HardcoreMode); leRAUsername->setText(QString::fromStdString(Config::RA_Username)); - leRAToken->setText(QString::fromStdString(Config::RA_Token)); + leRAPassword->setText(QString::fromStdString(Config::RA_Password)); #undef SET_ORIGVAL } @@ -260,13 +325,12 @@ void EmuSettingsDialog::done(int r) if (r == QDialog::Accepted) { - // Zapis ustawień RetroAchievements Config::RA_Enabled = cbRAEnabled->isChecked(); Config::RA_HardcoreMode = cbRAHardcore->isChecked(); Config::RA_Username = leRAUsername->text().toStdString(); - Config::RA_Token = leRAToken->text().toStdString(); + Config::RA_Password = leRAPassword->text().toStdString(); - Config::SaveRAConfig(); // Wywołanie Twojej funkcji zapisu z Config.cpp + Config::SaveRAConfig(); bool modified = false; #define CHECK_ORIGVAL(type, val) \ diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.h b/src/frontend/qt_sdl/EmuSettingsDialog.h index b3b792185e..a35861ce37 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.h +++ b/src/frontend/qt_sdl/EmuSettingsDialog.h @@ -21,6 +21,11 @@ #include +class QPushButton; +class QLabel; +class QCheckBox; +class QLineEdit; +class QGroupBox; namespace Ui { class EmuSettingsDialog; } class EmuSettingsDialog; @@ -82,12 +87,23 @@ private slots: void on_cbGdbEnabled_toggled(); private: + class QPushButton* btnRALogin; + class QLabel* lblRAStatus; + class QLabel* lblStatusText; + void UpdateLoginUI(); void verifyFirmware(); void updateLastBIOSFolder(QString& filename); Ui::EmuSettingsDialog* ui; EmuInstance* emuInstance; QString lastBIOSFolder; + + class QCheckBox* cbRAEnabled; + class QCheckBox* cbRAHardcore; + class QLineEdit* leRAUsername; + class QLineEdit* leRAToken; + class QLineEdit* leRAPassword; + class QGroupBox* groupRA; }; #endif // EMUSETTINGSDIALOG_H diff --git a/src/frontend/qt_sdl/Screen.cpp b/src/frontend/qt_sdl/Screen.cpp index 6d58fff981..25577aa71a 100755 --- a/src/frontend/qt_sdl/Screen.cpp +++ b/src/frontend/qt_sdl/Screen.cpp @@ -43,6 +43,8 @@ #include "OSD_shaders.h" #include "font.h" #include "version.h" +#include "RetroAchievements/RAClient.h" +#include using namespace melonDS; @@ -337,10 +339,10 @@ void ScreenPanel::tabletEvent(QTabletEvent* event) void ScreenPanel::touchEvent(QTouchEvent* event) { -#if QT_VERSION_MAJOR == 6 + #if QT_VERSION_MAJOR == 6 if (event->device()->type() == QInputDevice::DeviceType::TouchPad) return; -#endif + #endif event->accept(); if (!emuInstance->emuIsActive()) { touching = false; return; } @@ -349,15 +351,15 @@ void ScreenPanel::touchEvent(QTouchEvent* event) { case QEvent::TouchBegin: case QEvent::TouchUpdate: -#if QT_VERSION_MAJOR == 6 + #if QT_VERSION_MAJOR == 6 if (event->points().length() > 0) { QPointF lastPosition = event->points().first().lastPosition(); -#else + #else if (event->touchPoints().length() > 0) { QPointF lastPosition = event->touchPoints().first().lastPos(); -#endif + #endif int x = (int)lastPosition.x(); int y = (int)lastPosition.y(); From a81d479fe25441759d18d5c9f0bf14e4c3c41799 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:11:30 +0100 Subject: [PATCH 26/41] RA files --- src/RetroAchievements/RAClient.cpp | 588 +++ src/RetroAchievements/RAClient.h | 133 + src/RetroAchievements/RAFunctions.h | 11 + src/RetroAchievements/RAMemory.cpp | 29 + src/RetroAchievements/cacert.c | 5642 +++++++++++++++++++++++++++ 5 files changed, 6403 insertions(+) create mode 100644 src/RetroAchievements/RAClient.cpp create mode 100644 src/RetroAchievements/RAClient.h create mode 100644 src/RetroAchievements/RAFunctions.h create mode 100644 src/RetroAchievements/RAMemory.cpp create mode 100644 src/RetroAchievements/cacert.c diff --git a/src/RetroAchievements/RAClient.cpp b/src/RetroAchievements/RAClient.cpp new file mode 100644 index 0000000000..f7c7596396 --- /dev/null +++ b/src/RetroAchievements/RAClient.cpp @@ -0,0 +1,588 @@ +#include "RAClient.h" +#include "../NDS.h" +#include +#include +#include +#include +#include "../rcheevos/include/rc_consoles.h" +#include "../rcheevos/include/rc_client.h" +#include "RetroAchievements/cacert.c" + +extern const unsigned char _accacert[]; +extern const size_t _accacert_len; +static AchievementUnlockedCallback g_onAchievementUnlocked; +std::function m_onLogin; +std::string m_displayName; +void SetDisplayName(const char* name) +{ + m_displayName = name ? name : ""; +} + +const std::string& RAContext::GetDisplayName() const +{ + return m_displayName; +} +static uint64_t g_ra_mem_reads = 0; +static uint64_t g_ra_mem_logged = 0; + +// ================= cURL helper ================= + +static size_t CurlWrite(void* ptr, size_t size, size_t nmemb, void* userdata) +{ + std::string* s = static_cast(userdata); + s->append(static_cast(ptr), size * nmemb); + return size * nmemb; +} + +// ================= RAContext ================= + +bool RAContext::CanPause(uint32_t* frames_remaining) { + if (!client) return true; // Zmienione z m_Client na client + return rc_client_can_pause(client, frames_remaining) != 0; +} + +void RAContext::SetPaused(bool paused) { + m_IsPaused = paused; +} + +RAContext::RAContext() : nds(nullptr), client(nullptr) +{ + curl_global_init(CURL_GLOBAL_ALL); +} + +RAContext::~RAContext() +{ + Shutdown(); + curl_global_cleanup(); +} + +void RAContext::Init(melonDS::NDS* nds_) +{ + nds = nds_; + client = rc_client_create( + &RAContext::ReadMemory, + &RAContext::ServerCall + ); + // NDS = Nintendo DS + rc_client_set_userdata(client, this); // ← TO JEST KLUCZ + rc_client_set_allow_background_memory_reads(client, 1); + + // 3. 🔥 TUTAJ WKLEJAMY OBSŁUGĘ ZDARZEŃ (EVENT HANDLER) 🔥 + rc_client_set_event_handler(client, + [](const rc_client_event_t* event, rc_client_t* client) + { + switch (event->type) + { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + { + const rc_client_achievement_t* ach = + (const rc_client_achievement_t*)event->achievement; + printf("[RA] ACHIEVEMENT TRIGGERED LOCAL: %s\n", ach->title); + + if (g_onAchievementUnlocked) + { + g_onAchievementUnlocked( + ach->title, + ach->description, + ach->badge_url + ); + } + break; + } + + default: + break; + } +}); + + //printf("[RA] Client initialized (rcheevos 12+)\n"); +} + +void RAContext::Shutdown() +{ + if (client) { + rc_client_destroy(client); + client = nullptr; + } +} + +void RAContext::DoFrame() +{ + // 1. Zostawiamy Twoje podstawowe warunki + if (!nds || !client || m_IsPaused) + return; + + // 2. Zostawiamy Twoją logikę sprawdzania hashu + std::string currentHash = pendingGameHash.value_or(""); + if (currentHash != m_lastHash) { + gameLoaded = false; + m_lastHash = currentHash; + pendingLoadFailed = false; + isLoading = false; + } + + // 3. Zostawiamy Twoje blokady ładowania + if (pendingLoadFailed || isLoading) return; + + // 4. Zostawiamy Twoje wywołanie LoadGame + if (!gameLoaded && + pendingGameHash.has_value() && + nds->IsGameRunning()) + { + isLoading = true; + LoadGame(pendingGameHash->c_str()); + return; // <--- Dodałem to, żeby nie leciał dalej w tej samej klatce + } + + // --- MOJA GŁÓWNA POPRAWKA PONIŻEJ --- + + // 5. Blokada: dopóki gra nie jest w pełni rozpoznana, nie mielimy klatek RA + if (!gameLoaded) return; + + // 6. Twoje sprawdzenie, czy logika jest wymagana (teraz w dobrym miejscu) + if (rc_client_is_processing_required(client) == 0) { + return; + } + + // 7. Dopiero na końcu faktyczne przetwarzanie klatki + rc_client_do_frame(client); +} +// ================= Login / Load ================= + + void RAContext::SetLoggedIn(bool v) + { + if (m_logged_in == v) + return; + //printf("zalogowano i wykonano setloggedin"); + m_logged_in = v; + + if (m_onLogin) + m_onLogin(); + } + +static void LoginPasswordCallback(int result, const char* err, rc_client_t* client, void* userdata) +{ + RAContext* ctx = static_cast(userdata); + if (result == RC_OK) + { + const rc_client_user_t* user_info = rc_client_get_user_info(client); + printf("[RA] Logged in as %s\n", user_info->display_name); + if (ctx) + { + //printf("[RA] Login OK, waiting for game hash\n"); + } + //const rc_client_game_t* game = rc_client_get_game(client); + // Zapisz otrzymany token z RA + ctx->SetToken(user_info->token ? user_info->token : ""); + ctx->SetLoggedIn(true); + + // Możemy zapisać token w configu + //printf("[RA] New token saved: %s\n", ctx->GetToken().c_str()); + if (ctx->onLoginResponse) { + ctx->onLoginResponse(true, "Zalogowano pomyślnie!"); + } + } + else + { + ctx->loginSuccess = false; + ctx->loginErrorMessage = err ? err : "Unknown error"; + ctx->SetLoggedIn(false); + printf("[RA] Login failed: %s\n", err ? err : "unknown error"); + if (ctx->onLoginResponse) { + ctx->onLoginResponse(false, ctx->loginErrorMessage); + } + } +} + + + +void RAContext::LoadGame(const char* hash) +{ + if (!client) return; + //printf("[RA] LoadGame called with hash: %s\n", hash); + + // Dodajemy [this], aby lambda widziała zmienną gameLoaded + rc_client_begin_load_game( + client, + hash, + [](int result, const char* err, rc_client_t*, void* userdata) { + // Rzutujemy userdata z powrotem na RAContext*, aby mieć dostęp do klasy + auto* context = static_cast(userdata); + + if (result == 0) { + context->pendingLoadFailed = false; + context->isLoading = false; + printf("[RA] Game loaded\n"); + context->gameLoaded = true; // Używamy wskaźnika przekazanego przez userdata + context->currentGameInfo = rc_client_get_game_info(context->client); + //printf("[RA] Przzed Ongameloaded"); + if (context->onGameLoaded){ + //printf("[RA] W Ongameloaded"); + context->onGameLoaded();} + } + else { + printf("[RA] Load failed: %s\n", err); + context->gameLoaded = false; + context->pendingLoadError = err; + context->pendingLoadFailed = true; + context->isLoading = false; + if (context->onGameLoaded) { + context->onGameLoaded(); + } + + } + }, + this // Przekazujemy 'this' jako userdata + ); +} + +// ================= CALLBACKS rcheevos ================= + +uint32_t RAContext::ReadMemory(uint32_t address, + uint8_t* buffer, + uint32_t size, + rc_client_t* client) +{ + //printf("[RA][ReadMemory] called addr=%08X size=%u\n", address, size); + if (address >= 0x04000000 && address < 0x04000400) + { + //printf("[RA][IO READ] %08X\n", address); +} + g_ra_mem_reads++; + RAContext* ctx = + static_cast(rc_client_get_userdata(client)); + + if (buffer && size > 0) + memset(buffer, 0, size); + + if (!ctx || !ctx->nds || !buffer || size == 0) + return 0; + + // ===== ARM9 MAIN RAM ===== + // RA: 0x00000000 -> ARM9 0x02000000 (4MB) + constexpr uint32_t ARM9_RA_BASE = 0x00000000; + constexpr uint32_t ARM9_RA_SIZE = 0x00400000; + constexpr uint32_t ARM9_PHYS_BASE = 0x02000000; + + if (address >= ARM9_RA_BASE && address + size <= ARM9_RA_SIZE) + { + memcpy(buffer, + ctx->nds->MainRAM + address, + size); + /* + if (++g_ra_mem_logged % 10 == 0) + { + printf("[RA][ReadMemory][ARM9] reads=%llu addr=%08X size=%u val=%02X\n", + (unsigned long long)g_ra_mem_reads, + address, + size, + buffer[0]); + } + */ + + return size; + } + + // ===== WRAM ===== + // RA: 0x03000000 -> WRAM + constexpr uint32_t WRAM_RA_BASE = 0x03000000; + constexpr uint32_t WRAM_SIZE = 0x00008000; // 32KB + + if (address >= WRAM_RA_BASE && + address + size <= WRAM_RA_BASE + WRAM_SIZE) + { + uint32_t off = address - WRAM_RA_BASE; + memcpy(buffer, + ctx->nds->SharedWRAM + off, + size); + return size; + } + + // ===== ARM7 RAM (opcjonalne, ale bardzo zalecane) ===== + // RA: 0x01000000 -> ARM7 0x03800000 + constexpr uint32_t ARM7_RA_BASE = 0x01000000; + constexpr uint32_t ARM7_SIZE = 0x00010000; // 64KB + constexpr uint32_t ARM7_PHYS_BASE = 0x03800000; + + if (address >= ARM7_RA_BASE && + address + size <= ARM7_RA_BASE + ARM7_SIZE) + { + uint32_t off = address - ARM7_RA_BASE; + memcpy(buffer, + ctx->nds->ARM7WRAM + off, + size); + return size; + } + + /* + // ===== VRAM ===== + constexpr uint32_t VRAM_BASE = 0x06000000; + constexpr uint32_t VRAM_SIZE = 0x00180000; // 1.5 MB + if (address >= VRAM_BASE && address + size <= VRAM_BASE + VRAM_SIZE) + { + // w melonDS VRAM jest podzielone na banki, ale dla RA można narazie: + memset(buffer, 0, size); + return size; + } + + // ===== ARM9 IO ===== + constexpr uint32_t ARM9_IO_BASE = 0x04000000; + constexpr uint32_t ARM9_IO_SIZE = 0x00000400; // 1 KB + if (address >= ARM9_IO_BASE && address + size <= ARM9_IO_BASE + ARM9_IO_SIZE) + { + // tu można ewentualnie zwracać prawdziwe rejestry IO + // na razie tylko zero: + memset(buffer, 0, size); + return size; + } + */ + // ===== NIEOBSŁUGIWANE ===== + return 0; +} + +// funkcja do zapisu cacert do pliku tymczasowego +static std::string WriteCACertToTempFile() { +#ifdef _WIN32 + char tempPath[MAX_PATH]; + if (!GetTempPathA(MAX_PATH, tempPath)) { + return ""; + } + char tempFile[MAX_PATH]; + if (!GetTempFileNameA(tempPath, "ra", 0, tempFile)) { + return ""; + } + + FILE* f = fopen(tempFile, "wb"); + if (!f) return ""; + fwrite(_accacert, 1, _accacert_len, f); + fclose(f); + + return std::string(tempFile); +#else + char tmpName[] = "/tmp/ra_cacertXXXXXX"; + int fd = mkstemp(tmpName); + if (fd < 0) return ""; + + FILE* f = fdopen(fd, "wb"); + if (!f) return ""; + + fwrite(_accacert, 1, _accacert_len, f); + fclose(f); + + return std::string(tmpName); +#endif +} + +// funkcja do usuwania tymczasowego pliku +static void RemoveTempFile(const std::string& path) { + if (!path.empty()) { +#ifdef _WIN32 + DeleteFileA(path.c_str()); +#else + unlink(path.c_str()); +#endif + } +} + +// główna funkcja +void RAContext::ServerCall(const rc_api_request_t* request, + rc_client_server_callback_t callback, + void* userdata, + rc_client_t*) +{ + CURL* curl = curl_easy_init(); + if (!curl) { + callback(nullptr, userdata); + return; + } + + // zapisz cacert do pliku tymczasowego + std::string cacertPath = WriteCACertToTempFile(); + if (cacertPath.empty()) { + printf("[RA] Failed to create temp file for CA cert!\n"); + callback(nullptr, userdata); + curl_easy_cleanup(curl); + return; + } + + curl_easy_setopt(curl, CURLOPT_CAINFO, cacertPath.c_str()); + + std::string response_body; + curl_easy_setopt(curl, CURLOPT_USERAGENT, "melonDS/1.1 rcheevos/1.0"); + curl_easy_setopt(curl, CURLOPT_URL, request->url); + + if (request->post_data) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->post_data); + } + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + + CURLcode res = curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (res == CURLE_OK) { + rc_api_server_response_t api_resp; + api_resp.body = response_body.c_str(); + api_resp.body_length = static_cast(response_body.length()); + api_resp.http_status_code = static_cast(http_code); + callback(&api_resp, userdata); + } else { + printf("[RA] cURL Error %d: %s\n", res, curl_easy_strerror(res)); + callback(nullptr, userdata); + } + + curl_easy_cleanup(curl); + RemoveTempFile(cacertPath); +} +///////RESET +void RAContext::Reset() { + if (client) + rc_client_reset(client); +} + +void RAContext::DisableHardcore(const char* reason) +{ + if (client && rc_client_get_hardcore_enabled(client)) + { + rc_client_set_hardcore_enabled(client, 0); + printf("[RA] Hardcore disabled: %s\n", reason); + } +} +void RAContext::LoginWithPassword(const char* username, const char* password, bool hardcore) +{ + if (!client) return; + + // hardcore ustawiamy wcześniej + rc_client_set_hardcore_enabled(client, hardcore ? 1 : 0); + + // Wywołujemy logowanie hasłem + rc_client_begin_login_with_password( + client, + username, + password, + &LoginPasswordCallback, + this + ); +} +void RAContext::SetCredentials(const char* user, const char* password, bool hardcore) { + m_user = user; + m_password = password; + m_hardcore = hardcore; + //printf("[RA] Credentials set. user='%s'\n", m_user.c_str()); +} + +void RAContext::SetPendingGameHash(const char* hash) +{ + if (!hash || !*hash) + return; + + pendingGameHash = std::string(hash); + gameLoaded = false; + + //printf("[RA] Pending game hash set: %s\n", hash); +} +/* +void RAContext::SetPendingGame(const char* hash) +{ + pendingGameHash = hash; + + if (IsLoggedIn()) + { + LoadGame(pendingGameHash.c_str()); + } +} +*/ +void RAContext::AttachNDS(melonDS::NDS* nds_) +{ + if (nds == nds_) + return; + + nds = nds_; + //printf("[RA] NDS attached: %p\n", (void*)nds); +} +RAContext& RAContext::Get() +{ + static RAContext instance; + return instance; +} + +void RAContext::Enable() +{ + if (m_enabled) + return; + + m_enabled = true; + + Init(nds); + + if (!IsLoggedIn()) + { + LoginNow(); + } +} + +void RAContext::Disable() +{ + if (!m_enabled) + return; + + m_enabled = false; + + Shutdown(); + m_logged_in = false; + gameLoaded = false; + pendingGameHash.reset(); + + printf("[RA] Disabled\n"); +} + +void RAContext::LoginNow() +{ + /* + if (!client) + { + // inicjalizacja klienta, podłączenie readmemory + client = rc_client_create(&RAContext::ReadMemory, &RAContext::ServerCall); + rc_client_set_userdata(client, this); + rc_client_set_allow_background_memory_reads(client, 1); + printf("[RA] Client initialized (rcheevos 12+)\n"); + } + */ + if (m_logged_in) + return; + + if (!m_user.empty() && !m_password.empty()) + { + //printf("[RA] LoginNow: password\n"); + LoginWithPassword(m_user.c_str(), m_password.c_str(), m_hardcore); + } + else + { + printf("[RA] LoginNow: no credentials\n"); + } +} + +void RAContext::SetOnAchievementUnlocked(AchievementUnlockedCallback cb) +{ + g_onAchievementUnlocked = std::move(cb); +} + +void RAContext::SetOnGameLoadedCallback(std::function cb) +{ + //printf("[RA] SetOnGameLoadedCallback called\n"); + onGameLoaded = std::move(cb); +} + +/* +void RAContext::SetOnLogin(LoginCallback cb) +{ + m_onLogin = std::move(cb); + + // jeśli już zalogowany, odpal callback od razu + if (m_logged_in && m_onLogin) + m_onLogin(); +} +*/ \ No newline at end of file diff --git a/src/RetroAchievements/RAClient.h b/src/RetroAchievements/RAClient.h new file mode 100644 index 0000000000..d04153e508 --- /dev/null +++ b/src/RetroAchievements/RAClient.h @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include +#include + + +using AchievementUnlockedCallback = + std::function; + +// forward declaration – ZERO include hell +namespace melonDS { class NDS; } + +class RAContext { +public: + bool m_IsPaused = false; + bool CanPause(uint32_t* frames_remaining); + void SetPaused(bool paused); + std::function onLoginResponse; + bool pendingLoginResult = false; // Czy mamy nowy wynik logowania do pokazania? + bool loginSuccess = false; // Czy się udało? + std::string loginErrorMessage; // Treść błędu jeśli się nie udało + bool isLoading = false; + std::string m_lastHash; + std::string pendingLoadError; + bool pendingLoadFailed = false; + void SetOnGameLoadedCallback(std::function cb); + std::function onGameLoaded; + const rc_client_game_t* GetCurrentGameInfo() const { return currentGameInfo; } + bool IsGameLoaded() const { return gameLoaded; } + void SetOnAchievementUnlocked(AchievementUnlockedCallback cb); + void LoginNow(); + void Enable(); + void Disable(); + bool IsEnabled() const { return m_enabled; } + using LoginCallback = std::function; + + void SetOnLogin(LoginCallback cb) + { + m_onLogin = std::move(cb); + } + //bool IsLoggedIn() const; + static RAContext& Get(); + + void AttachNDS(melonDS::NDS* nds_); + void SetPendingGameHash(const char* hash); + //void SetPendingGame(const char* hash); + void LoginOnce(); + void SetToken(const std::string& token) { m_token = token; } + void SetLoggedIn(bool v); + // Singleton + /* + static RAContext& Instance() { + return Get(); // Przekierowanie do jedynej słusznej instancji + } + + static RAContext& Instance() { + static RAContext instance; + return instance; + } + */ + + + + // Inicjalizacja i zamknięcie + void Init(melonDS::NDS* nds); + void Shutdown(); + + // Wywoływane co klatkę + void DoFrame(); + + // Logowanie hasłem + void LoginWithPassword(const char* username, const char* password, bool hardcore); + + // Ustawienia konta + void SetCredentials(const char* user, const char* password, bool hardcore); + + // Ładowanie gry po hash + void LoadGame(const char* hash); + + // Hardcore / reset + void Reset(); + void DisableHardcore(const char* reason); + + // Gettery + bool IsLoggedIn() const { return m_logged_in; } + const std::string& GetUser() const { return m_user; } + const std::string& GetToken() const { return m_token; } + const std::string& GetDisplayName() const; + + // Wskaźniki + melonDS::NDS* nds = nullptr; + rc_client_t* client = nullptr; + + //new + bool m_logged_in = false; +private: + // Konstruktor / Destruktor + RAContext(); + ~RAContext(); + const rc_client_game_t* currentGameInfo = nullptr; + bool m_enabled = false; + LoginCallback m_onLogin; + std::optional pendingGameHash; + // Dane konta i stan + bool gameLoaded = false; + bool Loaowaniegry = false; + std::string m_user; + std::string m_token; + std::string m_password; + + bool m_hardcore = false; + + + // Callbacki rcheevos + static uint32_t ReadMemory( + uint32_t address, + uint8_t* buffer, + uint32_t size, + rc_client_t* client + ); + + static void ServerCall( + const rc_api_request_t* request, + rc_client_server_callback_t callback, + void* userdata, + rc_client_t* client + ); +}; \ No newline at end of file diff --git a/src/RetroAchievements/RAFunctions.h b/src/RetroAchievements/RAFunctions.h new file mode 100644 index 0000000000..2318ce9b73 --- /dev/null +++ b/src/RetroAchievements/RAFunctions.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +struct RAContext; + +extern "C" uint32_t ra_read_memory(uint32_t addr, uint8_t* buf, uint32_t size, rc_client_t* client); +extern "C" void ra_server_call(const rc_api_request_t* request, + rc_client_server_callback_t callback, + void* userdata, + rc_client_t* client); diff --git a/src/RetroAchievements/RAMemory.cpp b/src/RetroAchievements/RAMemory.cpp new file mode 100644 index 0000000000..da60370dea --- /dev/null +++ b/src/RetroAchievements/RAMemory.cpp @@ -0,0 +1,29 @@ +/* +#include "RAClient.h" +#include "../NDS.h" +#include + +uint32_t ra_read_memory( + uint32_t address, + uint8_t* buffer, + uint32_t size, + rc_client_t* client) +{ + RAContext* ctx = + static_cast(rc_client_get_userdata(client)); + + if (!ctx) + return 0; + + melonDS::NDS* nds = ctx->nds; // ← BEZ GetNDS() + + if (!nds) { + memset(buffer, 0, size); + return size; + } + + // TODO: real mapping + memset(buffer, 0, size); + return size; +} + */ \ No newline at end of file diff --git a/src/RetroAchievements/cacert.c b/src/RetroAchievements/cacert.c new file mode 100644 index 0000000000..9d789c0ec7 --- /dev/null +++ b/src/RetroAchievements/cacert.c @@ -0,0 +1,5642 @@ +/* + C-file generated by Bin2C + Compiled: Nov 20 2025 at 10:44:24 + + Copyright (C) 2018 + Segger Microcontroller GmbH + www.segger.com + + The Embedded Experts +*/ + +const unsigned char _accacert[225076UL + 1] = { + 0x23, 0x23, 0x0A, 0x23, 0x23, 0x20, 0x42, 0x75, 0x6E, 0x64, 0x6C, 0x65, 0x20, 0x6F, 0x66, 0x20, 0x43, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x0A, 0x23, 0x23, 0x0A, + 0x23, 0x23, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x4D, 0x6F, 0x7A, 0x69, 0x6C, 0x6C, 0x61, 0x20, 0x61, 0x73, 0x20, 0x6F, 0x66, 0x3A, 0x20, + 0x54, 0x75, 0x65, 0x20, 0x44, 0x65, 0x63, 0x20, 0x20, 0x32, 0x20, 0x30, 0x34, 0x3A, 0x31, 0x32, 0x3A, 0x30, 0x32, 0x20, 0x32, 0x30, 0x32, 0x35, 0x20, 0x47, 0x4D, 0x54, 0x0A, 0x23, 0x23, 0x0A, 0x23, 0x23, 0x20, 0x46, 0x69, 0x6E, 0x64, 0x20, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x68, 0x65, 0x72, 0x65, 0x3A, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x63, 0x75, 0x72, 0x6C, 0x2E, 0x73, 0x65, 0x2F, 0x64, + 0x6F, 0x63, 0x73, 0x2F, 0x63, 0x61, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x2E, 0x68, 0x74, 0x6D, 0x6C, 0x0A, 0x23, 0x23, 0x0A, 0x23, 0x23, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x62, 0x75, 0x6E, 0x64, 0x6C, + 0x65, 0x20, 0x6F, 0x66, 0x20, 0x58, 0x2E, 0x35, 0x30, 0x39, 0x20, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x6F, 0x66, 0x20, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x0A, 0x23, 0x23, 0x20, 0x28, 0x43, 0x41, 0x29, 0x2E, 0x20, 0x54, 0x68, 0x65, 0x73, 0x65, 0x20, 0x77, 0x65, 0x72, 0x65, 0x20, 0x61, 0x75, + 0x74, 0x6F, 0x6D, 0x61, 0x74, 0x69, 0x63, 0x61, 0x6C, 0x6C, 0x79, 0x20, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x65, 0x64, 0x20, 0x66, 0x72, 0x6F, 0x6D, 0x20, 0x4D, 0x6F, 0x7A, 0x69, 0x6C, 0x6C, 0x61, 0x27, 0x73, 0x20, 0x72, 0x6F, 0x6F, + 0x74, 0x20, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x0A, 0x23, 0x23, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x28, 0x63, 0x65, 0x72, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x74, 0x78, 0x74, 0x29, 0x2E, 0x20, 0x20, + 0x54, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63, 0x61, 0x6E, 0x20, 0x62, 0x65, 0x20, 0x66, 0x6F, 0x75, 0x6E, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6D, 0x6F, 0x7A, 0x69, 0x6C, 0x6C, 0x61, 0x20, 0x73, 0x6F, + 0x75, 0x72, 0x63, 0x65, 0x20, 0x74, 0x72, 0x65, 0x65, 0x3A, 0x0A, 0x23, 0x23, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x72, 0x61, 0x77, 0x2E, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x75, 0x73, 0x65, 0x72, 0x63, 0x6F, 0x6E, 0x74, + 0x65, 0x6E, 0x74, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x6D, 0x6F, 0x7A, 0x69, 0x6C, 0x6C, 0x61, 0x2D, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6F, 0x78, 0x2F, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6F, 0x78, 0x2F, 0x72, 0x65, 0x66, 0x73, 0x2F, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x2F, 0x72, 0x65, 0x6C, 0x65, 0x61, 0x73, 0x65, 0x2F, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2F, 0x6E, 0x73, 0x73, 0x2F, 0x6C, 0x69, 0x62, 0x2F, 0x63, 0x6B, 0x66, 0x77, 0x2F, 0x62, 0x75, 0x69, 0x6C, 0x74, 0x69, 0x6E, + 0x73, 0x2F, 0x63, 0x65, 0x72, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2E, 0x74, 0x78, 0x74, 0x0A, 0x23, 0x23, 0x0A, 0x23, 0x23, 0x20, 0x49, 0x74, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x69, 0x6E, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x20, 0x69, 0x6E, 0x20, 0x50, 0x45, 0x4D, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x66, 0x6F, 0x72, 0x65, 0x0A, 0x23, 0x23, + 0x20, 0x63, 0x61, 0x6E, 0x20, 0x62, 0x65, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6C, 0x79, 0x20, 0x75, 0x73, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x63, 0x75, 0x72, 0x6C, 0x20, 0x2F, 0x20, 0x6C, 0x69, 0x62, 0x63, 0x75, 0x72, + 0x6C, 0x20, 0x2F, 0x20, 0x70, 0x68, 0x70, 0x5F, 0x63, 0x75, 0x72, 0x6C, 0x2C, 0x20, 0x6F, 0x72, 0x20, 0x77, 0x69, 0x74, 0x68, 0x0A, 0x23, 0x23, 0x20, 0x61, 0x6E, 0x20, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2B, 0x6D, 0x6F, 0x64, 0x5F, 0x73, + 0x73, 0x6C, 0x20, 0x77, 0x65, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x53, 0x53, 0x4C, 0x20, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6F, 0x6E, 0x2E, 0x0A, 0x23, 0x23, 0x20, 0x4A, 0x75, 0x73, 0x74, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, + 0x53, 0x4C, 0x43, 0x41, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x69, 0x6C, 0x65, 0x2E, 0x0A, 0x23, 0x23, 0x0A, 0x23, 0x23, 0x20, 0x43, 0x6F, 0x6E, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x64, 0x6F, + 0x6E, 0x65, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x6D, 0x6B, 0x2D, 0x63, 0x61, 0x2D, 0x62, 0x75, 0x6E, 0x64, 0x6C, 0x65, 0x2E, 0x70, 0x6C, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x20, 0x31, 0x2E, 0x33, 0x30, 0x2E, 0x0A, 0x23, 0x23, + 0x20, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x3A, 0x20, 0x61, 0x39, 0x30, 0x33, 0x62, 0x33, 0x63, 0x64, 0x30, 0x35, 0x32, 0x33, 0x31, 0x65, 0x33, 0x39, 0x33, 0x33, 0x32, 0x35, 0x31, 0x35, 0x65, 0x66, 0x37, 0x65, 0x62, 0x65, 0x33, 0x37, 0x65, + 0x36, 0x39, 0x37, 0x32, 0x36, 0x32, 0x66, 0x33, 0x39, 0x35, 0x31, 0x35, 0x61, 0x35, 0x32, 0x30, 0x31, 0x35, 0x63, 0x32, 0x33, 0x63, 0x36, 0x32, 0x38, 0x30, 0x35, 0x62, 0x37, 0x33, 0x63, 0x64, 0x30, 0x0A, 0x23, 0x23, 0x0A, 0x0A, 0x0A, 0x45, + 0x6E, 0x74, 0x72, 0x75, 0x73, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x6B, 0x54, 0x43, 0x43, 0x41, 0x33, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x45, 0x52, 0x57, 0x74, 0x51, 0x56, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x44, 0x43, 0x42, 0x73, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x55, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, + 0x34, 0x78, 0x4F, 0x54, 0x41, 0x33, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x4D, 0x48, 0x64, 0x33, 0x64, 0x79, 0x35, 0x6C, 0x62, 0x6E, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4C, 0x6D, 0x35, 0x6C, 0x64, 0x43, 0x39, 0x44, 0x55, 0x46, + 0x4D, 0x67, 0x61, 0x58, 0x4D, 0x67, 0x61, 0x57, 0x35, 0x6A, 0x62, 0x33, 0x4A, 0x77, 0x0A, 0x62, 0x33, 0x4A, 0x68, 0x64, 0x47, 0x56, 0x6B, 0x49, 0x47, 0x4A, 0x35, 0x49, 0x48, 0x4A, 0x6C, 0x5A, 0x6D, 0x56, 0x79, 0x5A, 0x57, 0x35, 0x6A, 0x5A, + 0x54, 0x45, 0x66, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x57, 0x4B, 0x47, 0x4D, 0x70, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x44, 0x59, 0x67, 0x52, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x77, 0x67, 0x53, + 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x74, 0x4D, 0x43, 0x73, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x6B, 0x52, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, + 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x32, 0x4D, 0x54, 0x45, 0x79, + 0x4E, 0x7A, 0x49, 0x77, 0x4D, 0x6A, 0x4D, 0x30, 0x0A, 0x4D, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x49, 0x32, 0x4D, 0x54, 0x45, 0x79, 0x4E, 0x7A, 0x49, 0x77, 0x4E, 0x54, 0x4D, 0x30, 0x4D, 0x6C, 0x6F, 0x77, 0x67, 0x62, 0x41, 0x78, 0x43, 0x7A, 0x41, + 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x59, 0x77, 0x46, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x31, 0x46, 0x62, 0x6E, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4C, 0x43, 0x42, + 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x0A, 0x4D, 0x54, 0x6B, 0x77, 0x4E, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x7A, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x35, 0x75, 0x5A, 0x58, + 0x51, 0x76, 0x51, 0x31, 0x42, 0x54, 0x49, 0x47, 0x6C, 0x7A, 0x49, 0x47, 0x6C, 0x75, 0x59, 0x32, 0x39, 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x6C, 0x5A, 0x43, 0x42, 0x69, 0x65, 0x53, 0x42, 0x79, 0x5A, 0x57, 0x5A, 0x6C, 0x63, 0x6D, + 0x56, 0x75, 0x0A, 0x59, 0x32, 0x55, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x46, 0x69, 0x68, 0x6A, 0x4B, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x41, 0x32, 0x49, 0x45, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, + 0x33, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4C, 0x54, 0x41, 0x72, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4A, 0x45, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x0A, + 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x0A, 0x41, 0x4C, 0x61, + 0x56, 0x74, 0x6B, 0x4E, 0x43, 0x2B, 0x73, 0x5A, 0x74, 0x4B, 0x6D, 0x39, 0x49, 0x33, 0x35, 0x52, 0x4D, 0x4F, 0x56, 0x63, 0x46, 0x37, 0x73, 0x4E, 0x35, 0x45, 0x55, 0x46, 0x6F, 0x4E, 0x75, 0x33, 0x73, 0x2F, 0x70, 0x6F, 0x42, 0x6A, 0x36, 0x45, + 0x34, 0x4B, 0x50, 0x7A, 0x33, 0x45, 0x45, 0x5A, 0x6D, 0x4C, 0x6B, 0x30, 0x65, 0x47, 0x72, 0x45, 0x61, 0x54, 0x73, 0x62, 0x52, 0x77, 0x4A, 0x57, 0x49, 0x73, 0x4D, 0x6E, 0x2F, 0x4D, 0x59, 0x73, 0x7A, 0x0A, 0x41, 0x39, 0x75, 0x33, 0x67, 0x33, + 0x73, 0x2B, 0x49, 0x49, 0x52, 0x65, 0x37, 0x62, 0x4A, 0x57, 0x4B, 0x4B, 0x66, 0x34, 0x34, 0x4C, 0x6C, 0x41, 0x63, 0x54, 0x66, 0x46, 0x79, 0x30, 0x63, 0x4F, 0x6C, 0x79, 0x70, 0x6F, 0x77, 0x43, 0x4B, 0x56, 0x59, 0x68, 0x58, 0x62, 0x52, 0x39, + 0x6E, 0x31, 0x30, 0x43, 0x76, 0x2F, 0x67, 0x6B, 0x76, 0x4A, 0x72, 0x54, 0x37, 0x65, 0x54, 0x4E, 0x75, 0x51, 0x67, 0x46, 0x41, 0x2F, 0x43, 0x59, 0x71, 0x45, 0x41, 0x4F, 0x77, 0x77, 0x0A, 0x43, 0x6A, 0x30, 0x59, 0x7A, 0x66, 0x76, 0x39, 0x4B, + 0x6C, 0x6D, 0x61, 0x49, 0x35, 0x55, 0x58, 0x4C, 0x45, 0x57, 0x65, 0x48, 0x32, 0x35, 0x44, 0x65, 0x57, 0x30, 0x4D, 0x58, 0x4A, 0x6A, 0x2B, 0x53, 0x4B, 0x66, 0x46, 0x49, 0x30, 0x64, 0x63, 0x58, 0x76, 0x31, 0x75, 0x35, 0x78, 0x36, 0x30, 0x39, + 0x6D, 0x68, 0x46, 0x30, 0x59, 0x61, 0x44, 0x57, 0x36, 0x4B, 0x4B, 0x6A, 0x62, 0x48, 0x6A, 0x4B, 0x59, 0x44, 0x2B, 0x4A, 0x58, 0x47, 0x49, 0x72, 0x62, 0x36, 0x38, 0x0A, 0x6A, 0x36, 0x78, 0x53, 0x6C, 0x6B, 0x75, 0x71, 0x55, 0x59, 0x33, 0x6B, + 0x45, 0x7A, 0x45, 0x5A, 0x36, 0x45, 0x35, 0x4E, 0x6E, 0x39, 0x75, 0x73, 0x73, 0x32, 0x72, 0x56, 0x76, 0x44, 0x6C, 0x55, 0x63, 0x63, 0x70, 0x36, 0x65, 0x6E, 0x2B, 0x51, 0x33, 0x58, 0x30, 0x64, 0x67, 0x4E, 0x6D, 0x42, 0x75, 0x31, 0x6B, 0x6D, + 0x77, 0x68, 0x48, 0x2B, 0x35, 0x70, 0x50, 0x69, 0x39, 0x34, 0x44, 0x6B, 0x5A, 0x66, 0x73, 0x30, 0x4E, 0x77, 0x34, 0x70, 0x67, 0x48, 0x42, 0x4E, 0x0A, 0x72, 0x7A, 0x69, 0x47, 0x4C, 0x70, 0x35, 0x2F, 0x56, 0x36, 0x2B, 0x65, 0x46, 0x36, 0x37, + 0x72, 0x48, 0x4D, 0x73, 0x6F, 0x49, 0x56, 0x2B, 0x32, 0x48, 0x4E, 0x6A, 0x6E, 0x6F, 0x67, 0x51, 0x69, 0x2B, 0x64, 0x50, 0x61, 0x32, 0x4D, 0x73, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4F, 0x42, 0x73, 0x44, 0x43, 0x42, 0x72, 0x54, 0x41, + 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x0A, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, + 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x72, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x41, 0x45, 0x4A, 0x44, 0x41, 0x69, 0x67, 0x41, 0x38, 0x79, 0x4D, 0x44, 0x41, 0x32, 0x4D, 0x54, 0x45, 0x79, 0x4E, 0x7A, 0x49, 0x77, 0x4D, 0x6A, 0x4D, 0x30, 0x4D, 0x6C, + 0x71, 0x42, 0x44, 0x7A, 0x49, 0x77, 0x4D, 0x6A, 0x59, 0x78, 0x4D, 0x54, 0x49, 0x33, 0x4D, 0x6A, 0x41, 0x31, 0x0A, 0x4D, 0x7A, 0x51, 0x79, 0x57, 0x6A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, + 0x42, 0x52, 0x6F, 0x6B, 0x4F, 0x52, 0x6E, 0x70, 0x4B, 0x5A, 0x54, 0x67, 0x4D, 0x65, 0x47, 0x5A, 0x71, 0x54, 0x78, 0x39, 0x30, 0x74, 0x44, 0x2B, 0x34, 0x53, 0x39, 0x62, 0x54, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, + 0x67, 0x51, 0x55, 0x61, 0x4A, 0x44, 0x6B, 0x5A, 0x36, 0x53, 0x6D, 0x55, 0x34, 0x44, 0x48, 0x0A, 0x68, 0x6D, 0x61, 0x6B, 0x38, 0x66, 0x64, 0x4C, 0x51, 0x2F, 0x75, 0x45, 0x76, 0x57, 0x30, 0x77, 0x48, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, + 0x68, 0x76, 0x5A, 0x39, 0x42, 0x30, 0x45, 0x41, 0x42, 0x42, 0x41, 0x77, 0x44, 0x68, 0x73, 0x49, 0x56, 0x6A, 0x63, 0x75, 0x4D, 0x54, 0x6F, 0x30, 0x4C, 0x6A, 0x41, 0x44, 0x41, 0x67, 0x53, 0x51, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, + 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x0A, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, 0x54, 0x31, 0x44, 0x43, 0x77, 0x31, 0x77, 0x4D, 0x67, 0x4B, 0x74, 0x44, 0x35, 0x59, 0x2B, 0x69, 0x52, 0x44, 0x41, 0x55, + 0x67, 0x71, 0x56, 0x38, 0x5A, 0x79, 0x6E, 0x74, 0x79, 0x54, 0x74, 0x53, 0x78, 0x32, 0x39, 0x43, 0x57, 0x2B, 0x31, 0x52, 0x61, 0x47, 0x53, 0x77, 0x4D, 0x43, 0x50, 0x65, 0x79, 0x76, 0x49, 0x57, 0x6F, 0x6E, 0x58, 0x39, 0x74, 0x4F, 0x31, 0x4B, + 0x7A, 0x4B, 0x74, 0x76, 0x6E, 0x31, 0x49, 0x53, 0x4D, 0x0A, 0x59, 0x2F, 0x59, 0x50, 0x79, 0x79, 0x59, 0x42, 0x6B, 0x56, 0x42, 0x73, 0x39, 0x46, 0x38, 0x55, 0x34, 0x70, 0x4E, 0x30, 0x77, 0x42, 0x4F, 0x65, 0x4D, 0x44, 0x70, 0x51, 0x34, 0x37, + 0x52, 0x67, 0x78, 0x52, 0x7A, 0x77, 0x49, 0x6B, 0x53, 0x4E, 0x63, 0x55, 0x65, 0x73, 0x79, 0x42, 0x72, 0x4A, 0x36, 0x5A, 0x75, 0x61, 0x41, 0x47, 0x41, 0x54, 0x2F, 0x33, 0x42, 0x2B, 0x58, 0x78, 0x46, 0x4E, 0x53, 0x52, 0x75, 0x7A, 0x46, 0x56, + 0x4A, 0x37, 0x79, 0x56, 0x54, 0x61, 0x0A, 0x76, 0x35, 0x32, 0x56, 0x72, 0x32, 0x75, 0x61, 0x32, 0x4A, 0x37, 0x70, 0x38, 0x65, 0x52, 0x44, 0x6A, 0x65, 0x49, 0x52, 0x52, 0x44, 0x71, 0x2F, 0x72, 0x37, 0x32, 0x44, 0x51, 0x6E, 0x4E, 0x53, 0x69, + 0x36, 0x71, 0x37, 0x70, 0x79, 0x6E, 0x50, 0x39, 0x57, 0x51, 0x63, 0x43, 0x6B, 0x33, 0x52, 0x76, 0x4B, 0x71, 0x73, 0x6E, 0x79, 0x72, 0x51, 0x2F, 0x33, 0x39, 0x2F, 0x32, 0x6E, 0x33, 0x71, 0x73, 0x65, 0x30, 0x77, 0x4A, 0x63, 0x47, 0x45, 0x32, + 0x6A, 0x54, 0x53, 0x0A, 0x57, 0x33, 0x69, 0x44, 0x56, 0x75, 0x79, 0x63, 0x4E, 0x73, 0x4D, 0x6D, 0x34, 0x68, 0x48, 0x32, 0x5A, 0x30, 0x6B, 0x64, 0x6B, 0x71, 0x75, 0x4D, 0x2B, 0x2B, 0x76, 0x2F, 0x65, 0x75, 0x36, 0x46, 0x53, 0x71, 0x64, 0x51, + 0x67, 0x50, 0x43, 0x6E, 0x58, 0x45, 0x71, 0x55, 0x4C, 0x6C, 0x38, 0x46, 0x6D, 0x54, 0x78, 0x53, 0x51, 0x65, 0x44, 0x4E, 0x74, 0x47, 0x50, 0x50, 0x41, 0x55, 0x4F, 0x36, 0x6E, 0x49, 0x50, 0x63, 0x6A, 0x32, 0x41, 0x37, 0x38, 0x31, 0x71, 0x30, + 0x0A, 0x74, 0x48, 0x75, 0x75, 0x32, 0x67, 0x75, 0x51, 0x4F, 0x48, 0x58, 0x76, 0x67, 0x52, 0x31, 0x6D, 0x30, 0x76, 0x64, 0x58, 0x63, 0x44, 0x61, 0x7A, 0x76, 0x2F, 0x77, 0x6F, 0x72, 0x33, 0x45, 0x6C, 0x68, 0x56, 0x73, 0x54, 0x2F, 0x68, 0x35, + 0x2F, 0x57, 0x72, 0x51, 0x38, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x51, 0x75, 0x6F, 0x56, 0x61, 0x64, 0x69, + 0x73, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, + 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x74, 0x7A, 0x43, 0x43, 0x41, 0x35, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x43, 0x42, + 0x51, 0x6B, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x77, 0x52, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, + 0x6B, 0x30, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x0A, 0x45, 0x46, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, + 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x6C, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x65, + 0x46, 0x77, 0x30, 0x77, 0x4E, 0x6A, 0x45, 0x78, 0x4D, 0x6A, 0x51, 0x78, 0x0A, 0x4F, 0x44, 0x49, 0x33, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x54, 0x45, 0x78, 0x4D, 0x6A, 0x51, 0x78, 0x4F, 0x44, 0x49, 0x7A, 0x4D, 0x7A, 0x4E, + 0x61, 0x4D, 0x45, 0x55, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4A, 0x4E, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x52, 0x64, 0x57, 0x39, + 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, 0x4D, 0x0A, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x52, 0x73, 0x77, 0x47, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4A, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, + 0x52, 0x70, 0x63, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, + 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x0A, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x61, 0x47, 0x4D, 0x70, 0x4C, 0x6C, 0x41, 0x30, 0x41, 0x4C, 0x61, 0x38, 0x44, 0x4B, 0x59, 0x72, 0x77, 0x44, + 0x34, 0x48, 0x49, 0x72, 0x6B, 0x77, 0x5A, 0x68, 0x52, 0x30, 0x49, 0x6E, 0x36, 0x73, 0x70, 0x52, 0x49, 0x58, 0x7A, 0x4C, 0x34, 0x47, 0x74, 0x4D, 0x68, 0x36, 0x51, 0x52, 0x72, 0x2B, 0x6A, 0x68, 0x69, 0x59, 0x61, 0x48, 0x76, 0x35, 0x2B, 0x48, + 0x42, 0x67, 0x36, 0x0A, 0x58, 0x4A, 0x78, 0x67, 0x46, 0x79, 0x6F, 0x36, 0x64, 0x49, 0x4D, 0x7A, 0x4D, 0x48, 0x31, 0x68, 0x56, 0x42, 0x48, 0x4C, 0x37, 0x61, 0x76, 0x67, 0x35, 0x74, 0x4B, 0x69, 0x66, 0x76, 0x56, 0x72, 0x62, 0x78, 0x69, 0x33, + 0x43, 0x67, 0x73, 0x74, 0x2F, 0x65, 0x6B, 0x2B, 0x37, 0x77, 0x72, 0x47, 0x73, 0x78, 0x44, 0x70, 0x33, 0x4D, 0x4A, 0x47, 0x46, 0x2F, 0x68, 0x64, 0x2F, 0x61, 0x54, 0x61, 0x2F, 0x35, 0x35, 0x4A, 0x57, 0x70, 0x7A, 0x6D, 0x4D, 0x2B, 0x59, 0x6B, + 0x0A, 0x6C, 0x76, 0x63, 0x2F, 0x75, 0x6C, 0x73, 0x72, 0x48, 0x48, 0x6F, 0x31, 0x77, 0x74, 0x5A, 0x6E, 0x2F, 0x71, 0x74, 0x6D, 0x55, 0x49, 0x74, 0x74, 0x4B, 0x47, 0x41, 0x72, 0x37, 0x39, 0x64, 0x67, 0x77, 0x38, 0x65, 0x54, 0x76, 0x49, 0x30, + 0x32, 0x6B, 0x66, 0x4E, 0x2F, 0x2B, 0x4E, 0x73, 0x52, 0x45, 0x38, 0x53, 0x63, 0x64, 0x33, 0x62, 0x42, 0x72, 0x72, 0x63, 0x43, 0x61, 0x6F, 0x46, 0x36, 0x71, 0x55, 0x57, 0x44, 0x34, 0x67, 0x58, 0x6D, 0x75, 0x56, 0x62, 0x42, 0x0A, 0x6C, 0x44, + 0x65, 0x50, 0x53, 0x48, 0x46, 0x6A, 0x49, 0x75, 0x77, 0x58, 0x5A, 0x51, 0x65, 0x56, 0x69, 0x6B, 0x76, 0x66, 0x6A, 0x38, 0x5A, 0x61, 0x43, 0x75, 0x57, 0x77, 0x34, 0x31, 0x39, 0x65, 0x61, 0x78, 0x47, 0x72, 0x44, 0x50, 0x6D, 0x46, 0x36, 0x30, + 0x54, 0x70, 0x2B, 0x41, 0x52, 0x7A, 0x38, 0x75, 0x6E, 0x2B, 0x58, 0x4A, 0x69, 0x4D, 0x39, 0x58, 0x4F, 0x76, 0x61, 0x37, 0x52, 0x2B, 0x7A, 0x64, 0x52, 0x63, 0x41, 0x69, 0x74, 0x4D, 0x4F, 0x65, 0x47, 0x79, 0x0A, 0x6C, 0x5A, 0x55, 0x74, 0x51, + 0x6F, 0x66, 0x58, 0x31, 0x62, 0x4F, 0x51, 0x51, 0x37, 0x64, 0x73, 0x45, 0x2F, 0x48, 0x65, 0x33, 0x66, 0x62, 0x45, 0x2B, 0x49, 0x6B, 0x2F, 0x30, 0x58, 0x58, 0x31, 0x6B, 0x73, 0x4F, 0x52, 0x31, 0x59, 0x71, 0x49, 0x30, 0x4A, 0x44, 0x73, 0x33, + 0x47, 0x33, 0x65, 0x69, 0x63, 0x4A, 0x6C, 0x63, 0x5A, 0x61, 0x4C, 0x44, 0x51, 0x50, 0x39, 0x6E, 0x4C, 0x39, 0x62, 0x46, 0x71, 0x79, 0x53, 0x32, 0x2B, 0x72, 0x2B, 0x65, 0x58, 0x79, 0x74, 0x0A, 0x36, 0x36, 0x2F, 0x33, 0x46, 0x73, 0x76, 0x62, + 0x7A, 0x53, 0x55, 0x72, 0x35, 0x52, 0x2F, 0x37, 0x6D, 0x70, 0x2F, 0x69, 0x55, 0x63, 0x77, 0x36, 0x55, 0x77, 0x78, 0x49, 0x35, 0x67, 0x36, 0x39, 0x79, 0x62, 0x52, 0x32, 0x42, 0x6C, 0x4C, 0x6D, 0x45, 0x52, 0x4F, 0x46, 0x63, 0x6D, 0x4D, 0x44, + 0x42, 0x4F, 0x41, 0x45, 0x4E, 0x69, 0x73, 0x67, 0x47, 0x51, 0x4C, 0x6F, 0x64, 0x4B, 0x63, 0x66, 0x74, 0x73, 0x6C, 0x57, 0x5A, 0x76, 0x42, 0x31, 0x4A, 0x64, 0x78, 0x6E, 0x0A, 0x77, 0x51, 0x35, 0x68, 0x59, 0x49, 0x69, 0x7A, 0x50, 0x74, 0x47, + 0x6F, 0x2F, 0x4B, 0x50, 0x61, 0x48, 0x62, 0x44, 0x52, 0x73, 0x53, 0x4E, 0x55, 0x33, 0x30, 0x52, 0x32, 0x62, 0x65, 0x31, 0x42, 0x32, 0x4D, 0x47, 0x79, 0x49, 0x72, 0x5A, 0x54, 0x48, 0x4E, 0x38, 0x31, 0x48, 0x64, 0x79, 0x68, 0x64, 0x79, 0x6F, + 0x78, 0x35, 0x43, 0x33, 0x31, 0x35, 0x65, 0x58, 0x62, 0x79, 0x4F, 0x44, 0x2F, 0x35, 0x59, 0x44, 0x58, 0x43, 0x32, 0x4F, 0x67, 0x2F, 0x7A, 0x4F, 0x68, 0x0A, 0x44, 0x37, 0x6F, 0x73, 0x46, 0x52, 0x58, 0x71, 0x6C, 0x37, 0x50, 0x53, 0x6F, 0x72, + 0x57, 0x2B, 0x38, 0x6F, 0x79, 0x57, 0x48, 0x68, 0x71, 0x50, 0x48, 0x57, 0x79, 0x6B, 0x59, 0x54, 0x65, 0x35, 0x68, 0x6E, 0x4D, 0x7A, 0x31, 0x35, 0x65, 0x57, 0x6E, 0x69, 0x4E, 0x39, 0x67, 0x71, 0x52, 0x4D, 0x67, 0x65, 0x4B, 0x68, 0x30, 0x62, + 0x70, 0x6E, 0x58, 0x35, 0x55, 0x48, 0x6F, 0x79, 0x63, 0x52, 0x37, 0x68, 0x59, 0x51, 0x65, 0x37, 0x78, 0x46, 0x53, 0x6B, 0x79, 0x79, 0x0A, 0x42, 0x4E, 0x4B, 0x72, 0x37, 0x39, 0x58, 0x39, 0x44, 0x46, 0x48, 0x4F, 0x55, 0x47, 0x6F, 0x49, 0x4D, + 0x66, 0x6D, 0x52, 0x32, 0x67, 0x79, 0x50, 0x5A, 0x46, 0x77, 0x44, 0x77, 0x7A, 0x71, 0x4C, 0x49, 0x44, 0x39, 0x75, 0x6A, 0x57, 0x63, 0x39, 0x4F, 0x74, 0x62, 0x2B, 0x66, 0x56, 0x75, 0x49, 0x79, 0x56, 0x37, 0x37, 0x7A, 0x47, 0x48, 0x63, 0x69, + 0x7A, 0x4E, 0x33, 0x30, 0x30, 0x51, 0x79, 0x4E, 0x51, 0x6C, 0x69, 0x42, 0x4A, 0x49, 0x57, 0x45, 0x4E, 0x69, 0x65, 0x0A, 0x4A, 0x30, 0x66, 0x37, 0x4F, 0x79, 0x48, 0x6A, 0x2B, 0x4F, 0x73, 0x64, 0x57, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, + 0x6F, 0x34, 0x47, 0x77, 0x4D, 0x49, 0x47, 0x74, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x43, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, + 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x51, 0x61, 0x68, 0x47, 0x4B, 0x38, 0x53, 0x45, 0x77, 0x7A, 0x4A, 0x51, 0x54, 0x55, 0x37, 0x74, 0x44, + 0x32, 0x41, 0x38, 0x51, 0x5A, 0x52, 0x74, 0x47, 0x55, 0x61, 0x7A, 0x42, 0x75, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x5A, 0x7A, 0x42, 0x6C, 0x67, 0x42, 0x51, 0x61, 0x68, 0x47, 0x4B, 0x38, 0x53, 0x45, 0x77, 0x7A, 0x4A, 0x51, 0x54, + 0x55, 0x37, 0x74, 0x44, 0x32, 0x41, 0x38, 0x51, 0x5A, 0x52, 0x74, 0x47, 0x55, 0x0A, 0x61, 0x36, 0x46, 0x4A, 0x70, 0x45, 0x63, 0x77, 0x52, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, + 0x30, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x46, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x47, 0x7A, + 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x0A, 0x45, 0x6C, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6F, 0x49, 0x43, 0x42, + 0x51, 0x6B, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x44, 0x34, 0x4B, 0x46, 0x6B, 0x32, 0x66, 0x42, 0x6C, 0x75, 0x6F, 0x72, + 0x6E, 0x46, 0x64, 0x4C, 0x77, 0x55, 0x76, 0x0A, 0x5A, 0x2B, 0x59, 0x54, 0x52, 0x59, 0x50, 0x45, 0x4E, 0x76, 0x62, 0x7A, 0x77, 0x43, 0x59, 0x4D, 0x44, 0x62, 0x56, 0x48, 0x5A, 0x46, 0x33, 0x34, 0x74, 0x48, 0x4C, 0x4A, 0x52, 0x71, 0x55, 0x44, + 0x47, 0x43, 0x64, 0x56, 0x69, 0x58, 0x68, 0x39, 0x64, 0x75, 0x71, 0x57, 0x4E, 0x49, 0x41, 0x58, 0x49, 0x4E, 0x7A, 0x6E, 0x67, 0x2F, 0x69, 0x4E, 0x2F, 0x41, 0x65, 0x34, 0x32, 0x6C, 0x39, 0x4E, 0x4C, 0x6D, 0x65, 0x79, 0x68, 0x50, 0x33, 0x5A, + 0x52, 0x50, 0x78, 0x33, 0x0A, 0x55, 0x49, 0x48, 0x6D, 0x66, 0x4C, 0x54, 0x4A, 0x44, 0x51, 0x74, 0x79, 0x55, 0x2F, 0x68, 0x32, 0x42, 0x77, 0x64, 0x42, 0x52, 0x35, 0x59, 0x4D, 0x2B, 0x2B, 0x43, 0x43, 0x4A, 0x70, 0x4E, 0x56, 0x6A, 0x50, 0x34, + 0x69, 0x48, 0x32, 0x42, 0x6C, 0x66, 0x46, 0x2F, 0x6E, 0x4A, 0x72, 0x50, 0x33, 0x4D, 0x70, 0x43, 0x59, 0x55, 0x4E, 0x51, 0x33, 0x63, 0x56, 0x58, 0x32, 0x6B, 0x69, 0x46, 0x34, 0x39, 0x35, 0x56, 0x35, 0x2B, 0x76, 0x67, 0x74, 0x4A, 0x6F, 0x64, + 0x6D, 0x0A, 0x56, 0x6A, 0x42, 0x33, 0x70, 0x6A, 0x64, 0x34, 0x4D, 0x31, 0x49, 0x51, 0x57, 0x4B, 0x34, 0x2F, 0x59, 0x59, 0x37, 0x79, 0x61, 0x72, 0x48, 0x76, 0x47, 0x48, 0x35, 0x4B, 0x57, 0x57, 0x50, 0x4B, 0x6A, 0x61, 0x4A, 0x57, 0x31, 0x61, + 0x63, 0x76, 0x76, 0x46, 0x59, 0x66, 0x7A, 0x7A, 0x6E, 0x42, 0x34, 0x76, 0x73, 0x4B, 0x71, 0x42, 0x55, 0x73, 0x66, 0x55, 0x31, 0x36, 0x59, 0x38, 0x5A, 0x73, 0x6C, 0x30, 0x51, 0x38, 0x30, 0x6D, 0x2F, 0x44, 0x53, 0x68, 0x63, 0x4B, 0x0A, 0x2B, + 0x4A, 0x44, 0x53, 0x56, 0x36, 0x49, 0x5A, 0x55, 0x61, 0x55, 0x74, 0x6C, 0x30, 0x48, 0x61, 0x42, 0x30, 0x2B, 0x70, 0x55, 0x4E, 0x71, 0x51, 0x6A, 0x5A, 0x52, 0x47, 0x34, 0x54, 0x37, 0x77, 0x6C, 0x50, 0x30, 0x51, 0x41, 0x44, 0x6A, 0x31, 0x4F, + 0x2B, 0x68, 0x41, 0x34, 0x62, 0x52, 0x75, 0x56, 0x68, 0x6F, 0x67, 0x7A, 0x47, 0x39, 0x59, 0x6A, 0x65, 0x30, 0x75, 0x52, 0x59, 0x2F, 0x57, 0x36, 0x5A, 0x4D, 0x2F, 0x35, 0x37, 0x45, 0x73, 0x33, 0x7A, 0x72, 0x57, 0x0A, 0x49, 0x6F, 0x7A, 0x63, + 0x68, 0x4C, 0x73, 0x69, 0x62, 0x39, 0x44, 0x34, 0x35, 0x4D, 0x59, 0x35, 0x36, 0x51, 0x53, 0x49, 0x50, 0x4D, 0x4F, 0x36, 0x36, 0x31, 0x56, 0x36, 0x62, 0x59, 0x43, 0x5A, 0x4A, 0x50, 0x56, 0x73, 0x41, 0x66, 0x76, 0x34, 0x6C, 0x37, 0x43, 0x55, + 0x57, 0x2B, 0x76, 0x39, 0x30, 0x6D, 0x2F, 0x78, 0x64, 0x32, 0x67, 0x4E, 0x4E, 0x57, 0x51, 0x6A, 0x72, 0x4C, 0x68, 0x56, 0x6F, 0x51, 0x50, 0x52, 0x54, 0x55, 0x49, 0x5A, 0x33, 0x50, 0x68, 0x31, 0x0A, 0x57, 0x56, 0x61, 0x6A, 0x2B, 0x61, 0x68, + 0x4A, 0x65, 0x66, 0x69, 0x76, 0x44, 0x72, 0x6B, 0x52, 0x6F, 0x48, 0x79, 0x33, 0x61, 0x75, 0x30, 0x30, 0x30, 0x4C, 0x59, 0x6D, 0x59, 0x6A, 0x67, 0x61, 0x68, 0x77, 0x7A, 0x34, 0x36, 0x50, 0x30, 0x75, 0x30, 0x35, 0x42, 0x2F, 0x42, 0x35, 0x45, + 0x71, 0x48, 0x64, 0x5A, 0x2B, 0x58, 0x49, 0x57, 0x44, 0x6D, 0x62, 0x41, 0x34, 0x43, 0x44, 0x2F, 0x70, 0x58, 0x76, 0x6B, 0x31, 0x42, 0x2B, 0x54, 0x4A, 0x59, 0x6D, 0x35, 0x58, 0x0A, 0x66, 0x36, 0x64, 0x51, 0x6C, 0x66, 0x65, 0x36, 0x79, 0x4A, + 0x76, 0x6D, 0x6A, 0x71, 0x49, 0x42, 0x78, 0x64, 0x5A, 0x6D, 0x76, 0x33, 0x6C, 0x68, 0x38, 0x7A, 0x77, 0x63, 0x34, 0x62, 0x6D, 0x43, 0x58, 0x46, 0x32, 0x67, 0x77, 0x2B, 0x6E, 0x59, 0x53, 0x4C, 0x30, 0x5A, 0x6F, 0x68, 0x45, 0x55, 0x47, 0x57, + 0x36, 0x79, 0x68, 0x68, 0x74, 0x6F, 0x50, 0x6B, 0x67, 0x33, 0x47, 0x6F, 0x69, 0x33, 0x58, 0x5A, 0x5A, 0x65, 0x6E, 0x4D, 0x66, 0x76, 0x4A, 0x32, 0x49, 0x49, 0x0A, 0x34, 0x70, 0x45, 0x5A, 0x58, 0x4E, 0x4C, 0x78, 0x49, 0x64, 0x32, 0x36, 0x46, + 0x30, 0x4B, 0x43, 0x6C, 0x33, 0x47, 0x42, 0x55, 0x7A, 0x47, 0x70, 0x6E, 0x2F, 0x5A, 0x39, 0x59, 0x72, 0x39, 0x79, 0x34, 0x61, 0x4F, 0x54, 0x48, 0x63, 0x79, 0x4B, 0x4A, 0x6C, 0x6F, 0x4A, 0x4F, 0x4E, 0x44, 0x4F, 0x31, 0x77, 0x32, 0x41, 0x46, + 0x72, 0x52, 0x34, 0x70, 0x54, 0x71, 0x48, 0x54, 0x49, 0x32, 0x4B, 0x70, 0x64, 0x56, 0x47, 0x6C, 0x2F, 0x49, 0x73, 0x45, 0x4C, 0x6D, 0x38, 0x0A, 0x56, 0x43, 0x4C, 0x41, 0x41, 0x56, 0x42, 0x70, 0x51, 0x35, 0x37, 0x30, 0x73, 0x75, 0x39, 0x74, + 0x2B, 0x4F, 0x7A, 0x61, 0x38, 0x65, 0x4F, 0x78, 0x37, 0x39, 0x2B, 0x52, 0x6A, 0x31, 0x51, 0x71, 0x43, 0x79, 0x58, 0x42, 0x4A, 0x68, 0x6E, 0x45, 0x55, 0x68, 0x41, 0x46, 0x5A, 0x64, 0x57, 0x43, 0x45, 0x4F, 0x72, 0x43, 0x4D, 0x63, 0x30, 0x75, + 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x51, 0x75, 0x6F, 0x56, 0x61, 0x64, 0x69, 0x73, 0x20, 0x52, 0x6F, 0x6F, + 0x74, 0x20, 0x43, 0x41, 0x20, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x47, 0x6E, 0x54, 0x43, 0x43, 0x42, 0x49, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x43, 0x42, 0x63, 0x59, 0x77, 0x44, 0x51, + 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x77, 0x52, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, 0x30, 0x78, 0x47, 0x54, + 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x0A, 0x45, 0x46, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x6C, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x77, 0x4E, + 0x6A, 0x45, 0x78, 0x4D, 0x6A, 0x51, 0x78, 0x0A, 0x4F, 0x54, 0x45, 0x78, 0x4D, 0x6A, 0x4E, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x54, 0x45, 0x78, 0x4D, 0x6A, 0x51, 0x78, 0x4F, 0x54, 0x41, 0x32, 0x4E, 0x44, 0x52, 0x61, 0x4D, 0x45, 0x55, 0x78, + 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4A, 0x4E, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, + 0x63, 0x79, 0x42, 0x4D, 0x0A, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x52, 0x73, 0x77, 0x47, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4A, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, + 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x4D, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, + 0x43, 0x0A, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x4D, 0x56, 0x30, 0x49, 0x57, 0x56, 0x4A, 0x7A, 0x6D, 0x6D, 0x4E, 0x50, 0x54, 0x54, 0x65, 0x37, 0x2B, 0x37, 0x63, 0x65, 0x66, 0x51, 0x7A, + 0x6C, 0x4B, 0x5A, 0x62, 0x50, 0x6F, 0x46, 0x6F, 0x67, 0x30, 0x32, 0x77, 0x31, 0x5A, 0x6B, 0x58, 0x54, 0x50, 0x6B, 0x72, 0x67, 0x45, 0x51, 0x4B, 0x30, 0x43, 0x53, 0x7A, 0x47, 0x72, 0x76, 0x49, 0x32, 0x52, 0x61, 0x4E, 0x67, 0x67, 0x0A, 0x44, + 0x68, 0x6F, 0x42, 0x34, 0x68, 0x70, 0x37, 0x54, 0x68, 0x64, 0x64, 0x34, 0x6F, 0x71, 0x33, 0x50, 0x35, 0x6B, 0x61, 0x7A, 0x65, 0x74, 0x68, 0x71, 0x38, 0x4A, 0x6C, 0x70, 0x68, 0x2B, 0x33, 0x74, 0x37, 0x32, 0x33, 0x6A, 0x2F, 0x7A, 0x39, 0x63, + 0x49, 0x38, 0x4C, 0x6F, 0x47, 0x65, 0x2B, 0x41, 0x61, 0x4A, 0x5A, 0x7A, 0x33, 0x48, 0x6D, 0x44, 0x79, 0x6C, 0x32, 0x2F, 0x37, 0x46, 0x57, 0x65, 0x55, 0x55, 0x72, 0x48, 0x35, 0x35, 0x36, 0x56, 0x4F, 0x69, 0x6A, 0x0A, 0x4B, 0x54, 0x56, 0x6F, + 0x70, 0x41, 0x46, 0x50, 0x44, 0x36, 0x51, 0x75, 0x4E, 0x2B, 0x38, 0x62, 0x76, 0x2B, 0x4F, 0x50, 0x45, 0x4B, 0x68, 0x79, 0x71, 0x31, 0x68, 0x58, 0x35, 0x31, 0x53, 0x47, 0x79, 0x4D, 0x6E, 0x7A, 0x57, 0x39, 0x6F, 0x73, 0x32, 0x6C, 0x32, 0x4F, + 0x62, 0x6A, 0x79, 0x6A, 0x50, 0x74, 0x72, 0x37, 0x67, 0x75, 0x58, 0x64, 0x38, 0x6C, 0x79, 0x79, 0x42, 0x54, 0x4E, 0x76, 0x69, 0x6A, 0x62, 0x4F, 0x30, 0x42, 0x4E, 0x4F, 0x2F, 0x37, 0x39, 0x4B, 0x0A, 0x44, 0x44, 0x52, 0x4D, 0x70, 0x73, 0x4D, + 0x68, 0x76, 0x56, 0x41, 0x45, 0x56, 0x65, 0x75, 0x78, 0x75, 0x35, 0x33, 0x37, 0x52, 0x52, 0x35, 0x6B, 0x46, 0x64, 0x35, 0x56, 0x41, 0x59, 0x77, 0x43, 0x64, 0x72, 0x58, 0x4C, 0x6F, 0x54, 0x39, 0x43, 0x61, 0x62, 0x77, 0x76, 0x76, 0x57, 0x68, + 0x44, 0x46, 0x6C, 0x61, 0x4A, 0x4B, 0x6A, 0x64, 0x68, 0x6B, 0x66, 0x32, 0x6D, 0x72, 0x6B, 0x37, 0x41, 0x79, 0x78, 0x52, 0x6C, 0x6C, 0x44, 0x64, 0x4C, 0x6B, 0x67, 0x62, 0x76, 0x0A, 0x42, 0x4E, 0x44, 0x49, 0x6E, 0x49, 0x6A, 0x62, 0x43, 0x33, + 0x75, 0x42, 0x72, 0x37, 0x45, 0x39, 0x4B, 0x73, 0x52, 0x6C, 0x4F, 0x6E, 0x69, 0x32, 0x37, 0x74, 0x79, 0x41, 0x73, 0x64, 0x4C, 0x54, 0x6D, 0x5A, 0x77, 0x36, 0x37, 0x6D, 0x74, 0x61, 0x61, 0x37, 0x4F, 0x4E, 0x74, 0x39, 0x58, 0x4F, 0x6E, 0x4D, + 0x4B, 0x2B, 0x70, 0x55, 0x73, 0x76, 0x46, 0x72, 0x47, 0x65, 0x61, 0x44, 0x73, 0x47, 0x62, 0x36, 0x35, 0x39, 0x6E, 0x2F, 0x6A, 0x65, 0x37, 0x4D, 0x77, 0x70, 0x0A, 0x70, 0x35, 0x69, 0x6A, 0x4A, 0x55, 0x4D, 0x76, 0x37, 0x2F, 0x46, 0x66, 0x4A, + 0x75, 0x47, 0x49, 0x54, 0x66, 0x68, 0x65, 0x62, 0x74, 0x66, 0x5A, 0x46, 0x47, 0x34, 0x5A, 0x4D, 0x32, 0x6D, 0x6E, 0x4F, 0x34, 0x53, 0x4A, 0x6B, 0x38, 0x52, 0x54, 0x56, 0x52, 0x4F, 0x68, 0x55, 0x58, 0x68, 0x41, 0x2B, 0x4C, 0x6A, 0x4A, 0x6F, + 0x75, 0x35, 0x37, 0x75, 0x6C, 0x4A, 0x43, 0x67, 0x35, 0x34, 0x55, 0x37, 0x51, 0x56, 0x53, 0x57, 0x6C, 0x6C, 0x57, 0x70, 0x35, 0x66, 0x38, 0x0A, 0x6E, 0x54, 0x38, 0x4B, 0x4B, 0x64, 0x6A, 0x63, 0x54, 0x35, 0x45, 0x4F, 0x45, 0x37, 0x7A, 0x65, + 0x6C, 0x61, 0x54, 0x66, 0x69, 0x35, 0x6D, 0x2B, 0x72, 0x4A, 0x73, 0x7A, 0x69, 0x4F, 0x2B, 0x31, 0x67, 0x61, 0x38, 0x62, 0x78, 0x69, 0x4A, 0x54, 0x79, 0x50, 0x62, 0x48, 0x37, 0x70, 0x63, 0x55, 0x73, 0x4D, 0x56, 0x38, 0x65, 0x46, 0x4C, 0x49, + 0x38, 0x4D, 0x35, 0x75, 0x64, 0x32, 0x43, 0x45, 0x70, 0x75, 0x6B, 0x71, 0x64, 0x69, 0x44, 0x74, 0x57, 0x41, 0x45, 0x58, 0x0A, 0x4D, 0x4A, 0x50, 0x70, 0x47, 0x6F, 0x76, 0x67, 0x63, 0x32, 0x50, 0x5A, 0x61, 0x70, 0x4B, 0x55, 0x53, 0x55, 0x36, + 0x30, 0x72, 0x55, 0x71, 0x46, 0x78, 0x4B, 0x4D, 0x69, 0x4D, 0x50, 0x77, 0x4A, 0x37, 0x57, 0x67, 0x69, 0x63, 0x36, 0x61, 0x49, 0x44, 0x46, 0x55, 0x68, 0x57, 0x4D, 0x58, 0x68, 0x4F, 0x70, 0x38, 0x71, 0x33, 0x63, 0x72, 0x68, 0x6B, 0x4F, 0x44, + 0x5A, 0x63, 0x36, 0x74, 0x73, 0x67, 0x4C, 0x6A, 0x6F, 0x43, 0x32, 0x53, 0x54, 0x6F, 0x4A, 0x79, 0x4D, 0x0A, 0x47, 0x66, 0x2B, 0x7A, 0x30, 0x67, 0x7A, 0x73, 0x6B, 0x53, 0x61, 0x48, 0x69, 0x72, 0x4F, 0x69, 0x34, 0x58, 0x43, 0x50, 0x4C, 0x41, + 0x72, 0x6C, 0x7A, 0x57, 0x31, 0x6F, 0x55, 0x65, 0x76, 0x61, 0x50, 0x77, 0x56, 0x2F, 0x69, 0x7A, 0x4C, 0x6D, 0x45, 0x31, 0x78, 0x72, 0x2F, 0x6C, 0x39, 0x41, 0x34, 0x69, 0x4C, 0x49, 0x74, 0x4C, 0x52, 0x6B, 0x54, 0x39, 0x61, 0x36, 0x66, 0x55, + 0x67, 0x2B, 0x71, 0x47, 0x6B, 0x4D, 0x31, 0x37, 0x75, 0x47, 0x63, 0x63, 0x6C, 0x7A, 0x0A, 0x75, 0x44, 0x38, 0x37, 0x6E, 0x53, 0x56, 0x4C, 0x32, 0x76, 0x39, 0x41, 0x36, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x34, 0x49, 0x42, 0x6C, + 0x54, 0x43, 0x43, 0x41, 0x5A, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x43, 0x42, 0x34, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x67, 0x42, + 0x49, 0x48, 0x5A, 0x4D, 0x49, 0x48, 0x57, 0x4D, 0x49, 0x48, 0x54, 0x0A, 0x42, 0x67, 0x6B, 0x72, 0x42, 0x67, 0x45, 0x45, 0x41, 0x62, 0x35, 0x59, 0x41, 0x41, 0x4D, 0x77, 0x67, 0x63, 0x55, 0x77, 0x67, 0x5A, 0x4D, 0x47, 0x43, 0x43, 0x73, 0x47, + 0x41, 0x51, 0x55, 0x46, 0x42, 0x77, 0x49, 0x43, 0x4D, 0x49, 0x47, 0x47, 0x47, 0x6F, 0x47, 0x44, 0x51, 0x57, 0x35, 0x35, 0x49, 0x48, 0x56, 0x7A, 0x5A, 0x53, 0x42, 0x76, 0x5A, 0x69, 0x42, 0x30, 0x61, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x4E, 0x6C, + 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x0A, 0x59, 0x58, 0x52, 0x6C, 0x49, 0x47, 0x4E, 0x76, 0x62, 0x6E, 0x4E, 0x30, 0x61, 0x58, 0x52, 0x31, 0x64, 0x47, 0x56, 0x7A, 0x49, 0x47, 0x46, 0x6A, 0x59, 0x32, 0x56, 0x77, 0x64, 0x47, 0x46, + 0x75, 0x59, 0x32, 0x55, 0x67, 0x62, 0x32, 0x59, 0x67, 0x64, 0x47, 0x68, 0x6C, 0x49, 0x46, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x79, 0x42, + 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x0A, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x5A, 0x53, 0x42, 0x51, 0x62, 0x32, 0x78, 0x70, 0x59, 0x33, 0x6B, 0x67, 0x4C, 0x79, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, + 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x46, 0x42, 0x79, 0x59, 0x57, 0x4E, 0x30, 0x61, 0x57, 0x4E, 0x6C, 0x49, 0x46, 0x4E, 0x30, 0x59, 0x58, 0x52, 0x6C, 0x62, 0x57, 0x56, 0x75, 0x64, 0x43, 0x34, 0x77, 0x4C, 0x51, 0x59, 0x49, 0x4B, 0x77, + 0x59, 0x42, 0x0A, 0x42, 0x51, 0x55, 0x48, 0x41, 0x67, 0x45, 0x57, 0x49, 0x57, 0x68, 0x30, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x33, 0x64, 0x33, 0x63, 0x75, 0x63, 0x58, 0x56, 0x76, 0x64, 0x6D, 0x46, 0x6B, 0x61, 0x58, 0x4E, 0x6E, 0x62, + 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4C, 0x32, 0x4E, 0x77, 0x63, 0x7A, 0x41, 0x4C, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, 0x0A, + 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x50, 0x4C, 0x41, 0x45, 0x2B, 0x43, 0x43, 0x51, 0x7A, 0x37, 0x37, 0x37, 0x69, 0x39, 0x6E, 0x4D, 0x70, 0x59, 0x31, 0x58, 0x4E, 0x75, 0x34, 0x79, 0x77, 0x4C, 0x51, 0x4D, 0x47, 0x34, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x52, 0x6E, 0x4D, 0x47, 0x57, 0x41, 0x46, 0x50, 0x4C, 0x41, 0x45, 0x2B, 0x43, 0x43, 0x51, 0x7A, 0x37, 0x37, 0x37, 0x69, 0x39, 0x6E, 0x4D, 0x70, 0x59, 0x31, 0x58, 0x4E, 0x75, 0x34, 0x0A, 0x79, 0x77, 0x4C, + 0x51, 0x6F, 0x55, 0x6D, 0x6B, 0x52, 0x7A, 0x42, 0x46, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x43, 0x54, 0x54, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, + 0x51, 0x55, 0x58, 0x56, 0x76, 0x56, 0x6D, 0x46, 0x6B, 0x61, 0x58, 0x4D, 0x67, 0x54, 0x47, 0x6C, 0x74, 0x61, 0x58, 0x52, 0x6C, 0x5A, 0x44, 0x45, 0x62, 0x4D, 0x42, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x41, 0x78, 0x4D, 0x53, 0x55, 0x58, + 0x56, 0x76, 0x56, 0x6D, 0x46, 0x6B, 0x61, 0x58, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x7A, 0x67, 0x67, 0x49, 0x46, 0x78, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x54, 0x36, 0x32, 0x67, 0x4C, 0x45, 0x7A, 0x36, 0x77, 0x50, 0x4A, 0x76, 0x39, 0x32, 0x5A, 0x56, 0x0A, 0x71, 0x79, 0x4D, 0x30, 0x37, 0x75, 0x63, 0x70, 0x32, + 0x73, 0x4E, 0x62, 0x74, 0x72, 0x43, 0x44, 0x32, 0x64, 0x44, 0x51, 0x34, 0x69, 0x48, 0x37, 0x38, 0x32, 0x43, 0x6E, 0x4F, 0x31, 0x31, 0x67, 0x55, 0x79, 0x65, 0x69, 0x6D, 0x2F, 0x59, 0x49, 0x49, 0x69, 0x72, 0x6E, 0x76, 0x36, 0x42, 0x79, 0x35, + 0x5A, 0x77, 0x6B, 0x61, 0x6A, 0x47, 0x78, 0x6B, 0x48, 0x6F, 0x6E, 0x32, 0x34, 0x51, 0x52, 0x69, 0x53, 0x65, 0x6D, 0x64, 0x31, 0x6F, 0x34, 0x31, 0x37, 0x2B, 0x73, 0x0A, 0x68, 0x76, 0x7A, 0x75, 0x58, 0x59, 0x4F, 0x38, 0x42, 0x73, 0x62, 0x52, + 0x64, 0x32, 0x73, 0x50, 0x62, 0x53, 0x51, 0x76, 0x53, 0x33, 0x70, 0x73, 0x70, 0x77, 0x65, 0x57, 0x79, 0x75, 0x4F, 0x45, 0x6E, 0x36, 0x32, 0x49, 0x69, 0x78, 0x32, 0x72, 0x46, 0x6F, 0x31, 0x62, 0x5A, 0x68, 0x66, 0x5A, 0x46, 0x76, 0x53, 0x4C, + 0x67, 0x4E, 0x4C, 0x64, 0x2B, 0x4C, 0x4A, 0x32, 0x77, 0x2F, 0x77, 0x34, 0x45, 0x36, 0x6F, 0x4D, 0x33, 0x6B, 0x4A, 0x70, 0x4B, 0x32, 0x37, 0x7A, 0x0A, 0x50, 0x4F, 0x75, 0x41, 0x4A, 0x39, 0x76, 0x31, 0x70, 0x6B, 0x51, 0x4E, 0x6E, 0x31, 0x70, + 0x56, 0x57, 0x51, 0x76, 0x56, 0x44, 0x56, 0x4A, 0x49, 0x78, 0x61, 0x36, 0x66, 0x38, 0x69, 0x2B, 0x41, 0x78, 0x65, 0x6F, 0x79, 0x55, 0x44, 0x55, 0x53, 0x6C, 0x79, 0x37, 0x42, 0x34, 0x66, 0x2F, 0x78, 0x49, 0x34, 0x68, 0x52, 0x4F, 0x4A, 0x2F, + 0x79, 0x5A, 0x6C, 0x5A, 0x32, 0x35, 0x77, 0x39, 0x52, 0x6C, 0x36, 0x56, 0x53, 0x44, 0x45, 0x31, 0x4A, 0x55, 0x5A, 0x55, 0x32, 0x0A, 0x50, 0x62, 0x2B, 0x69, 0x53, 0x77, 0x77, 0x51, 0x48, 0x59, 0x61, 0x5A, 0x54, 0x4B, 0x72, 0x7A, 0x63, 0x68, + 0x47, 0x54, 0x35, 0x4F, 0x72, 0x32, 0x6D, 0x39, 0x71, 0x6F, 0x58, 0x61, 0x64, 0x4E, 0x74, 0x35, 0x34, 0x43, 0x72, 0x6E, 0x4D, 0x41, 0x79, 0x4E, 0x6F, 0x6A, 0x41, 0x2B, 0x6A, 0x35, 0x36, 0x68, 0x6C, 0x30, 0x59, 0x67, 0x43, 0x55, 0x79, 0x79, + 0x49, 0x67, 0x76, 0x70, 0x53, 0x6E, 0x57, 0x62, 0x57, 0x43, 0x61, 0x72, 0x36, 0x5A, 0x65, 0x58, 0x71, 0x70, 0x0A, 0x38, 0x6B, 0x6F, 0x6B, 0x55, 0x76, 0x64, 0x30, 0x2F, 0x62, 0x70, 0x4F, 0x35, 0x71, 0x67, 0x64, 0x41, 0x6D, 0x36, 0x78, 0x44, + 0x59, 0x42, 0x45, 0x77, 0x61, 0x37, 0x54, 0x49, 0x7A, 0x64, 0x66, 0x75, 0x34, 0x56, 0x38, 0x4B, 0x35, 0x49, 0x75, 0x36, 0x48, 0x36, 0x6C, 0x69, 0x39, 0x32, 0x5A, 0x34, 0x62, 0x38, 0x6E, 0x62, 0x79, 0x31, 0x64, 0x71, 0x6E, 0x75, 0x48, 0x2F, + 0x67, 0x72, 0x64, 0x53, 0x2F, 0x79, 0x4F, 0x39, 0x53, 0x62, 0x6B, 0x62, 0x6E, 0x42, 0x43, 0x0A, 0x62, 0x6A, 0x50, 0x73, 0x4D, 0x5A, 0x35, 0x37, 0x6B, 0x38, 0x48, 0x6B, 0x79, 0x57, 0x6B, 0x61, 0x50, 0x63, 0x42, 0x72, 0x54, 0x69, 0x4A, 0x74, + 0x37, 0x71, 0x74, 0x59, 0x54, 0x63, 0x62, 0x51, 0x51, 0x63, 0x45, 0x72, 0x36, 0x6B, 0x38, 0x53, 0x68, 0x31, 0x37, 0x72, 0x52, 0x64, 0x68, 0x73, 0x39, 0x5A, 0x67, 0x43, 0x30, 0x36, 0x44, 0x59, 0x56, 0x59, 0x6F, 0x47, 0x6D, 0x52, 0x6D, 0x69, + 0x6F, 0x48, 0x66, 0x52, 0x4D, 0x4A, 0x36, 0x73, 0x7A, 0x48, 0x58, 0x75, 0x0A, 0x67, 0x2F, 0x57, 0x77, 0x59, 0x6A, 0x6E, 0x50, 0x62, 0x46, 0x66, 0x69, 0x54, 0x4E, 0x4B, 0x52, 0x43, 0x77, 0x35, 0x31, 0x4B, 0x42, 0x75, 0x61, 0x76, 0x2F, 0x30, + 0x61, 0x51, 0x2F, 0x48, 0x4B, 0x64, 0x2F, 0x73, 0x37, 0x6A, 0x32, 0x47, 0x34, 0x61, 0x53, 0x67, 0x57, 0x51, 0x67, 0x52, 0x65, 0x63, 0x43, 0x6F, 0x63, 0x49, 0x64, 0x69, 0x50, 0x34, 0x62, 0x30, 0x6A, 0x57, 0x79, 0x31, 0x30, 0x51, 0x4A, 0x4C, + 0x5A, 0x59, 0x78, 0x6B, 0x4E, 0x63, 0x39, 0x31, 0x70, 0x0A, 0x76, 0x47, 0x4A, 0x48, 0x76, 0x4F, 0x42, 0x30, 0x4B, 0x37, 0x4C, 0x72, 0x66, 0x62, 0x35, 0x42, 0x47, 0x37, 0x58, 0x41, 0x52, 0x73, 0x57, 0x68, 0x49, 0x73, 0x74, 0x66, 0x54, 0x73, + 0x45, 0x6F, 0x6B, 0x74, 0x34, 0x59, 0x75, 0x74, 0x55, 0x71, 0x4B, 0x4C, 0x73, 0x52, 0x69, 0x78, 0x65, 0x54, 0x6D, 0x4A, 0x6C, 0x67, 0x6C, 0x46, 0x77, 0x6A, 0x7A, 0x31, 0x6F, 0x6E, 0x6C, 0x31, 0x34, 0x4C, 0x42, 0x51, 0x61, 0x54, 0x4E, 0x78, + 0x34, 0x37, 0x61, 0x54, 0x62, 0x72, 0x0A, 0x71, 0x5A, 0x35, 0x68, 0x48, 0x59, 0x38, 0x79, 0x32, 0x6F, 0x34, 0x4D, 0x31, 0x6E, 0x51, 0x2B, 0x65, 0x77, 0x6B, 0x6B, 0x32, 0x67, 0x46, 0x33, 0x52, 0x38, 0x51, 0x37, 0x7A, 0x54, 0x53, 0x4D, 0x6D, + 0x66, 0x58, 0x4B, 0x34, 0x53, 0x56, 0x68, 0x4D, 0x37, 0x4A, 0x5A, 0x47, 0x2B, 0x4A, 0x75, 0x31, 0x7A, 0x64, 0x58, 0x74, 0x67, 0x32, 0x70, 0x45, 0x74, 0x6F, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x41, 0x73, 0x73, 0x75, 0x72, 0x65, 0x64, 0x20, 0x49, 0x44, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, + 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, + 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x74, 0x7A, 0x43, 0x43, 0x41, 0x70, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x44, + 0x4F, 0x66, 0x67, 0x35, 0x52, 0x66, 0x59, 0x52, 0x76, 0x36, 0x50, 0x35, 0x57, 0x44, 0x38, 0x47, 0x2F, 0x41, 0x77, 0x4F, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, + 0x44, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, + 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, + 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x51, 0x77, 0x0A, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x74, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x42, 0x63, 0x33, 0x4E, + 0x31, 0x63, 0x6D, 0x56, 0x6B, 0x49, 0x45, 0x6C, 0x45, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x59, 0x78, 0x4D, 0x54, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, + 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x45, 0x78, 0x0A, 0x4D, 0x54, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, + 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x0A, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, + 0x51, 0x51, 0x44, 0x45, 0x78, 0x74, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x42, 0x63, 0x33, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x6B, 0x49, 0x45, 0x6C, 0x45, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, + 0x30, 0x45, 0x77, 0x0A, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, + 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x74, 0x44, 0x68, 0x58, 0x4F, 0x35, 0x45, 0x4F, 0x41, 0x58, 0x4C, 0x47, 0x48, 0x38, 0x37, 0x64, 0x67, 0x2B, 0x58, 0x45, 0x53, 0x70, 0x61, 0x37, 0x63, 0x4A, 0x70, 0x53, 0x49, 0x71, 0x76, 0x54, 0x4F, + 0x0A, 0x39, 0x53, 0x41, 0x35, 0x4B, 0x46, 0x68, 0x67, 0x44, 0x50, 0x69, 0x41, 0x32, 0x71, 0x6B, 0x56, 0x6C, 0x54, 0x4A, 0x68, 0x50, 0x4C, 0x57, 0x78, 0x4B, 0x49, 0x53, 0x4B, 0x69, 0x74, 0x79, 0x66, 0x43, 0x67, 0x79, 0x44, 0x46, 0x33, 0x71, + 0x50, 0x6B, 0x4B, 0x79, 0x4B, 0x35, 0x33, 0x6C, 0x54, 0x58, 0x44, 0x47, 0x45, 0x4B, 0x76, 0x59, 0x50, 0x6D, 0x44, 0x49, 0x32, 0x64, 0x73, 0x7A, 0x65, 0x33, 0x54, 0x79, 0x6F, 0x6F, 0x75, 0x39, 0x71, 0x2B, 0x79, 0x48, 0x79, 0x0A, 0x55, 0x6D, + 0x48, 0x66, 0x6E, 0x79, 0x44, 0x58, 0x48, 0x2B, 0x4B, 0x78, 0x32, 0x66, 0x34, 0x59, 0x5A, 0x4E, 0x49, 0x53, 0x57, 0x31, 0x2F, 0x35, 0x57, 0x42, 0x67, 0x31, 0x76, 0x45, 0x66, 0x4E, 0x6F, 0x54, 0x62, 0x35, 0x61, 0x33, 0x2F, 0x55, 0x73, 0x44, + 0x67, 0x2B, 0x77, 0x52, 0x76, 0x44, 0x6A, 0x44, 0x50, 0x5A, 0x32, 0x43, 0x38, 0x59, 0x2F, 0x69, 0x67, 0x50, 0x73, 0x36, 0x65, 0x44, 0x31, 0x73, 0x4E, 0x75, 0x52, 0x4D, 0x42, 0x68, 0x4E, 0x5A, 0x59, 0x57, 0x0A, 0x2F, 0x6C, 0x6D, 0x63, 0x69, + 0x33, 0x5A, 0x74, 0x31, 0x2F, 0x47, 0x69, 0x53, 0x77, 0x30, 0x72, 0x2F, 0x77, 0x74, 0x79, 0x32, 0x70, 0x35, 0x67, 0x30, 0x49, 0x36, 0x51, 0x4E, 0x63, 0x5A, 0x34, 0x56, 0x59, 0x63, 0x67, 0x6F, 0x63, 0x2F, 0x6C, 0x62, 0x51, 0x72, 0x49, 0x53, + 0x58, 0x77, 0x78, 0x6D, 0x44, 0x4E, 0x73, 0x49, 0x75, 0x6D, 0x48, 0x30, 0x44, 0x4A, 0x61, 0x6F, 0x72, 0x6F, 0x54, 0x67, 0x68, 0x48, 0x74, 0x4F, 0x52, 0x65, 0x64, 0x6D, 0x54, 0x70, 0x79, 0x0A, 0x6F, 0x65, 0x62, 0x36, 0x70, 0x4E, 0x6E, 0x56, + 0x46, 0x7A, 0x46, 0x31, 0x72, 0x6F, 0x56, 0x39, 0x49, 0x71, 0x34, 0x2F, 0x41, 0x55, 0x61, 0x47, 0x39, 0x69, 0x68, 0x35, 0x79, 0x4C, 0x48, 0x61, 0x35, 0x46, 0x63, 0x58, 0x78, 0x48, 0x34, 0x63, 0x44, 0x72, 0x43, 0x30, 0x6B, 0x71, 0x5A, 0x57, + 0x73, 0x37, 0x32, 0x79, 0x6C, 0x2B, 0x32, 0x71, 0x70, 0x2F, 0x43, 0x33, 0x78, 0x61, 0x67, 0x2F, 0x6C, 0x52, 0x62, 0x51, 0x2F, 0x36, 0x47, 0x57, 0x36, 0x77, 0x68, 0x66, 0x0A, 0x47, 0x48, 0x64, 0x50, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, + 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, + 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x46, 0x0A, 0x36, 0x36, 0x4B, 0x76, 0x39, 0x4A, 0x4C, 0x4C, 0x67, 0x6A, 0x45, 0x74, 0x55, 0x59, + 0x75, 0x6E, 0x70, 0x79, 0x47, 0x64, 0x38, 0x32, 0x33, 0x49, 0x44, 0x7A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x52, 0x46, 0x36, 0x36, 0x4B, 0x76, 0x39, 0x4A, 0x4C, 0x4C, 0x67, 0x6A, + 0x45, 0x74, 0x55, 0x59, 0x75, 0x6E, 0x70, 0x79, 0x47, 0x64, 0x38, 0x32, 0x33, 0x49, 0x44, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x0A, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, + 0x51, 0x45, 0x41, 0x6F, 0x67, 0x36, 0x38, 0x33, 0x2B, 0x4C, 0x74, 0x38, 0x4F, 0x4E, 0x79, 0x63, 0x33, 0x70, 0x6B, 0x6C, 0x4C, 0x2F, 0x33, 0x63, 0x6D, 0x62, 0x59, 0x4D, 0x75, 0x52, 0x43, 0x64, 0x57, 0x4B, 0x75, 0x68, 0x2B, 0x76, 0x79, 0x31, + 0x64, 0x6E, 0x65, 0x56, 0x72, 0x4F, 0x66, 0x7A, 0x4D, 0x34, 0x55, 0x4B, 0x4C, 0x6B, 0x4E, 0x6C, 0x32, 0x42, 0x63, 0x0A, 0x45, 0x6B, 0x78, 0x59, 0x35, 0x4E, 0x4D, 0x39, 0x67, 0x30, 0x6C, 0x46, 0x57, 0x4A, 0x63, 0x31, 0x61, 0x52, 0x71, 0x6F, + 0x52, 0x2B, 0x70, 0x57, 0x78, 0x6E, 0x6D, 0x72, 0x45, 0x74, 0x68, 0x6E, 0x67, 0x59, 0x54, 0x66, 0x66, 0x77, 0x6B, 0x38, 0x6C, 0x4F, 0x61, 0x34, 0x4A, 0x69, 0x77, 0x67, 0x76, 0x54, 0x32, 0x7A, 0x4B, 0x49, 0x6E, 0x33, 0x58, 0x2F, 0x38, 0x69, + 0x34, 0x70, 0x65, 0x45, 0x48, 0x2B, 0x6C, 0x6C, 0x37, 0x34, 0x66, 0x67, 0x33, 0x38, 0x46, 0x6E, 0x0A, 0x53, 0x62, 0x4E, 0x64, 0x36, 0x37, 0x49, 0x4A, 0x4B, 0x75, 0x73, 0x6D, 0x37, 0x58, 0x69, 0x2B, 0x66, 0x54, 0x38, 0x72, 0x38, 0x37, 0x63, + 0x6D, 0x4E, 0x57, 0x31, 0x66, 0x69, 0x51, 0x47, 0x32, 0x53, 0x56, 0x75, 0x66, 0x41, 0x51, 0x57, 0x62, 0x71, 0x7A, 0x30, 0x6C, 0x77, 0x63, 0x79, 0x32, 0x66, 0x38, 0x4C, 0x78, 0x62, 0x34, 0x62, 0x47, 0x2B, 0x6D, 0x52, 0x6F, 0x36, 0x34, 0x45, + 0x74, 0x6C, 0x4F, 0x74, 0x43, 0x74, 0x2F, 0x71, 0x4D, 0x48, 0x74, 0x31, 0x69, 0x0A, 0x38, 0x62, 0x35, 0x51, 0x5A, 0x37, 0x64, 0x73, 0x76, 0x66, 0x50, 0x78, 0x48, 0x32, 0x73, 0x4D, 0x4E, 0x67, 0x63, 0x57, 0x66, 0x7A, 0x64, 0x38, 0x71, 0x56, + 0x74, 0x74, 0x65, 0x76, 0x45, 0x53, 0x52, 0x6D, 0x43, 0x44, 0x31, 0x79, 0x63, 0x45, 0x76, 0x6B, 0x76, 0x4F, 0x6C, 0x37, 0x37, 0x44, 0x5A, 0x79, 0x70, 0x6F, 0x45, 0x64, 0x2B, 0x41, 0x35, 0x77, 0x77, 0x7A, 0x5A, 0x72, 0x38, 0x54, 0x44, 0x52, + 0x52, 0x75, 0x38, 0x33, 0x38, 0x66, 0x59, 0x78, 0x41, 0x65, 0x0A, 0x2B, 0x6F, 0x30, 0x62, 0x4A, 0x57, 0x31, 0x73, 0x6A, 0x36, 0x57, 0x33, 0x59, 0x51, 0x47, 0x78, 0x30, 0x71, 0x4D, 0x6D, 0x6F, 0x52, 0x42, 0x78, 0x6E, 0x61, 0x33, 0x69, 0x77, + 0x2F, 0x6E, 0x44, 0x6D, 0x56, 0x47, 0x33, 0x4B, 0x77, 0x63, 0x49, 0x7A, 0x69, 0x37, 0x6D, 0x55, 0x4C, 0x4B, 0x6E, 0x2B, 0x67, 0x70, 0x46, 0x4C, 0x36, 0x4C, 0x77, 0x38, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, + 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x72, 0x7A, 0x43, 0x43, 0x41, 0x70, 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x44, 0x76, 0x67, 0x56, + 0x70, 0x42, 0x43, 0x52, 0x72, 0x47, 0x68, 0x64, 0x57, 0x72, 0x4A, 0x57, 0x5A, 0x48, 0x48, 0x53, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x44, 0x42, 0x68, 0x4D, + 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, + 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, + 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x41, 0x77, 0x0A, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, + 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x77, 0x4E, 0x6A, 0x45, 0x78, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x54, 0x45, + 0x78, 0x4D, 0x54, 0x41, 0x77, 0x0A, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x47, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, + 0x64, 0x33, 0x0A, 0x64, 0x79, 0x35, 0x6B, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, 0x56, 0x79, 0x64, 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, 0x78, 0x49, 0x44, 0x41, 0x65, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x30, 0x52, 0x70, 0x5A, + 0x32, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x0A, + 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x34, 0x6A, 0x76, 0x68, 0x45, 0x58, 0x4C, 0x65, + 0x71, 0x4B, 0x54, 0x54, 0x6F, 0x31, 0x65, 0x71, 0x55, 0x4B, 0x4B, 0x50, 0x43, 0x33, 0x65, 0x51, 0x79, 0x61, 0x4B, 0x6C, 0x37, 0x68, 0x4C, 0x4F, 0x6C, 0x6C, 0x73, 0x42, 0x43, 0x53, 0x44, 0x4D, 0x41, 0x5A, 0x4F, 0x6E, 0x0A, 0x54, 0x6A, 0x43, + 0x33, 0x55, 0x2F, 0x64, 0x44, 0x78, 0x47, 0x6B, 0x41, 0x56, 0x35, 0x33, 0x69, 0x6A, 0x53, 0x4C, 0x64, 0x68, 0x77, 0x5A, 0x41, 0x41, 0x49, 0x45, 0x4A, 0x7A, 0x73, 0x34, 0x62, 0x67, 0x37, 0x2F, 0x66, 0x7A, 0x54, 0x74, 0x78, 0x52, 0x75, 0x4C, + 0x57, 0x5A, 0x73, 0x63, 0x46, 0x73, 0x33, 0x59, 0x6E, 0x46, 0x6F, 0x39, 0x37, 0x6E, 0x68, 0x36, 0x56, 0x66, 0x65, 0x36, 0x33, 0x53, 0x4B, 0x4D, 0x49, 0x32, 0x74, 0x61, 0x76, 0x65, 0x67, 0x77, 0x35, 0x0A, 0x42, 0x6D, 0x56, 0x2F, 0x53, 0x6C, + 0x30, 0x66, 0x76, 0x42, 0x66, 0x34, 0x71, 0x37, 0x37, 0x75, 0x4B, 0x4E, 0x64, 0x30, 0x66, 0x33, 0x70, 0x34, 0x6D, 0x56, 0x6D, 0x46, 0x61, 0x47, 0x35, 0x63, 0x49, 0x7A, 0x4A, 0x4C, 0x76, 0x30, 0x37, 0x41, 0x36, 0x46, 0x70, 0x74, 0x34, 0x33, + 0x43, 0x2F, 0x64, 0x78, 0x43, 0x2F, 0x2F, 0x41, 0x48, 0x32, 0x68, 0x64, 0x6D, 0x6F, 0x52, 0x42, 0x42, 0x59, 0x4D, 0x71, 0x6C, 0x31, 0x47, 0x4E, 0x58, 0x52, 0x6F, 0x72, 0x35, 0x48, 0x0A, 0x34, 0x69, 0x64, 0x71, 0x39, 0x4A, 0x6F, 0x7A, 0x2B, + 0x45, 0x6B, 0x49, 0x59, 0x49, 0x76, 0x55, 0x58, 0x37, 0x51, 0x36, 0x68, 0x4C, 0x2B, 0x68, 0x71, 0x6B, 0x70, 0x4D, 0x66, 0x54, 0x37, 0x50, 0x54, 0x31, 0x39, 0x73, 0x64, 0x6C, 0x36, 0x67, 0x53, 0x7A, 0x65, 0x52, 0x6E, 0x74, 0x77, 0x69, 0x35, + 0x6D, 0x33, 0x4F, 0x46, 0x42, 0x71, 0x4F, 0x61, 0x73, 0x76, 0x2B, 0x7A, 0x62, 0x4D, 0x55, 0x5A, 0x42, 0x66, 0x48, 0x57, 0x79, 0x6D, 0x65, 0x4D, 0x72, 0x2F, 0x79, 0x0A, 0x37, 0x76, 0x72, 0x54, 0x43, 0x30, 0x4C, 0x55, 0x71, 0x37, 0x64, 0x42, + 0x4D, 0x74, 0x6F, 0x4D, 0x31, 0x4F, 0x2F, 0x34, 0x67, 0x64, 0x57, 0x37, 0x6A, 0x56, 0x67, 0x2F, 0x74, 0x52, 0x76, 0x6F, 0x53, 0x53, 0x69, 0x69, 0x63, 0x4E, 0x6F, 0x78, 0x42, 0x4E, 0x33, 0x33, 0x73, 0x68, 0x62, 0x79, 0x54, 0x41, 0x70, 0x4F, + 0x42, 0x36, 0x6A, 0x74, 0x53, 0x6A, 0x31, 0x65, 0x74, 0x58, 0x2B, 0x6A, 0x6B, 0x4D, 0x4F, 0x76, 0x4A, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x0A, 0x6F, 0x32, 0x4D, 0x77, 0x59, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, + 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, + 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x41, 0x39, 0x35, 0x51, 0x4E, 0x56, 0x62, 0x52, 0x54, 0x4C, 0x74, 0x6D, 0x0A, 0x38, 0x4B, 0x50, 0x69, 0x47, 0x78, 0x76, 0x44, 0x6C, 0x37, 0x49, 0x39, 0x30, 0x56, 0x55, 0x77, 0x48, 0x77, + 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, 0x55, 0x41, 0x39, 0x35, 0x51, 0x4E, 0x56, 0x62, 0x52, 0x54, 0x4C, 0x74, 0x6D, 0x38, 0x4B, 0x50, 0x69, 0x47, 0x78, 0x76, 0x44, 0x6C, 0x37, 0x49, 0x39, 0x30, 0x56, + 0x55, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x0A, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4D, 0x75, 0x63, 0x4E, 0x36, 0x70, 0x49, 0x45, 0x78, 0x49, 0x4B, 0x2B, + 0x74, 0x31, 0x45, 0x6E, 0x45, 0x39, 0x53, 0x73, 0x50, 0x54, 0x66, 0x72, 0x67, 0x54, 0x31, 0x65, 0x58, 0x6B, 0x49, 0x6F, 0x79, 0x51, 0x59, 0x2F, 0x45, 0x73, 0x72, 0x68, 0x4D, 0x41, 0x74, 0x75, 0x64, 0x58, 0x48, 0x2F, 0x76, 0x54, 0x42, 0x48, + 0x31, 0x6A, 0x4C, 0x75, 0x47, 0x32, 0x63, 0x65, 0x6E, 0x54, 0x6E, 0x6D, 0x43, 0x6D, 0x72, 0x0A, 0x45, 0x62, 0x58, 0x6A, 0x63, 0x4B, 0x43, 0x68, 0x7A, 0x55, 0x79, 0x49, 0x6D, 0x5A, 0x4F, 0x4D, 0x6B, 0x58, 0x44, 0x69, 0x71, 0x77, 0x38, 0x63, + 0x76, 0x70, 0x4F, 0x70, 0x2F, 0x32, 0x50, 0x56, 0x35, 0x41, 0x64, 0x67, 0x30, 0x36, 0x4F, 0x2F, 0x6E, 0x56, 0x73, 0x4A, 0x38, 0x64, 0x57, 0x4F, 0x34, 0x31, 0x50, 0x30, 0x6A, 0x6D, 0x50, 0x36, 0x50, 0x36, 0x66, 0x62, 0x74, 0x47, 0x62, 0x66, + 0x59, 0x6D, 0x62, 0x57, 0x30, 0x57, 0x35, 0x42, 0x6A, 0x66, 0x49, 0x74, 0x0A, 0x74, 0x65, 0x70, 0x33, 0x53, 0x70, 0x2B, 0x64, 0x57, 0x4F, 0x49, 0x72, 0x57, 0x63, 0x42, 0x41, 0x49, 0x2B, 0x30, 0x74, 0x4B, 0x49, 0x4A, 0x46, 0x50, 0x6E, 0x6C, + 0x55, 0x6B, 0x69, 0x61, 0x59, 0x34, 0x49, 0x42, 0x49, 0x71, 0x44, 0x66, 0x76, 0x38, 0x4E, 0x5A, 0x35, 0x59, 0x42, 0x62, 0x65, 0x72, 0x4F, 0x67, 0x4F, 0x7A, 0x57, 0x36, 0x73, 0x52, 0x42, 0x63, 0x34, 0x4C, 0x30, 0x6E, 0x61, 0x34, 0x55, 0x55, + 0x2B, 0x4B, 0x72, 0x6B, 0x32, 0x55, 0x38, 0x38, 0x36, 0x0A, 0x55, 0x41, 0x62, 0x33, 0x4C, 0x75, 0x6A, 0x45, 0x56, 0x30, 0x6C, 0x73, 0x59, 0x53, 0x45, 0x59, 0x31, 0x51, 0x53, 0x74, 0x65, 0x44, 0x77, 0x73, 0x4F, 0x6F, 0x42, 0x72, 0x70, 0x2B, + 0x75, 0x76, 0x46, 0x52, 0x54, 0x70, 0x32, 0x49, 0x6E, 0x42, 0x75, 0x54, 0x68, 0x73, 0x34, 0x70, 0x46, 0x73, 0x69, 0x76, 0x39, 0x6B, 0x75, 0x58, 0x63, 0x6C, 0x56, 0x7A, 0x44, 0x41, 0x47, 0x79, 0x53, 0x6A, 0x34, 0x64, 0x7A, 0x70, 0x33, 0x30, + 0x64, 0x38, 0x74, 0x62, 0x51, 0x6B, 0x0A, 0x43, 0x41, 0x55, 0x77, 0x37, 0x43, 0x32, 0x39, 0x43, 0x37, 0x39, 0x46, 0x76, 0x31, 0x43, 0x35, 0x71, 0x66, 0x50, 0x72, 0x6D, 0x41, 0x45, 0x53, 0x72, 0x63, 0x69, 0x49, 0x78, 0x70, 0x67, 0x30, 0x58, + 0x34, 0x30, 0x4B, 0x50, 0x4D, 0x62, 0x70, 0x31, 0x5A, 0x57, 0x56, 0x62, 0x64, 0x34, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x48, 0x69, 0x67, 0x68, 0x20, 0x41, 0x73, 0x73, 0x75, 0x72, 0x61, 0x6E, 0x63, 0x65, 0x20, 0x45, 0x56, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x78, 0x54, 0x43, 0x43, 0x41, 0x71, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x51, 0x41, 0x71, 0x78, 0x63, 0x4A, 0x6D, 0x6F, 0x4C, 0x51, 0x4A, 0x75, 0x50, 0x43, 0x33, 0x6E, 0x79, 0x72, 0x6B, 0x59, 0x6C, 0x64, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, + 0x46, 0x41, 0x44, 0x42, 0x73, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, + 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, + 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x73, 0x77, 0x0A, 0x4B, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x4A, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x49, 0x61, + 0x57, 0x64, 0x6F, 0x49, 0x45, 0x46, 0x7A, 0x63, 0x33, 0x56, 0x79, 0x59, 0x57, 0x35, 0x6A, 0x5A, 0x53, 0x42, 0x46, 0x56, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x32, 0x4D, + 0x54, 0x45, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x0A, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x78, 0x4D, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x62, 0x44, 0x45, 0x4C, + 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x45, 0x52, 0x70, 0x5A, 0x32, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, + 0x49, 0x45, 0x6C, 0x75, 0x59, 0x7A, 0x45, 0x5A, 0x0A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x51, 0x64, 0x33, 0x64, 0x33, 0x4C, 0x6D, 0x52, 0x70, 0x5A, 0x32, 0x6C, 0x6A, 0x5A, 0x58, 0x4A, 0x30, 0x4C, 0x6D, 0x4E, + 0x76, 0x62, 0x54, 0x45, 0x72, 0x4D, 0x43, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x69, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x47, 0x6C, 0x6E, 0x61, 0x43, 0x42, 0x42, 0x63, 0x33, 0x4E, + 0x31, 0x63, 0x6D, 0x46, 0x75, 0x0A, 0x59, 0x32, 0x55, 0x67, 0x52, 0x56, 0x59, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, + 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4D, 0x62, 0x4D, 0x35, 0x58, 0x50, 0x6D, 0x2B, 0x39, 0x53, 0x37, 0x35, 0x53, + 0x30, 0x74, 0x0A, 0x4D, 0x71, 0x62, 0x66, 0x35, 0x59, 0x45, 0x2F, 0x79, 0x63, 0x30, 0x6C, 0x53, 0x62, 0x5A, 0x78, 0x4B, 0x73, 0x50, 0x56, 0x6C, 0x44, 0x52, 0x6E, 0x6F, 0x67, 0x6F, 0x63, 0x73, 0x46, 0x39, 0x70, 0x70, 0x6B, 0x43, 0x78, 0x78, + 0x4C, 0x65, 0x79, 0x6A, 0x39, 0x43, 0x59, 0x70, 0x4B, 0x6C, 0x42, 0x57, 0x54, 0x72, 0x54, 0x33, 0x4A, 0x54, 0x57, 0x50, 0x4E, 0x74, 0x30, 0x4F, 0x4B, 0x52, 0x4B, 0x7A, 0x45, 0x30, 0x6C, 0x67, 0x76, 0x64, 0x4B, 0x70, 0x56, 0x4D, 0x53, 0x0A, + 0x4F, 0x4F, 0x37, 0x7A, 0x53, 0x57, 0x31, 0x78, 0x6B, 0x58, 0x35, 0x6A, 0x74, 0x71, 0x75, 0x6D, 0x58, 0x38, 0x4F, 0x6B, 0x68, 0x50, 0x68, 0x50, 0x59, 0x6C, 0x47, 0x2B, 0x2B, 0x4D, 0x58, 0x73, 0x32, 0x7A, 0x69, 0x53, 0x34, 0x77, 0x62, 0x6C, + 0x43, 0x4A, 0x45, 0x4D, 0x78, 0x43, 0x68, 0x42, 0x56, 0x66, 0x76, 0x4C, 0x57, 0x6F, 0x6B, 0x56, 0x66, 0x6E, 0x48, 0x6F, 0x4E, 0x62, 0x39, 0x4E, 0x63, 0x67, 0x6B, 0x39, 0x76, 0x6A, 0x6F, 0x34, 0x55, 0x46, 0x74, 0x33, 0x0A, 0x4D, 0x52, 0x75, + 0x4E, 0x73, 0x38, 0x63, 0x6B, 0x52, 0x5A, 0x71, 0x6E, 0x72, 0x47, 0x30, 0x41, 0x46, 0x46, 0x6F, 0x45, 0x74, 0x37, 0x6F, 0x54, 0x36, 0x31, 0x45, 0x4B, 0x6D, 0x45, 0x46, 0x42, 0x49, 0x6B, 0x35, 0x6C, 0x59, 0x59, 0x65, 0x42, 0x51, 0x56, 0x43, + 0x6D, 0x65, 0x56, 0x79, 0x4A, 0x33, 0x68, 0x6C, 0x4B, 0x56, 0x39, 0x55, 0x75, 0x35, 0x6C, 0x30, 0x63, 0x55, 0x79, 0x78, 0x2B, 0x6D, 0x4D, 0x30, 0x61, 0x42, 0x68, 0x61, 0x6B, 0x61, 0x48, 0x50, 0x51, 0x0A, 0x4E, 0x41, 0x51, 0x54, 0x58, 0x4B, + 0x46, 0x78, 0x30, 0x31, 0x70, 0x38, 0x56, 0x64, 0x74, 0x65, 0x5A, 0x4F, 0x45, 0x33, 0x68, 0x7A, 0x42, 0x57, 0x42, 0x4F, 0x55, 0x52, 0x74, 0x43, 0x6D, 0x41, 0x45, 0x76, 0x46, 0x35, 0x4F, 0x59, 0x69, 0x69, 0x41, 0x68, 0x46, 0x38, 0x4A, 0x32, + 0x61, 0x33, 0x69, 0x4C, 0x64, 0x34, 0x38, 0x73, 0x6F, 0x4B, 0x71, 0x44, 0x69, 0x72, 0x43, 0x6D, 0x54, 0x43, 0x76, 0x32, 0x5A, 0x64, 0x6C, 0x59, 0x54, 0x42, 0x6F, 0x53, 0x55, 0x65, 0x0A, 0x68, 0x31, 0x30, 0x61, 0x55, 0x41, 0x73, 0x67, 0x45, + 0x73, 0x78, 0x42, 0x75, 0x32, 0x34, 0x4C, 0x55, 0x54, 0x69, 0x34, 0x53, 0x38, 0x73, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x6A, 0x4D, 0x47, 0x45, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, + 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x0A, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, + 0x42, 0x42, 0x59, 0x45, 0x46, 0x4C, 0x45, 0x2B, 0x77, 0x32, 0x6B, 0x44, 0x2B, 0x4C, 0x39, 0x48, 0x41, 0x64, 0x53, 0x59, 0x4A, 0x68, 0x6F, 0x49, 0x41, 0x75, 0x39, 0x6A, 0x5A, 0x43, 0x76, 0x44, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, + 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x4C, 0x45, 0x2B, 0x77, 0x32, 0x6B, 0x44, 0x2B, 0x4C, 0x39, 0x48, 0x41, 0x64, 0x53, 0x59, 0x0A, 0x4A, 0x68, 0x6F, 0x49, 0x41, 0x75, 0x39, 0x6A, 0x5A, 0x43, 0x76, 0x44, 0x4D, 0x41, 0x30, + 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x41, 0x63, 0x47, 0x67, 0x61, 0x58, 0x33, 0x4E, 0x65, 0x63, 0x6E, 0x7A, 0x79, 0x49, 0x5A, 0x67, 0x59, + 0x49, 0x56, 0x79, 0x48, 0x62, 0x49, 0x55, 0x66, 0x34, 0x4B, 0x6D, 0x65, 0x71, 0x76, 0x78, 0x67, 0x79, 0x64, 0x6B, 0x41, 0x51, 0x0A, 0x56, 0x38, 0x47, 0x4B, 0x38, 0x33, 0x72, 0x5A, 0x45, 0x57, 0x57, 0x4F, 0x4E, 0x66, 0x71, 0x65, 0x2F, 0x45, + 0x57, 0x31, 0x6E, 0x74, 0x6C, 0x4D, 0x4D, 0x55, 0x75, 0x34, 0x6B, 0x65, 0x68, 0x44, 0x4C, 0x49, 0x36, 0x7A, 0x65, 0x4D, 0x37, 0x62, 0x34, 0x31, 0x4E, 0x35, 0x63, 0x64, 0x62, 0x6C, 0x49, 0x5A, 0x51, 0x42, 0x32, 0x6C, 0x57, 0x48, 0x6D, 0x69, + 0x52, 0x6B, 0x39, 0x6F, 0x70, 0x6D, 0x7A, 0x4E, 0x36, 0x63, 0x4E, 0x38, 0x32, 0x6F, 0x4E, 0x4C, 0x46, 0x70, 0x0A, 0x6D, 0x79, 0x50, 0x49, 0x6E, 0x6E, 0x67, 0x69, 0x4B, 0x33, 0x42, 0x44, 0x34, 0x31, 0x56, 0x48, 0x4D, 0x57, 0x45, 0x5A, 0x37, + 0x31, 0x6A, 0x46, 0x68, 0x53, 0x39, 0x4F, 0x4D, 0x50, 0x61, 0x67, 0x4D, 0x52, 0x59, 0x6A, 0x79, 0x4F, 0x66, 0x69, 0x5A, 0x52, 0x59, 0x7A, 0x79, 0x37, 0x38, 0x61, 0x47, 0x36, 0x41, 0x39, 0x2B, 0x4D, 0x70, 0x65, 0x69, 0x7A, 0x47, 0x4C, 0x59, + 0x41, 0x69, 0x4A, 0x4C, 0x51, 0x77, 0x47, 0x58, 0x46, 0x4B, 0x33, 0x78, 0x50, 0x6B, 0x4B, 0x0A, 0x6D, 0x4E, 0x45, 0x56, 0x58, 0x35, 0x38, 0x53, 0x76, 0x6E, 0x77, 0x32, 0x59, 0x7A, 0x69, 0x39, 0x52, 0x4B, 0x52, 0x2F, 0x35, 0x43, 0x59, 0x72, + 0x43, 0x73, 0x53, 0x58, 0x61, 0x51, 0x33, 0x70, 0x6A, 0x4F, 0x4C, 0x41, 0x45, 0x46, 0x65, 0x34, 0x79, 0x48, 0x59, 0x53, 0x6B, 0x56, 0x58, 0x79, 0x53, 0x47, 0x6E, 0x59, 0x76, 0x43, 0x6F, 0x43, 0x57, 0x77, 0x39, 0x45, 0x31, 0x43, 0x41, 0x78, + 0x32, 0x2F, 0x53, 0x36, 0x63, 0x43, 0x5A, 0x64, 0x6B, 0x47, 0x43, 0x65, 0x0A, 0x76, 0x45, 0x73, 0x58, 0x43, 0x53, 0x2B, 0x30, 0x79, 0x78, 0x35, 0x44, 0x61, 0x4D, 0x6B, 0x48, 0x4A, 0x38, 0x48, 0x53, 0x58, 0x50, 0x66, 0x71, 0x49, 0x62, 0x6C, + 0x6F, 0x45, 0x70, 0x77, 0x38, 0x6E, 0x4C, 0x2B, 0x65, 0x2F, 0x49, 0x42, 0x63, 0x6D, 0x32, 0x50, 0x4E, 0x37, 0x45, 0x65, 0x71, 0x4A, 0x53, 0x64, 0x6E, 0x6F, 0x44, 0x66, 0x7A, 0x41, 0x49, 0x4A, 0x39, 0x56, 0x4E, 0x65, 0x70, 0x2B, 0x4F, 0x6B, + 0x75, 0x45, 0x36, 0x4E, 0x33, 0x36, 0x42, 0x39, 0x4B, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x77, 0x69, + 0x73, 0x73, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x47, 0x6F, 0x6C, 0x64, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x75, 0x6A, 0x43, 0x43, 0x41, + 0x36, 0x4B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4A, 0x41, 0x4C, 0x74, 0x41, 0x48, 0x45, 0x50, 0x31, 0x58, 0x6B, 0x2B, 0x77, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, + 0x51, 0x55, 0x41, 0x4D, 0x45, 0x55, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x49, 0x4D, 0x52, 0x55, 0x77, 0x0A, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x78, 0x54, + 0x64, 0x32, 0x6C, 0x7A, 0x63, 0x31, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x51, 0x55, 0x63, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x6C, 0x4E, 0x33, 0x61, 0x58, 0x4E, 0x7A, 0x55, 0x32, 0x6C, 0x6E, + 0x62, 0x69, 0x42, 0x48, 0x62, 0x32, 0x78, 0x6B, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x52, 0x7A, 0x49, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x0A, 0x4D, 0x44, 0x59, 0x78, 0x4D, 0x44, 0x49, 0x31, 0x4D, 0x44, 0x67, 0x7A, 0x4D, 0x44, 0x4D, + 0x31, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x59, 0x78, 0x4D, 0x44, 0x49, 0x31, 0x4D, 0x44, 0x67, 0x7A, 0x4D, 0x44, 0x4D, 0x31, 0x57, 0x6A, 0x42, 0x46, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, + 0x44, 0x53, 0x44, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x55, 0x33, 0x64, 0x70, 0x0A, 0x63, 0x33, 0x4E, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x45, 0x46, 0x48, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x5A, 0x54, 0x64, 0x32, 0x6C, 0x7A, 0x63, 0x31, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x52, 0x32, 0x39, 0x73, 0x5A, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x45, 0x63, 0x79, 0x4D, 0x49, + 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x0A, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, + 0x67, 0x45, 0x41, 0x72, 0x2B, 0x54, 0x75, 0x66, 0x6F, 0x73, 0x6B, 0x44, 0x68, 0x4A, 0x75, 0x71, 0x56, 0x41, 0x74, 0x46, 0x6B, 0x51, 0x37, 0x6B, 0x70, 0x4A, 0x63, 0x79, 0x72, 0x68, 0x64, 0x68, 0x4A, 0x4A, 0x43, 0x45, 0x79, 0x71, 0x38, 0x5A, + 0x56, 0x65, 0x43, 0x51, 0x44, 0x35, 0x58, 0x4A, 0x4D, 0x31, 0x51, 0x69, 0x79, 0x55, 0x71, 0x0A, 0x74, 0x32, 0x2F, 0x38, 0x37, 0x36, 0x4C, 0x51, 0x77, 0x42, 0x38, 0x43, 0x4A, 0x45, 0x6F, 0x54, 0x6C, 0x6F, 0x38, 0x6A, 0x45, 0x2B, 0x59, 0x6F, + 0x57, 0x41, 0x43, 0x6A, 0x52, 0x38, 0x63, 0x47, 0x70, 0x34, 0x51, 0x6A, 0x4B, 0x37, 0x75, 0x39, 0x6C, 0x69, 0x74, 0x2F, 0x56, 0x63, 0x79, 0x4C, 0x77, 0x56, 0x63, 0x66, 0x44, 0x6D, 0x4A, 0x6C, 0x44, 0x39, 0x30, 0x39, 0x56, 0x6F, 0x70, 0x7A, + 0x32, 0x71, 0x35, 0x2B, 0x62, 0x62, 0x71, 0x42, 0x48, 0x48, 0x35, 0x43, 0x0A, 0x6A, 0x43, 0x41, 0x31, 0x32, 0x55, 0x4E, 0x4E, 0x68, 0x50, 0x71, 0x45, 0x32, 0x31, 0x49, 0x73, 0x38, 0x77, 0x34, 0x6E, 0x64, 0x77, 0x74, 0x72, 0x76, 0x78, 0x45, + 0x76, 0x63, 0x6E, 0x69, 0x66, 0x4C, 0x74, 0x67, 0x2B, 0x35, 0x68, 0x67, 0x33, 0x57, 0x69, 0x70, 0x79, 0x2B, 0x64, 0x70, 0x69, 0x6B, 0x4A, 0x4B, 0x56, 0x79, 0x68, 0x2B, 0x63, 0x36, 0x62, 0x4D, 0x38, 0x4B, 0x38, 0x76, 0x7A, 0x41, 0x52, 0x4F, + 0x2F, 0x57, 0x73, 0x2F, 0x42, 0x74, 0x51, 0x70, 0x67, 0x0A, 0x76, 0x64, 0x32, 0x31, 0x6D, 0x57, 0x52, 0x54, 0x75, 0x4B, 0x43, 0x57, 0x73, 0x32, 0x2F, 0x69, 0x4A, 0x6E, 0x65, 0x52, 0x6A, 0x4F, 0x42, 0x69, 0x45, 0x41, 0x4B, 0x66, 0x4E, 0x41, + 0x2B, 0x6B, 0x31, 0x5A, 0x49, 0x7A, 0x55, 0x64, 0x36, 0x2B, 0x6A, 0x62, 0x71, 0x45, 0x65, 0x6D, 0x41, 0x38, 0x61, 0x74, 0x75, 0x66, 0x4B, 0x2B, 0x7A, 0x65, 0x33, 0x67, 0x45, 0x2F, 0x62, 0x6B, 0x33, 0x6C, 0x55, 0x49, 0x62, 0x4C, 0x74, 0x4B, + 0x2F, 0x74, 0x52, 0x45, 0x44, 0x46, 0x0A, 0x79, 0x6C, 0x71, 0x4D, 0x32, 0x74, 0x49, 0x72, 0x66, 0x4B, 0x6A, 0x75, 0x76, 0x71, 0x62, 0x6C, 0x43, 0x71, 0x6F, 0x4F, 0x70, 0x64, 0x38, 0x46, 0x55, 0x72, 0x64, 0x56, 0x78, 0x79, 0x4A, 0x64, 0x4D, + 0x6D, 0x71, 0x58, 0x6C, 0x32, 0x4D, 0x54, 0x32, 0x38, 0x6E, 0x62, 0x65, 0x54, 0x5A, 0x37, 0x68, 0x54, 0x70, 0x4B, 0x78, 0x56, 0x4B, 0x4A, 0x2B, 0x53, 0x54, 0x6E, 0x6E, 0x58, 0x65, 0x70, 0x67, 0x76, 0x39, 0x56, 0x48, 0x4B, 0x56, 0x78, 0x61, + 0x53, 0x76, 0x52, 0x0A, 0x41, 0x69, 0x54, 0x79, 0x73, 0x79, 0x62, 0x55, 0x61, 0x39, 0x6F, 0x45, 0x56, 0x65, 0x58, 0x42, 0x43, 0x73, 0x64, 0x74, 0x4D, 0x44, 0x65, 0x51, 0x4B, 0x75, 0x53, 0x65, 0x46, 0x44, 0x4E, 0x65, 0x46, 0x68, 0x64, 0x56, + 0x78, 0x56, 0x75, 0x31, 0x79, 0x7A, 0x53, 0x4A, 0x6B, 0x76, 0x47, 0x64, 0x4A, 0x6F, 0x2B, 0x68, 0x42, 0x39, 0x54, 0x47, 0x73, 0x6E, 0x68, 0x51, 0x32, 0x77, 0x77, 0x4D, 0x43, 0x33, 0x77, 0x4C, 0x6A, 0x45, 0x48, 0x58, 0x75, 0x65, 0x6E, 0x64, + 0x0A, 0x6A, 0x49, 0x6A, 0x33, 0x6F, 0x30, 0x32, 0x79, 0x4D, 0x73, 0x7A, 0x59, 0x46, 0x39, 0x72, 0x4E, 0x74, 0x38, 0x35, 0x6D, 0x6E, 0x64, 0x54, 0x39, 0x58, 0x76, 0x2B, 0x39, 0x6C, 0x7A, 0x34, 0x70, 0x64, 0x65, 0x64, 0x2B, 0x70, 0x32, 0x4A, + 0x59, 0x72, 0x79, 0x55, 0x30, 0x70, 0x55, 0x48, 0x48, 0x50, 0x62, 0x77, 0x4E, 0x55, 0x4D, 0x6F, 0x44, 0x41, 0x77, 0x38, 0x49, 0x57, 0x68, 0x2B, 0x56, 0x63, 0x33, 0x68, 0x69, 0x76, 0x36, 0x39, 0x79, 0x46, 0x47, 0x6B, 0x4F, 0x0A, 0x70, 0x65, + 0x55, 0x44, 0x44, 0x6E, 0x69, 0x4F, 0x4A, 0x69, 0x68, 0x43, 0x38, 0x41, 0x63, 0x4C, 0x59, 0x69, 0x41, 0x51, 0x5A, 0x7A, 0x6C, 0x47, 0x2B, 0x71, 0x6B, 0x44, 0x7A, 0x41, 0x51, 0x34, 0x65, 0x6D, 0x62, 0x76, 0x49, 0x49, 0x4F, 0x31, 0x6A, 0x45, + 0x70, 0x57, 0x6A, 0x70, 0x45, 0x41, 0x2F, 0x49, 0x35, 0x63, 0x67, 0x74, 0x36, 0x49, 0x6F, 0x4D, 0x50, 0x69, 0x61, 0x47, 0x35, 0x39, 0x6A, 0x65, 0x38, 0x38, 0x33, 0x57, 0x58, 0x30, 0x58, 0x61, 0x78, 0x52, 0x0A, 0x37, 0x79, 0x53, 0x41, 0x72, + 0x71, 0x70, 0x57, 0x6C, 0x32, 0x2F, 0x35, 0x72, 0x58, 0x33, 0x61, 0x59, 0x54, 0x2B, 0x59, 0x64, 0x7A, 0x79, 0x6C, 0x6B, 0x62, 0x59, 0x63, 0x6A, 0x43, 0x62, 0x61, 0x5A, 0x61, 0x49, 0x4A, 0x62, 0x63, 0x48, 0x69, 0x56, 0x4F, 0x4F, 0x35, 0x79, + 0x6B, 0x78, 0x4D, 0x67, 0x49, 0x39, 0x33, 0x65, 0x32, 0x43, 0x61, 0x48, 0x74, 0x2B, 0x32, 0x38, 0x6B, 0x67, 0x65, 0x44, 0x72, 0x70, 0x4F, 0x56, 0x47, 0x32, 0x59, 0x34, 0x4F, 0x47, 0x69, 0x0A, 0x47, 0x71, 0x4A, 0x33, 0x55, 0x4D, 0x2F, 0x45, + 0x59, 0x35, 0x4C, 0x73, 0x52, 0x78, 0x6D, 0x64, 0x36, 0x2B, 0x5A, 0x72, 0x7A, 0x73, 0x45, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4F, 0x42, 0x72, 0x44, 0x43, 0x42, 0x71, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, + 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x0A, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, + 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x57, 0x79, 0x56, 0x37, 0x6C, 0x71, 0x52, 0x6C, 0x55, 0x58, 0x36, 0x34, 0x4F, 0x66, 0x50, 0x41, 0x65, 0x47, 0x5A, 0x65, 0x36, 0x44, 0x72, 0x6E, 0x38, 0x4F, 0x34, 0x77, 0x48, 0x77, 0x59, + 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, 0x55, 0x57, 0x79, 0x56, 0x37, 0x6C, 0x71, 0x52, 0x6C, 0x55, 0x58, 0x36, 0x34, 0x0A, 0x4F, 0x66, 0x50, 0x41, 0x65, 0x47, 0x5A, 0x65, 0x36, 0x44, 0x72, 0x6E, 0x38, 0x4F, + 0x34, 0x77, 0x52, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x67, 0x42, 0x44, 0x38, 0x77, 0x50, 0x54, 0x41, 0x37, 0x42, 0x67, 0x6C, 0x67, 0x68, 0x58, 0x51, 0x42, 0x57, 0x51, 0x45, 0x43, 0x41, 0x51, 0x45, 0x77, 0x4C, 0x6A, 0x41, 0x73, 0x42, 0x67, + 0x67, 0x72, 0x42, 0x67, 0x45, 0x46, 0x42, 0x51, 0x63, 0x43, 0x41, 0x52, 0x59, 0x67, 0x61, 0x48, 0x52, 0x30, 0x63, 0x44, 0x6F, 0x76, 0x0A, 0x4C, 0x33, 0x4A, 0x6C, 0x63, 0x47, 0x39, 0x7A, 0x61, 0x58, 0x52, 0x76, 0x63, 0x6E, 0x6B, 0x75, 0x63, + 0x33, 0x64, 0x70, 0x63, 0x33, 0x4E, 0x7A, 0x61, 0x57, 0x64, 0x75, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x44, 0x67, + 0x67, 0x49, 0x42, 0x41, 0x43, 0x65, 0x36, 0x34, 0x35, 0x52, 0x38, 0x38, 0x61, 0x37, 0x41, 0x33, 0x68, 0x66, 0x6D, 0x0A, 0x35, 0x64, 0x6A, 0x56, 0x39, 0x56, 0x53, 0x77, 0x67, 0x2F, 0x53, 0x37, 0x7A, 0x56, 0x34, 0x46, 0x65, 0x30, 0x2B, 0x66, + 0x64, 0x57, 0x61, 0x76, 0x50, 0x4F, 0x68, 0x57, 0x66, 0x76, 0x78, 0x79, 0x65, 0x44, 0x67, 0x44, 0x32, 0x53, 0x74, 0x69, 0x47, 0x77, 0x43, 0x35, 0x2B, 0x4F, 0x6C, 0x67, 0x7A, 0x63, 0x7A, 0x4F, 0x55, 0x59, 0x72, 0x48, 0x55, 0x44, 0x46, 0x75, + 0x34, 0x55, 0x70, 0x2B, 0x47, 0x43, 0x39, 0x70, 0x57, 0x62, 0x59, 0x39, 0x5A, 0x49, 0x45, 0x72, 0x0A, 0x34, 0x34, 0x4F, 0x45, 0x35, 0x69, 0x4B, 0x48, 0x6A, 0x6E, 0x33, 0x67, 0x37, 0x67, 0x4B, 0x5A, 0x59, 0x62, 0x67, 0x65, 0x39, 0x4C, 0x67, + 0x72, 0x69, 0x42, 0x49, 0x57, 0x68, 0x4D, 0x49, 0x78, 0x6B, 0x7A, 0x69, 0x57, 0x4D, 0x61, 0x61, 0x35, 0x4F, 0x31, 0x4D, 0x2F, 0x77, 0x79, 0x53, 0x54, 0x56, 0x6C, 0x74, 0x70, 0x6B, 0x75, 0x7A, 0x46, 0x77, 0x62, 0x73, 0x34, 0x41, 0x4F, 0x50, + 0x73, 0x46, 0x36, 0x6D, 0x34, 0x33, 0x4D, 0x64, 0x38, 0x41, 0x59, 0x4F, 0x66, 0x0A, 0x4D, 0x6B, 0x65, 0x36, 0x55, 0x69, 0x49, 0x30, 0x48, 0x54, 0x4A, 0x36, 0x43, 0x56, 0x61, 0x6E, 0x66, 0x43, 0x55, 0x32, 0x71, 0x54, 0x31, 0x4C, 0x32, 0x73, + 0x43, 0x43, 0x62, 0x77, 0x71, 0x37, 0x45, 0x73, 0x69, 0x48, 0x53, 0x79, 0x63, 0x52, 0x2B, 0x52, 0x34, 0x74, 0x78, 0x35, 0x4D, 0x2F, 0x6E, 0x74, 0x74, 0x66, 0x4A, 0x6D, 0x74, 0x53, 0x32, 0x53, 0x36, 0x4B, 0x38, 0x52, 0x54, 0x47, 0x52, 0x49, + 0x30, 0x56, 0x71, 0x62, 0x65, 0x2F, 0x76, 0x64, 0x36, 0x6D, 0x0A, 0x47, 0x75, 0x36, 0x75, 0x4C, 0x66, 0x74, 0x49, 0x64, 0x78, 0x66, 0x2B, 0x75, 0x2B, 0x79, 0x76, 0x47, 0x50, 0x55, 0x71, 0x55, 0x66, 0x41, 0x35, 0x68, 0x4A, 0x65, 0x56, 0x62, + 0x47, 0x34, 0x62, 0x77, 0x79, 0x76, 0x45, 0x64, 0x47, 0x42, 0x35, 0x4A, 0x62, 0x41, 0x4B, 0x4A, 0x39, 0x2F, 0x66, 0x58, 0x74, 0x49, 0x35, 0x7A, 0x30, 0x56, 0x39, 0x51, 0x6B, 0x76, 0x66, 0x73, 0x79, 0x77, 0x65, 0x78, 0x63, 0x5A, 0x64, 0x79, + 0x6C, 0x55, 0x36, 0x6F, 0x4A, 0x78, 0x70, 0x0A, 0x6D, 0x6F, 0x2F, 0x61, 0x37, 0x37, 0x4B, 0x77, 0x50, 0x4A, 0x2B, 0x48, 0x62, 0x42, 0x49, 0x72, 0x5A, 0x58, 0x41, 0x56, 0x55, 0x6A, 0x45, 0x61, 0x4A, 0x4D, 0x39, 0x76, 0x4D, 0x53, 0x4E, 0x51, + 0x48, 0x34, 0x78, 0x50, 0x6A, 0x79, 0x50, 0x44, 0x64, 0x45, 0x46, 0x6A, 0x48, 0x46, 0x57, 0x6F, 0x46, 0x4E, 0x30, 0x2B, 0x34, 0x46, 0x46, 0x51, 0x7A, 0x2F, 0x45, 0x62, 0x4D, 0x46, 0x59, 0x4F, 0x6B, 0x72, 0x43, 0x43, 0x68, 0x64, 0x69, 0x44, + 0x79, 0x79, 0x4A, 0x6B, 0x0A, 0x76, 0x43, 0x32, 0x34, 0x4A, 0x64, 0x56, 0x55, 0x6F, 0x72, 0x67, 0x47, 0x36, 0x71, 0x32, 0x53, 0x70, 0x43, 0x53, 0x67, 0x77, 0x59, 0x61, 0x31, 0x53, 0x68, 0x4E, 0x71, 0x52, 0x38, 0x38, 0x75, 0x43, 0x31, 0x61, + 0x56, 0x56, 0x4D, 0x76, 0x4F, 0x6D, 0x74, 0x74, 0x71, 0x74, 0x4B, 0x61, 0x79, 0x32, 0x30, 0x45, 0x49, 0x68, 0x69, 0x64, 0x33, 0x39, 0x32, 0x71, 0x67, 0x51, 0x6D, 0x77, 0x4C, 0x4F, 0x4D, 0x37, 0x58, 0x64, 0x56, 0x41, 0x79, 0x6B, 0x73, 0x4C, + 0x66, 0x0A, 0x4B, 0x7A, 0x41, 0x69, 0x53, 0x4E, 0x44, 0x56, 0x51, 0x54, 0x67, 0x6C, 0x58, 0x61, 0x54, 0x70, 0x58, 0x5A, 0x2F, 0x47, 0x6C, 0x48, 0x58, 0x51, 0x52, 0x66, 0x30, 0x77, 0x6C, 0x30, 0x4F, 0x50, 0x6B, 0x4B, 0x73, 0x4B, 0x78, 0x34, + 0x5A, 0x7A, 0x59, 0x45, 0x70, 0x70, 0x4C, 0x64, 0x36, 0x6C, 0x65, 0x4E, 0x63, 0x47, 0x32, 0x6D, 0x71, 0x65, 0x53, 0x7A, 0x35, 0x33, 0x4F, 0x69, 0x41, 0x54, 0x49, 0x67, 0x48, 0x51, 0x76, 0x32, 0x69, 0x65, 0x59, 0x32, 0x42, 0x72, 0x0A, 0x4E, + 0x55, 0x30, 0x4C, 0x62, 0x62, 0x71, 0x68, 0x50, 0x63, 0x43, 0x54, 0x34, 0x48, 0x38, 0x6A, 0x73, 0x31, 0x57, 0x74, 0x63, 0x69, 0x56, 0x4F, 0x52, 0x76, 0x6E, 0x53, 0x46, 0x75, 0x2B, 0x77, 0x5A, 0x4D, 0x45, 0x42, 0x6E, 0x75, 0x6E, 0x4B, 0x6F, + 0x47, 0x71, 0x59, 0x44, 0x73, 0x2F, 0x59, 0x59, 0x50, 0x49, 0x76, 0x53, 0x62, 0x6A, 0x6B, 0x51, 0x75, 0x45, 0x34, 0x4E, 0x52, 0x62, 0x30, 0x79, 0x47, 0x35, 0x50, 0x39, 0x34, 0x46, 0x57, 0x36, 0x4C, 0x71, 0x6A, 0x0A, 0x76, 0x69, 0x4F, 0x76, + 0x72, 0x76, 0x31, 0x76, 0x41, 0x2B, 0x41, 0x43, 0x4F, 0x7A, 0x42, 0x32, 0x2B, 0x68, 0x74, 0x74, 0x51, 0x63, 0x38, 0x42, 0x73, 0x65, 0x6D, 0x34, 0x79, 0x57, 0x62, 0x30, 0x32, 0x79, 0x62, 0x7A, 0x4F, 0x71, 0x52, 0x30, 0x38, 0x6B, 0x6B, 0x6B, + 0x57, 0x38, 0x6D, 0x77, 0x30, 0x46, 0x66, 0x42, 0x2B, 0x6A, 0x35, 0x36, 0x34, 0x5A, 0x66, 0x4A, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, + 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x75, 0x44, 0x43, 0x43, 0x41, 0x71, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x51, 0x44, 0x50, 0x43, 0x4F, 0x58, 0x41, 0x67, 0x57, 0x70, 0x61, 0x31, 0x43, 0x66, 0x2F, 0x44, 0x72, 0x4A, 0x78, 0x68, 0x5A, 0x30, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, + 0x55, 0x46, 0x41, 0x44, 0x42, 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x58, 0x55, + 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x6C, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x51, 0x32, 0x39, 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x78, 0x46, 0x7A, 0x41, 0x56, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x4D, 0x54, 0x44, 0x6C, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x5A, 0x56, 0x52, 0x79, 0x0A, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x32, 0x4D, 0x54, 0x45, 0x77, 0x4E, 0x7A, 0x45, 0x35, + 0x4D, 0x7A, 0x45, 0x78, 0x4F, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x49, 0x35, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x45, 0x35, 0x4E, 0x44, 0x41, 0x31, 0x4E, 0x56, 0x6F, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x49, 0x44, 0x41, 0x65, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x46, 0x31, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x5A, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x4E, + 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, 0x52, 0x63, 0x77, 0x46, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x77, 0x35, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x55, 0x63, 0x6E, 0x56, + 0x7A, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x0A, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, + 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4B, 0x75, 0x6B, 0x67, 0x65, 0x57, 0x56, 0x7A, 0x66, 0x58, 0x32, 0x46, 0x49, 0x37, 0x43, 0x54, 0x38, 0x72, 0x55, 0x34, 0x6E, 0x69, 0x56, 0x57, 0x4A, 0x78, 0x42, 0x34, 0x51, + 0x32, 0x5A, 0x51, 0x43, 0x51, 0x58, 0x0A, 0x4F, 0x5A, 0x45, 0x7A, 0x5A, 0x75, 0x6D, 0x2B, 0x34, 0x59, 0x4F, 0x76, 0x59, 0x6C, 0x79, 0x4A, 0x30, 0x66, 0x77, 0x6B, 0x57, 0x32, 0x47, 0x7A, 0x34, 0x42, 0x45, 0x52, 0x51, 0x52, 0x77, 0x64, 0x62, + 0x76, 0x43, 0x34, 0x75, 0x2F, 0x6A, 0x65, 0x70, 0x34, 0x47, 0x36, 0x70, 0x6B, 0x6A, 0x47, 0x6E, 0x78, 0x32, 0x39, 0x76, 0x6F, 0x36, 0x70, 0x51, 0x54, 0x36, 0x34, 0x6C, 0x4F, 0x30, 0x70, 0x47, 0x74, 0x53, 0x4F, 0x30, 0x67, 0x4D, 0x64, 0x41, + 0x2B, 0x39, 0x74, 0x0A, 0x44, 0x57, 0x63, 0x63, 0x56, 0x39, 0x63, 0x47, 0x72, 0x63, 0x72, 0x49, 0x39, 0x66, 0x34, 0x4F, 0x72, 0x32, 0x59, 0x6C, 0x53, 0x41, 0x53, 0x57, 0x43, 0x31, 0x32, 0x6A, 0x75, 0x68, 0x62, 0x44, 0x43, 0x45, 0x2F, 0x52, + 0x52, 0x76, 0x67, 0x55, 0x58, 0x50, 0x4C, 0x49, 0x58, 0x67, 0x47, 0x5A, 0x62, 0x66, 0x32, 0x49, 0x7A, 0x49, 0x61, 0x6F, 0x77, 0x57, 0x38, 0x78, 0x51, 0x6D, 0x78, 0x53, 0x50, 0x6D, 0x6A, 0x4C, 0x38, 0x78, 0x6B, 0x30, 0x33, 0x37, 0x75, 0x48, + 0x0A, 0x47, 0x46, 0x61, 0x41, 0x4A, 0x73, 0x54, 0x51, 0x33, 0x4D, 0x42, 0x76, 0x33, 0x39, 0x36, 0x67, 0x77, 0x70, 0x45, 0x57, 0x6F, 0x47, 0x51, 0x52, 0x53, 0x30, 0x53, 0x38, 0x48, 0x76, 0x62, 0x6E, 0x2B, 0x6D, 0x50, 0x65, 0x5A, 0x71, 0x78, + 0x32, 0x70, 0x48, 0x47, 0x6A, 0x37, 0x44, 0x61, 0x55, 0x61, 0x48, 0x70, 0x33, 0x70, 0x4C, 0x48, 0x6E, 0x44, 0x69, 0x2B, 0x42, 0x65, 0x75, 0x4B, 0x31, 0x63, 0x6F, 0x62, 0x76, 0x6F, 0x6D, 0x75, 0x4C, 0x38, 0x41, 0x2F, 0x62, 0x0A, 0x30, 0x31, + 0x6B, 0x2F, 0x75, 0x6E, 0x4B, 0x38, 0x52, 0x43, 0x53, 0x63, 0x34, 0x33, 0x4F, 0x7A, 0x39, 0x36, 0x39, 0x58, 0x4C, 0x30, 0x49, 0x6D, 0x6E, 0x61, 0x6C, 0x30, 0x75, 0x67, 0x42, 0x53, 0x38, 0x6B, 0x76, 0x4E, 0x55, 0x33, 0x78, 0x48, 0x43, 0x7A, + 0x61, 0x46, 0x44, 0x6D, 0x61, 0x70, 0x43, 0x4A, 0x63, 0x57, 0x4E, 0x46, 0x66, 0x42, 0x5A, 0x76, 0x65, 0x41, 0x34, 0x2B, 0x31, 0x77, 0x56, 0x4D, 0x65, 0x54, 0x34, 0x43, 0x34, 0x6F, 0x46, 0x56, 0x6D, 0x48, 0x0A, 0x75, 0x72, 0x73, 0x43, 0x41, + 0x77, 0x45, 0x41, 0x41, 0x61, 0x4F, 0x42, 0x6E, 0x54, 0x43, 0x42, 0x6D, 0x6A, 0x41, 0x54, 0x42, 0x67, 0x6B, 0x72, 0x42, 0x67, 0x45, 0x45, 0x41, 0x59, 0x49, 0x33, 0x46, 0x41, 0x49, 0x45, 0x42, 0x68, 0x34, 0x45, 0x41, 0x45, 0x4D, 0x41, 0x51, + 0x54, 0x41, 0x4C, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x0A, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, + 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x51, 0x6A, 0x4B, 0x32, 0x46, 0x76, 0x6F, 0x45, 0x2F, 0x66, 0x35, 0x64, 0x53, 0x33, 0x72, 0x44, 0x2F, 0x66, 0x64, 0x4D, 0x51, 0x42, 0x31, 0x61, + 0x51, 0x36, 0x38, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x52, 0x30, 0x66, 0x42, 0x43, 0x30, 0x77, 0x4B, 0x7A, 0x41, 0x70, 0x6F, 0x43, 0x65, 0x67, 0x4A, 0x59, 0x59, 0x6A, 0x0A, 0x61, 0x48, 0x52, 0x30, 0x63, 0x44, 0x6F, 0x76, 0x4C, 0x32, 0x4E, + 0x79, 0x62, 0x43, 0x35, 0x7A, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, 0x76, 0x55, 0x31, 0x52, 0x44, 0x51, 0x53, 0x35, 0x6A, 0x63, 0x6D, 0x77, 0x77, 0x45, 0x41, 0x59, + 0x4A, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x41, 0x47, 0x43, 0x4E, 0x78, 0x55, 0x42, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x41, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x0A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, + 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x44, 0x44, 0x74, 0x54, 0x30, 0x72, 0x68, 0x57, 0x44, 0x70, 0x53, 0x63, 0x6C, 0x75, 0x31, 0x70, 0x71, 0x4E, 0x6C, 0x47, 0x4B, 0x61, 0x37, 0x55, 0x54, 0x74, 0x33, 0x36, 0x5A, 0x33, 0x71, 0x30, 0x35, + 0x39, 0x63, 0x34, 0x45, 0x56, 0x6C, 0x65, 0x77, 0x33, 0x4B, 0x57, 0x2B, 0x4A, 0x77, 0x55, 0x4C, 0x4B, 0x55, 0x42, 0x52, 0x53, 0x75, 0x0A, 0x53, 0x63, 0x65, 0x4E, 0x51, 0x51, 0x63, 0x53, 0x63, 0x35, 0x52, 0x2B, 0x44, 0x43, 0x4D, 0x68, 0x2F, + 0x62, 0x77, 0x51, 0x66, 0x32, 0x41, 0x51, 0x57, 0x6E, 0x4C, 0x31, 0x6D, 0x41, 0x36, 0x73, 0x37, 0x4C, 0x6C, 0x2F, 0x33, 0x58, 0x70, 0x76, 0x58, 0x64, 0x4D, 0x63, 0x39, 0x50, 0x2B, 0x49, 0x42, 0x57, 0x6C, 0x43, 0x71, 0x51, 0x56, 0x78, 0x79, + 0x4C, 0x65, 0x73, 0x4A, 0x75, 0x67, 0x75, 0x74, 0x49, 0x78, 0x71, 0x2F, 0x33, 0x48, 0x63, 0x75, 0x4C, 0x48, 0x66, 0x0A, 0x6D, 0x62, 0x78, 0x38, 0x49, 0x56, 0x51, 0x72, 0x35, 0x46, 0x69, 0x69, 0x75, 0x31, 0x63, 0x70, 0x72, 0x70, 0x36, 0x70, + 0x6F, 0x78, 0x6B, 0x6D, 0x44, 0x35, 0x6B, 0x75, 0x43, 0x4C, 0x44, 0x76, 0x2F, 0x57, 0x6E, 0x50, 0x6D, 0x52, 0x6F, 0x4A, 0x6A, 0x65, 0x4F, 0x6E, 0x6E, 0x79, 0x76, 0x4A, 0x4E, 0x6A, 0x52, 0x37, 0x4A, 0x4C, 0x4E, 0x34, 0x54, 0x4A, 0x55, 0x58, + 0x70, 0x41, 0x59, 0x6D, 0x48, 0x72, 0x5A, 0x6B, 0x55, 0x6A, 0x5A, 0x66, 0x59, 0x47, 0x66, 0x5A, 0x0A, 0x6E, 0x4D, 0x55, 0x46, 0x64, 0x41, 0x76, 0x6E, 0x5A, 0x79, 0x50, 0x53, 0x43, 0x50, 0x79, 0x49, 0x36, 0x61, 0x36, 0x4C, 0x66, 0x2B, 0x45, + 0x77, 0x39, 0x44, 0x64, 0x2B, 0x2F, 0x63, 0x59, 0x79, 0x32, 0x69, 0x32, 0x65, 0x52, 0x44, 0x41, 0x77, 0x62, 0x4F, 0x34, 0x48, 0x33, 0x74, 0x49, 0x30, 0x2F, 0x4E, 0x4C, 0x2F, 0x51, 0x50, 0x5A, 0x4C, 0x39, 0x47, 0x5A, 0x47, 0x42, 0x6C, 0x53, + 0x6D, 0x38, 0x6A, 0x49, 0x4B, 0x59, 0x79, 0x59, 0x77, 0x61, 0x35, 0x76, 0x52, 0x0A, 0x33, 0x49, 0x74, 0x48, 0x75, 0x75, 0x47, 0x35, 0x31, 0x57, 0x4C, 0x51, 0x6F, 0x71, 0x44, 0x30, 0x5A, 0x77, 0x56, 0x34, 0x4B, 0x57, 0x4D, 0x61, 0x62, 0x77, + 0x54, 0x57, 0x2B, 0x4D, 0x5A, 0x4D, 0x6F, 0x35, 0x71, 0x78, 0x4E, 0x37, 0x53, 0x4E, 0x35, 0x53, 0x68, 0x4C, 0x48, 0x5A, 0x34, 0x73, 0x77, 0x72, 0x68, 0x6F, 0x76, 0x4F, 0x30, 0x43, 0x37, 0x6A, 0x45, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x43, 0x41, 0x0A, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x76, 0x44, 0x43, 0x43, 0x41, 0x71, 0x53, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x42, 0x31, 0x59, 0x69, 0x70, 0x4F, 0x6A, 0x55, 0x69, 0x6F, 0x6C, 0x4E, 0x39, 0x42, 0x50, 0x49, + 0x38, 0x50, 0x6A, 0x71, 0x70, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x44, 0x42, 0x4B, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, + 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x58, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x6C, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x51, 0x32, 0x39, + 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x5A, 0x53, 0x42, 0x48, 0x0A, 0x62, 0x47, + 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x59, 0x78, 0x4D, 0x54, 0x41, 0x33, 0x4D, 0x54, 0x6B, 0x30, 0x4D, 0x6A, 0x49, 0x34, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x6B, 0x78, 0x4D, 0x6A, + 0x4D, 0x78, 0x4D, 0x54, 0x6B, 0x31, 0x4D, 0x6A, 0x41, 0x32, 0x57, 0x6A, 0x42, 0x4B, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x67, 0x0A, 0x4D, 0x42, 0x34, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x58, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x6C, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x51, 0x32, 0x39, 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x78, 0x47, + 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x5A, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x0A, 0x51, 0x30, 0x45, 0x77, 0x67, 0x67, 0x45, 0x69, + 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x76, + 0x4E, 0x53, 0x37, 0x59, 0x72, 0x47, 0x78, 0x56, 0x61, 0x51, 0x5A, 0x78, 0x35, 0x52, 0x4E, 0x6F, 0x4A, 0x4C, 0x4E, 0x50, 0x32, 0x4D, 0x77, 0x68, 0x52, 0x2F, 0x6A, 0x78, 0x0A, 0x59, 0x44, 0x69, 0x4A, 0x69, 0x51, 0x50, 0x70, 0x76, 0x65, 0x70, + 0x65, 0x52, 0x6C, 0x4D, 0x4A, 0x33, 0x46, 0x7A, 0x31, 0x57, 0x75, 0x6A, 0x33, 0x52, 0x53, 0x6F, 0x43, 0x36, 0x7A, 0x46, 0x68, 0x31, 0x79, 0x6B, 0x7A, 0x54, 0x4D, 0x37, 0x48, 0x66, 0x41, 0x6F, 0x33, 0x66, 0x67, 0x2B, 0x36, 0x4D, 0x70, 0x6A, + 0x68, 0x48, 0x5A, 0x65, 0x76, 0x6A, 0x38, 0x66, 0x63, 0x79, 0x54, 0x69, 0x57, 0x38, 0x39, 0x73, 0x61, 0x2F, 0x46, 0x48, 0x74, 0x61, 0x4D, 0x62, 0x51, 0x0A, 0x62, 0x71, 0x52, 0x38, 0x4A, 0x4E, 0x47, 0x75, 0x51, 0x73, 0x69, 0x57, 0x55, 0x47, + 0x4D, 0x75, 0x34, 0x50, 0x35, 0x31, 0x2F, 0x70, 0x69, 0x6E, 0x58, 0x30, 0x6B, 0x75, 0x6C, 0x65, 0x4D, 0x35, 0x4D, 0x32, 0x53, 0x4F, 0x48, 0x71, 0x52, 0x66, 0x6B, 0x4E, 0x4A, 0x6E, 0x50, 0x4C, 0x4C, 0x5A, 0x2F, 0x6B, 0x47, 0x35, 0x56, 0x61, + 0x63, 0x4A, 0x6A, 0x6E, 0x49, 0x46, 0x48, 0x6F, 0x76, 0x64, 0x52, 0x49, 0x57, 0x43, 0x51, 0x74, 0x42, 0x4A, 0x77, 0x42, 0x31, 0x67, 0x0A, 0x38, 0x4E, 0x45, 0x58, 0x4C, 0x4A, 0x58, 0x72, 0x39, 0x71, 0x58, 0x42, 0x6B, 0x71, 0x50, 0x46, 0x77, + 0x71, 0x63, 0x49, 0x59, 0x41, 0x31, 0x67, 0x42, 0x42, 0x43, 0x57, 0x65, 0x5A, 0x34, 0x57, 0x4E, 0x4F, 0x61, 0x70, 0x74, 0x76, 0x6F, 0x6C, 0x52, 0x54, 0x6E, 0x49, 0x48, 0x6D, 0x58, 0x35, 0x6B, 0x2F, 0x57, 0x71, 0x38, 0x56, 0x4C, 0x63, 0x6D, + 0x5A, 0x67, 0x39, 0x70, 0x59, 0x59, 0x61, 0x44, 0x44, 0x55, 0x7A, 0x2B, 0x6B, 0x75, 0x6C, 0x42, 0x41, 0x59, 0x56, 0x0A, 0x48, 0x44, 0x47, 0x41, 0x37, 0x36, 0x6F, 0x59, 0x61, 0x38, 0x4A, 0x37, 0x31, 0x39, 0x72, 0x4F, 0x2B, 0x54, 0x4D, 0x67, + 0x31, 0x66, 0x57, 0x39, 0x61, 0x6A, 0x4D, 0x74, 0x67, 0x51, 0x54, 0x37, 0x73, 0x46, 0x7A, 0x55, 0x6E, 0x4B, 0x50, 0x69, 0x58, 0x42, 0x33, 0x6A, 0x71, 0x55, 0x4A, 0x31, 0x58, 0x6E, 0x76, 0x55, 0x64, 0x2B, 0x38, 0x35, 0x56, 0x4C, 0x72, 0x4A, + 0x43, 0x68, 0x67, 0x62, 0x45, 0x70, 0x6C, 0x4A, 0x4C, 0x34, 0x68, 0x4C, 0x2F, 0x56, 0x42, 0x69, 0x0A, 0x30, 0x58, 0x50, 0x6E, 0x6A, 0x33, 0x70, 0x44, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x67, 0x5A, 0x30, 0x77, 0x67, 0x5A, 0x6F, + 0x77, 0x45, 0x77, 0x59, 0x4A, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x41, 0x47, 0x43, 0x4E, 0x78, 0x51, 0x43, 0x42, 0x41, 0x59, 0x65, 0x42, 0x41, 0x42, 0x44, 0x41, 0x45, 0x45, 0x77, 0x43, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x42, 0x41, 0x51, + 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, + 0x59, 0x45, 0x46, 0x4B, 0x39, 0x45, 0x42, 0x4D, 0x4A, 0x42, 0x66, 0x6B, 0x69, 0x44, 0x32, 0x30, 0x34, 0x35, 0x41, 0x75, 0x7A, 0x73, 0x68, 0x48, 0x72, 0x6D, 0x7A, 0x73, 0x6D, 0x6B, 0x4D, 0x44, 0x51, 0x47, 0x41, 0x31, 0x55, 0x64, 0x48, 0x77, + 0x51, 0x74, 0x4D, 0x43, 0x73, 0x77, 0x4B, 0x61, 0x41, 0x6E, 0x0A, 0x6F, 0x43, 0x57, 0x47, 0x49, 0x32, 0x68, 0x30, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x6A, 0x63, 0x6D, 0x77, 0x75, 0x63, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x6C, 0x64, + 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4C, 0x31, 0x4E, 0x48, 0x51, 0x30, 0x45, 0x75, 0x59, 0x33, 0x4A, 0x73, 0x4D, 0x42, 0x41, 0x47, 0x43, 0x53, 0x73, 0x47, 0x41, 0x51, 0x51, 0x42, 0x67, 0x6A, 0x63, 0x56, 0x41, + 0x51, 0x51, 0x44, 0x41, 0x67, 0x45, 0x41, 0x0A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x42, 0x6A, 0x47, 0x67, 0x68, 0x41, + 0x66, 0x61, 0x52, 0x65, 0x55, 0x77, 0x31, 0x33, 0x32, 0x48, 0x71, 0x75, 0x48, 0x77, 0x30, 0x4C, 0x55, 0x52, 0x59, 0x44, 0x37, 0x78, 0x68, 0x38, 0x79, 0x4F, 0x4F, 0x76, 0x61, 0x6C, 0x69, 0x54, 0x46, 0x47, 0x43, 0x52, 0x73, 0x6F, 0x54, 0x63, + 0x69, 0x45, 0x36, 0x2B, 0x0A, 0x4F, 0x59, 0x6F, 0x36, 0x38, 0x2B, 0x61, 0x43, 0x69, 0x56, 0x30, 0x42, 0x4E, 0x37, 0x4F, 0x72, 0x4A, 0x4B, 0x51, 0x56, 0x44, 0x70, 0x49, 0x31, 0x57, 0x6B, 0x70, 0x45, 0x58, 0x6B, 0x35, 0x58, 0x2B, 0x6E, 0x58, + 0x4F, 0x48, 0x30, 0x6A, 0x4F, 0x5A, 0x76, 0x51, 0x38, 0x51, 0x43, 0x61, 0x53, 0x6D, 0x47, 0x77, 0x62, 0x37, 0x69, 0x52, 0x47, 0x44, 0x42, 0x65, 0x7A, 0x55, 0x71, 0x58, 0x62, 0x70, 0x5A, 0x47, 0x52, 0x7A, 0x7A, 0x66, 0x54, 0x62, 0x2B, 0x63, + 0x6E, 0x0A, 0x43, 0x44, 0x70, 0x4F, 0x47, 0x52, 0x38, 0x36, 0x70, 0x31, 0x68, 0x63, 0x46, 0x38, 0x39, 0x35, 0x50, 0x34, 0x76, 0x6B, 0x70, 0x39, 0x4D, 0x6D, 0x49, 0x35, 0x30, 0x6D, 0x44, 0x31, 0x68, 0x70, 0x2F, 0x45, 0x64, 0x2B, 0x73, 0x74, + 0x43, 0x4E, 0x69, 0x35, 0x4F, 0x2F, 0x4B, 0x55, 0x39, 0x44, 0x61, 0x58, 0x52, 0x32, 0x5A, 0x30, 0x76, 0x50, 0x42, 0x34, 0x7A, 0x6D, 0x41, 0x76, 0x65, 0x31, 0x34, 0x62, 0x52, 0x44, 0x74, 0x55, 0x73, 0x74, 0x46, 0x4A, 0x2F, 0x35, 0x0A, 0x33, + 0x43, 0x59, 0x4E, 0x76, 0x36, 0x5A, 0x48, 0x64, 0x41, 0x62, 0x59, 0x69, 0x4E, 0x45, 0x36, 0x4B, 0x54, 0x43, 0x45, 0x7A, 0x74, 0x49, 0x35, 0x67, 0x47, 0x49, 0x62, 0x71, 0x4D, 0x64, 0x58, 0x53, 0x62, 0x78, 0x71, 0x56, 0x56, 0x46, 0x6E, 0x46, + 0x55, 0x71, 0x2B, 0x4E, 0x51, 0x66, 0x6B, 0x31, 0x58, 0x57, 0x59, 0x4E, 0x33, 0x6B, 0x77, 0x46, 0x4E, 0x73, 0x70, 0x6E, 0x57, 0x7A, 0x46, 0x61, 0x63, 0x78, 0x48, 0x56, 0x61, 0x49, 0x77, 0x39, 0x38, 0x78, 0x63, 0x0A, 0x66, 0x38, 0x4C, 0x44, + 0x6D, 0x42, 0x78, 0x72, 0x54, 0x68, 0x61, 0x41, 0x36, 0x33, 0x70, 0x34, 0x5A, 0x55, 0x57, 0x69, 0x41, 0x42, 0x71, 0x76, 0x44, 0x41, 0x31, 0x56, 0x5A, 0x44, 0x52, 0x49, 0x75, 0x4A, 0x4B, 0x35, 0x38, 0x62, 0x52, 0x51, 0x4B, 0x66, 0x4A, 0x50, + 0x49, 0x78, 0x2F, 0x61, 0x62, 0x4B, 0x77, 0x66, 0x52, 0x4F, 0x48, 0x64, 0x49, 0x33, 0x68, 0x52, 0x57, 0x38, 0x63, 0x57, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x4F, 0x4D, 0x4F, 0x44, 0x4F, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x48, 0x54, 0x43, 0x43, 0x41, 0x77, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x54, 0x6F, + 0x45, 0x74, 0x69, 0x6F, 0x4A, 0x6C, 0x34, 0x41, 0x73, 0x43, 0x37, 0x6A, 0x34, 0x31, 0x41, 0x6B, 0x62, 0x6C, 0x50, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x44, + 0x43, 0x42, 0x67, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x30, 0x49, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x45, 0x6B, 0x64, 0x79, 0x5A, + 0x57, 0x46, 0x30, 0x5A, 0x58, 0x49, 0x67, 0x54, 0x57, 0x46, 0x75, 0x59, 0x32, 0x68, 0x6C, 0x63, 0x33, 0x52, 0x6C, 0x63, 0x6A, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x48, 0x55, 0x32, 0x46, 0x73, 0x5A, + 0x6D, 0x39, 0x79, 0x5A, 0x44, 0x45, 0x61, 0x4D, 0x42, 0x67, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x52, 0x51, 0x30, 0x39, 0x4E, 0x54, 0x30, 0x52, 0x50, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, + 0x5A, 0x57, 0x51, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x48, 0x6B, 0x4E, 0x50, 0x54, 0x55, 0x39, 0x45, 0x54, 0x79, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, + 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x0A, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x77, 0x4E, 0x6A, 0x45, 0x79, 0x4D, 0x44, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, + 0x61, 0x46, 0x77, 0x30, 0x79, 0x4F, 0x54, 0x45, 0x79, 0x4D, 0x7A, 0x45, 0x79, 0x4D, 0x7A, 0x55, 0x35, 0x4E, 0x54, 0x6C, 0x61, 0x4D, 0x49, 0x47, 0x42, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, + 0x48, 0x51, 0x6A, 0x45, 0x62, 0x0A, 0x4D, 0x42, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x42, 0x4D, 0x53, 0x52, 0x33, 0x4A, 0x6C, 0x59, 0x58, 0x52, 0x6C, 0x63, 0x69, 0x42, 0x4E, 0x59, 0x57, 0x35, 0x6A, 0x61, 0x47, 0x56, 0x7A, 0x64, 0x47, + 0x56, 0x79, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x45, 0x77, 0x64, 0x54, 0x59, 0x57, 0x78, 0x6D, 0x62, 0x33, 0x4A, 0x6B, 0x4D, 0x52, 0x6F, 0x77, 0x47, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, + 0x46, 0x44, 0x0A, 0x54, 0x30, 0x31, 0x50, 0x52, 0x45, 0x38, 0x67, 0x51, 0x30, 0x45, 0x67, 0x54, 0x47, 0x6C, 0x74, 0x61, 0x58, 0x52, 0x6C, 0x5A, 0x44, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x65, 0x51, + 0x30, 0x39, 0x4E, 0x54, 0x30, 0x52, 0x50, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x0A, + 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, + 0x41, 0x51, 0x45, 0x41, 0x30, 0x45, 0x43, 0x4C, 0x69, 0x33, 0x4C, 0x6A, 0x6B, 0x52, 0x76, 0x33, 0x55, 0x63, 0x45, 0x62, 0x56, 0x41, 0x53, 0x59, 0x30, 0x36, 0x6D, 0x2F, 0x77, 0x65, 0x61, 0x4B, 0x58, 0x54, 0x75, 0x48, 0x0A, 0x2B, 0x37, 0x75, + 0x49, 0x7A, 0x67, 0x33, 0x6A, 0x4C, 0x7A, 0x38, 0x47, 0x6C, 0x76, 0x43, 0x69, 0x4B, 0x56, 0x43, 0x5A, 0x72, 0x74, 0x73, 0x37, 0x6F, 0x56, 0x65, 0x77, 0x64, 0x46, 0x46, 0x78, 0x7A, 0x65, 0x31, 0x43, 0x6B, 0x55, 0x31, 0x42, 0x2F, 0x71, 0x6E, + 0x49, 0x32, 0x47, 0x71, 0x47, 0x64, 0x30, 0x53, 0x37, 0x57, 0x57, 0x61, 0x58, 0x55, 0x46, 0x36, 0x30, 0x31, 0x43, 0x78, 0x77, 0x52, 0x4D, 0x2F, 0x61, 0x4E, 0x35, 0x56, 0x43, 0x61, 0x54, 0x77, 0x77, 0x0A, 0x78, 0x48, 0x47, 0x7A, 0x55, 0x76, + 0x41, 0x68, 0x54, 0x61, 0x48, 0x59, 0x75, 0x6A, 0x6C, 0x38, 0x48, 0x4A, 0x36, 0x6A, 0x4A, 0x4A, 0x33, 0x79, 0x67, 0x78, 0x61, 0x59, 0x71, 0x68, 0x5A, 0x38, 0x51, 0x35, 0x73, 0x56, 0x57, 0x37, 0x65, 0x75, 0x4E, 0x4A, 0x48, 0x2B, 0x31, 0x47, + 0x49, 0x6D, 0x47, 0x45, 0x61, 0x61, 0x50, 0x2B, 0x76, 0x42, 0x2B, 0x66, 0x47, 0x51, 0x56, 0x2B, 0x75, 0x73, 0x65, 0x67, 0x32, 0x4C, 0x32, 0x33, 0x49, 0x77, 0x61, 0x6D, 0x62, 0x56, 0x0A, 0x34, 0x45, 0x61, 0x6A, 0x63, 0x4E, 0x78, 0x6F, 0x32, + 0x66, 0x38, 0x45, 0x53, 0x49, 0x6C, 0x33, 0x33, 0x72, 0x58, 0x70, 0x2B, 0x32, 0x64, 0x74, 0x51, 0x65, 0x6D, 0x38, 0x4F, 0x62, 0x30, 0x79, 0x32, 0x57, 0x49, 0x43, 0x38, 0x62, 0x47, 0x6F, 0x50, 0x57, 0x34, 0x33, 0x6E, 0x4F, 0x49, 0x76, 0x34, + 0x74, 0x4F, 0x69, 0x4A, 0x6F, 0x76, 0x47, 0x75, 0x46, 0x56, 0x44, 0x69, 0x4F, 0x45, 0x6A, 0x50, 0x71, 0x58, 0x53, 0x4A, 0x44, 0x6C, 0x71, 0x52, 0x36, 0x73, 0x41, 0x0A, 0x31, 0x4B, 0x47, 0x7A, 0x71, 0x53, 0x58, 0x2B, 0x44, 0x54, 0x2B, 0x6E, + 0x48, 0x62, 0x72, 0x54, 0x55, 0x63, 0x45, 0x4C, 0x70, 0x4E, 0x71, 0x73, 0x4F, 0x4F, 0x39, 0x56, 0x55, 0x43, 0x51, 0x46, 0x5A, 0x55, 0x61, 0x54, 0x4E, 0x45, 0x38, 0x74, 0x6A, 0x61, 0x33, 0x47, 0x31, 0x43, 0x45, 0x5A, 0x30, 0x6F, 0x37, 0x4B, + 0x42, 0x57, 0x46, 0x78, 0x42, 0x33, 0x4E, 0x48, 0x35, 0x59, 0x6F, 0x5A, 0x45, 0x72, 0x30, 0x45, 0x54, 0x63, 0x35, 0x4F, 0x6E, 0x4B, 0x56, 0x49, 0x0A, 0x72, 0x4C, 0x73, 0x6D, 0x39, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x34, 0x47, + 0x4F, 0x4D, 0x49, 0x47, 0x4C, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x51, 0x4C, 0x57, 0x4F, 0x57, 0x4C, 0x78, 0x6B, 0x77, 0x56, 0x4E, 0x36, 0x52, 0x41, 0x71, 0x54, 0x43, 0x70, 0x49, 0x62, 0x35, + 0x48, 0x4E, 0x6C, 0x70, 0x57, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x0A, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, + 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x42, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x38, 0x45, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x44, 0x36, 0x67, 0x50, 0x4B, 0x41, 0x36, 0x68, 0x6A, 0x68, 0x6F, 0x64, 0x48, + 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x57, 0x39, 0x6B, 0x0A, 0x62, 0x32, 0x4E, 0x68, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x39, 0x44, 0x54, 0x30, 0x31, 0x50, 0x52, 0x45, 0x39, 0x44, 0x5A, + 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4C, 0x6D, 0x4E, 0x79, 0x62, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, + 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x0A, 0x41, 0x51, 0x45, 0x41, 0x50, 0x70, 0x69, 0x65, 0x6D, 0x2F, 0x59, 0x62, 0x36, 0x64, 0x63, 0x35, 0x74, 0x33, 0x69, 0x75, 0x48, 0x58, 0x49, 0x59, + 0x53, 0x64, 0x4F, 0x48, 0x35, 0x45, 0x4F, 0x43, 0x36, 0x7A, 0x2F, 0x4A, 0x71, 0x76, 0x57, 0x6F, 0x74, 0x65, 0x39, 0x56, 0x66, 0x43, 0x46, 0x53, 0x5A, 0x66, 0x6E, 0x56, 0x44, 0x65, 0x46, 0x73, 0x39, 0x44, 0x36, 0x4D, 0x6B, 0x33, 0x4F, 0x52, + 0x4C, 0x67, 0x4C, 0x45, 0x54, 0x67, 0x64, 0x78, 0x62, 0x38, 0x43, 0x50, 0x0A, 0x4F, 0x47, 0x45, 0x49, 0x71, 0x42, 0x36, 0x42, 0x43, 0x73, 0x41, 0x76, 0x49, 0x43, 0x39, 0x42, 0x69, 0x35, 0x48, 0x63, 0x53, 0x45, 0x57, 0x38, 0x38, 0x63, 0x62, + 0x65, 0x75, 0x6E, 0x5A, 0x72, 0x4D, 0x38, 0x67, 0x41, 0x4C, 0x54, 0x46, 0x47, 0x54, 0x4F, 0x33, 0x6E, 0x6E, 0x63, 0x2B, 0x49, 0x6C, 0x50, 0x38, 0x7A, 0x77, 0x46, 0x62, 0x6F, 0x4A, 0x49, 0x59, 0x6D, 0x75, 0x4E, 0x67, 0x34, 0x4F, 0x4E, 0x38, + 0x71, 0x61, 0x39, 0x30, 0x53, 0x7A, 0x4D, 0x63, 0x2F, 0x0A, 0x52, 0x78, 0x64, 0x4D, 0x6F, 0x73, 0x49, 0x47, 0x6C, 0x67, 0x6E, 0x57, 0x32, 0x2F, 0x34, 0x2F, 0x50, 0x45, 0x5A, 0x42, 0x33, 0x31, 0x6A, 0x69, 0x56, 0x67, 0x38, 0x38, 0x4F, 0x38, + 0x45, 0x63, 0x6B, 0x7A, 0x58, 0x5A, 0x4F, 0x46, 0x4B, 0x73, 0x37, 0x73, 0x6A, 0x73, 0x4C, 0x6A, 0x42, 0x4F, 0x6C, 0x44, 0x57, 0x30, 0x4A, 0x42, 0x39, 0x4C, 0x65, 0x47, 0x6E, 0x61, 0x38, 0x67, 0x49, 0x34, 0x7A, 0x4A, 0x56, 0x53, 0x6B, 0x2F, + 0x42, 0x77, 0x4A, 0x56, 0x6D, 0x63, 0x0A, 0x49, 0x47, 0x66, 0x45, 0x37, 0x76, 0x6D, 0x4C, 0x56, 0x32, 0x48, 0x30, 0x6B, 0x6E, 0x5A, 0x39, 0x50, 0x34, 0x53, 0x4E, 0x56, 0x62, 0x66, 0x6F, 0x35, 0x61, 0x7A, 0x56, 0x38, 0x66, 0x55, 0x5A, 0x56, + 0x71, 0x5A, 0x61, 0x2B, 0x35, 0x41, 0x63, 0x72, 0x35, 0x50, 0x72, 0x35, 0x52, 0x7A, 0x55, 0x5A, 0x35, 0x64, 0x64, 0x42, 0x41, 0x36, 0x2B, 0x43, 0x34, 0x4F, 0x6D, 0x46, 0x34, 0x4F, 0x35, 0x4D, 0x42, 0x4B, 0x67, 0x78, 0x54, 0x4D, 0x56, 0x42, + 0x62, 0x6B, 0x4E, 0x0A, 0x2B, 0x38, 0x63, 0x46, 0x64, 0x75, 0x50, 0x59, 0x53, 0x6F, 0x33, 0x38, 0x4E, 0x42, 0x65, 0x6A, 0x78, 0x69, 0x45, 0x6F, 0x76, 0x6A, 0x42, 0x46, 0x4D, 0x52, 0x37, 0x48, 0x65, 0x4C, 0x35, 0x59, 0x59, 0x54, 0x69, 0x73, + 0x4F, 0x2B, 0x49, 0x42, 0x5A, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x4F, 0x4D, 0x4F, + 0x44, 0x4F, 0x20, 0x45, 0x43, 0x43, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x69, 0x54, 0x43, 0x43, 0x41, 0x67, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x48, 0x30, 0x65, 0x76, 0x71, 0x6D, + 0x49, 0x41, 0x63, 0x46, 0x42, 0x55, 0x54, 0x41, 0x47, 0x65, 0x6D, 0x32, 0x4F, 0x5A, 0x4B, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x43, 0x42, 0x68, 0x54, 0x45, 0x4C, 0x4D, 0x41, + 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x0A, 0x52, 0x30, 0x49, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x45, 0x6B, 0x64, 0x79, 0x5A, 0x57, 0x46, 0x30, 0x5A, 0x58, 0x49, 0x67, 0x54, + 0x57, 0x46, 0x75, 0x59, 0x32, 0x68, 0x6C, 0x63, 0x33, 0x52, 0x6C, 0x63, 0x6A, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x48, 0x55, 0x32, 0x46, 0x73, 0x5A, 0x6D, 0x39, 0x79, 0x5A, 0x44, 0x45, 0x61, 0x4D, + 0x42, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x68, 0x4D, 0x52, 0x51, 0x30, 0x39, 0x4E, 0x54, 0x30, 0x52, 0x50, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x4B, 0x7A, 0x41, 0x70, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x49, 0x6B, 0x4E, 0x50, 0x54, 0x55, 0x39, 0x45, 0x54, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, + 0x62, 0x69, 0x42, 0x42, 0x0A, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x67, 0x77, 0x4D, 0x7A, 0x41, 0x32, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, + 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x34, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x43, 0x42, 0x68, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x30, 0x49, + 0x78, 0x0A, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x45, 0x6B, 0x64, 0x79, 0x5A, 0x57, 0x46, 0x30, 0x5A, 0x58, 0x49, 0x67, 0x54, 0x57, 0x46, 0x75, 0x59, 0x32, 0x68, 0x6C, 0x63, 0x33, 0x52, 0x6C, 0x63, 0x6A, + 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x48, 0x55, 0x32, 0x46, 0x73, 0x5A, 0x6D, 0x39, 0x79, 0x5A, 0x44, 0x45, 0x61, 0x4D, 0x42, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x52, 0x0A, 0x51, + 0x30, 0x39, 0x4E, 0x54, 0x30, 0x52, 0x50, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x49, 0x6B, 0x4E, 0x50, 0x54, + 0x55, 0x39, 0x45, 0x54, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x0A, 0x62, 0x33, 0x4A, 0x70, + 0x64, 0x48, 0x6B, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x51, 0x44, 0x52, 0x33, 0x73, 0x76, + 0x64, 0x63, 0x6D, 0x43, 0x46, 0x59, 0x58, 0x37, 0x64, 0x65, 0x53, 0x52, 0x46, 0x74, 0x53, 0x72, 0x59, 0x70, 0x6E, 0x31, 0x50, 0x6C, 0x49, 0x4C, 0x42, 0x73, 0x35, 0x42, 0x41, 0x48, 0x2B, 0x58, 0x0A, 0x34, 0x51, 0x6F, 0x6B, 0x50, 0x42, 0x30, + 0x42, 0x42, 0x4F, 0x34, 0x39, 0x30, 0x6F, 0x30, 0x4A, 0x6C, 0x77, 0x7A, 0x67, 0x64, 0x65, 0x54, 0x36, 0x2B, 0x33, 0x65, 0x4B, 0x4B, 0x76, 0x55, 0x44, 0x59, 0x45, 0x73, 0x32, 0x69, 0x78, 0x59, 0x6A, 0x46, 0x71, 0x30, 0x4A, 0x63, 0x66, 0x52, + 0x4B, 0x39, 0x43, 0x68, 0x51, 0x74, 0x50, 0x36, 0x49, 0x48, 0x47, 0x34, 0x2F, 0x62, 0x43, 0x38, 0x76, 0x43, 0x56, 0x6C, 0x62, 0x70, 0x56, 0x73, 0x4C, 0x4D, 0x35, 0x6E, 0x69, 0x0A, 0x77, 0x7A, 0x32, 0x4A, 0x2B, 0x57, 0x6F, 0x73, 0x37, 0x37, + 0x4C, 0x54, 0x42, 0x75, 0x6D, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x31, 0x63, 0x61, 0x63, 0x5A, 0x53, 0x42, 0x6D, 0x38, 0x6E, 0x5A, 0x33, 0x71, 0x51, 0x55, + 0x66, 0x66, 0x6C, 0x4D, 0x52, 0x49, 0x64, 0x35, 0x6E, 0x54, 0x65, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x0A, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, + 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x41, 0x44, 0x42, 0x6C, 0x41, + 0x6A, 0x45, 0x41, 0x37, 0x77, 0x4E, 0x62, 0x65, 0x71, 0x79, 0x33, 0x65, 0x41, 0x70, 0x79, 0x74, 0x34, 0x6A, 0x66, 0x2F, 0x37, 0x56, 0x47, 0x0A, 0x46, 0x41, 0x6B, 0x4B, 0x2B, 0x71, 0x44, 0x6D, 0x66, 0x51, 0x6A, 0x47, 0x47, 0x6F, 0x65, 0x39, + 0x47, 0x4B, 0x68, 0x7A, 0x76, 0x53, 0x62, 0x4B, 0x59, 0x41, 0x79, 0x64, 0x7A, 0x70, 0x6D, 0x66, 0x7A, 0x31, 0x77, 0x50, 0x4D, 0x4F, 0x47, 0x2B, 0x46, 0x44, 0x48, 0x71, 0x41, 0x6A, 0x41, 0x55, 0x39, 0x4A, 0x4D, 0x38, 0x53, 0x61, 0x63, 0x7A, + 0x65, 0x70, 0x42, 0x47, 0x52, 0x37, 0x4E, 0x6A, 0x66, 0x52, 0x4F, 0x62, 0x54, 0x72, 0x64, 0x76, 0x47, 0x44, 0x65, 0x41, 0x0A, 0x55, 0x2F, 0x37, 0x64, 0x49, 0x4F, 0x41, 0x31, 0x6D, 0x6A, 0x62, 0x52, 0x78, 0x77, 0x47, 0x35, 0x35, 0x74, 0x7A, + 0x64, 0x38, 0x2F, 0x38, 0x64, 0x4C, 0x44, 0x6F, 0x57, 0x56, 0x39, 0x6D, 0x53, 0x4F, 0x64, 0x59, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x69, 0x67, 0x6E, 0x61, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x71, 0x44, 0x43, 0x43, 0x41, 0x70, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4A, 0x41, 0x50, 0x37, 0x63, 0x34, 0x77, 0x45, 0x50, 0x79, + 0x55, 0x6A, 0x2F, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x4D, 0x44, 0x51, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, + 0x6B, 0x5A, 0x53, 0x4D, 0x52, 0x49, 0x77, 0x0A, 0x45, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x6C, 0x45, 0x61, 0x47, 0x6C, 0x74, 0x65, 0x57, 0x39, 0x30, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x4D, 0x4D, 0x43, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x32, 0x35, 0x68, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x33, 0x4D, 0x44, 0x59, 0x79, 0x4F, 0x54, 0x45, 0x31, 0x4D, 0x54, 0x4D, 0x77, 0x4E, 0x56, 0x6F, 0x58, + 0x44, 0x54, 0x49, 0x33, 0x0A, 0x4D, 0x44, 0x59, 0x79, 0x4F, 0x54, 0x45, 0x31, 0x4D, 0x54, 0x4D, 0x77, 0x4E, 0x56, 0x6F, 0x77, 0x4E, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x6C, 0x49, + 0x78, 0x45, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x55, 0x52, 0x6F, 0x61, 0x57, 0x31, 0x35, 0x62, 0x33, 0x52, 0x70, 0x63, 0x7A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, + 0x49, 0x0A, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6E, 0x62, 0x6D, 0x45, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, + 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x44, 0x49, 0x61, 0x50, 0x48, 0x4A, 0x31, 0x74, 0x61, 0x7A, 0x4E, 0x48, 0x55, 0x6D, 0x67, 0x68, 0x37, 0x73, 0x74, 0x4C, 0x37, 0x71, 0x0A, 0x58, + 0x4F, 0x45, 0x6D, 0x37, 0x52, 0x46, 0x48, 0x59, 0x65, 0x47, 0x69, 0x66, 0x42, 0x5A, 0x34, 0x51, 0x43, 0x48, 0x6B, 0x59, 0x4A, 0x35, 0x61, 0x79, 0x47, 0x50, 0x68, 0x78, 0x4C, 0x47, 0x57, 0x6B, 0x76, 0x38, 0x59, 0x62, 0x57, 0x6B, 0x6A, 0x34, + 0x53, 0x74, 0x69, 0x39, 0x39, 0x33, 0x69, 0x4E, 0x69, 0x2B, 0x52, 0x42, 0x37, 0x6C, 0x49, 0x7A, 0x77, 0x37, 0x73, 0x65, 0x62, 0x59, 0x73, 0x35, 0x7A, 0x52, 0x4C, 0x63, 0x41, 0x67, 0x6C, 0x6F, 0x7A, 0x79, 0x48, 0x0A, 0x47, 0x78, 0x6E, 0x79, + 0x67, 0x51, 0x63, 0x50, 0x4F, 0x4A, 0x41, 0x5A, 0x30, 0x78, 0x48, 0x2B, 0x68, 0x72, 0x54, 0x79, 0x30, 0x56, 0x34, 0x65, 0x48, 0x70, 0x62, 0x4E, 0x67, 0x47, 0x7A, 0x4F, 0x4F, 0x7A, 0x47, 0x54, 0x74, 0x76, 0x4B, 0x67, 0x30, 0x4B, 0x6D, 0x56, + 0x45, 0x6E, 0x32, 0x6C, 0x6D, 0x73, 0x78, 0x72, 0x79, 0x49, 0x52, 0x57, 0x69, 0x6A, 0x4F, 0x70, 0x35, 0x79, 0x49, 0x56, 0x55, 0x78, 0x62, 0x77, 0x7A, 0x42, 0x66, 0x73, 0x56, 0x31, 0x2F, 0x70, 0x0A, 0x6F, 0x67, 0x71, 0x59, 0x43, 0x64, 0x37, + 0x6A, 0x58, 0x35, 0x78, 0x76, 0x33, 0x45, 0x6A, 0x6A, 0x68, 0x51, 0x73, 0x56, 0x57, 0x71, 0x61, 0x36, 0x6E, 0x36, 0x78, 0x49, 0x34, 0x77, 0x6D, 0x79, 0x39, 0x2F, 0x51, 0x79, 0x33, 0x6C, 0x34, 0x30, 0x76, 0x68, 0x78, 0x34, 0x58, 0x55, 0x4A, + 0x62, 0x7A, 0x67, 0x34, 0x69, 0x6A, 0x30, 0x32, 0x51, 0x31, 0x33, 0x30, 0x79, 0x47, 0x4C, 0x4D, 0x4C, 0x4C, 0x47, 0x71, 0x2F, 0x6A, 0x6A, 0x38, 0x55, 0x45, 0x59, 0x6B, 0x67, 0x0A, 0x44, 0x6E, 0x63, 0x55, 0x74, 0x54, 0x32, 0x55, 0x43, 0x49, + 0x66, 0x33, 0x4A, 0x52, 0x37, 0x56, 0x73, 0x6D, 0x41, 0x41, 0x37, 0x47, 0x38, 0x71, 0x4B, 0x43, 0x56, 0x75, 0x4B, 0x6A, 0x34, 0x59, 0x59, 0x78, 0x63, 0x6C, 0x50, 0x7A, 0x35, 0x45, 0x49, 0x42, 0x62, 0x32, 0x4A, 0x73, 0x67, 0x6C, 0x72, 0x67, + 0x56, 0x4B, 0x74, 0x4F, 0x64, 0x6A, 0x4C, 0x50, 0x4F, 0x4D, 0x46, 0x6C, 0x4E, 0x2B, 0x58, 0x50, 0x73, 0x52, 0x47, 0x67, 0x6A, 0x42, 0x52, 0x6D, 0x4B, 0x66, 0x0A, 0x49, 0x72, 0x6A, 0x78, 0x77, 0x6F, 0x31, 0x70, 0x33, 0x50, 0x6F, 0x36, 0x57, + 0x41, 0x62, 0x66, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x67, 0x62, 0x77, 0x77, 0x67, 0x62, 0x6B, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, + 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x47, 0x75, 0x33, 0x2B, 0x51, 0x54, 0x6D, 0x51, 0x0A, 0x74, 0x43, 0x52, 0x5A, 0x76, 0x67, 0x48, 0x79, 0x55, 0x74, 0x56, 0x46, 0x39, 0x6C, 0x6F, 0x35, + 0x33, 0x42, 0x45, 0x77, 0x5A, 0x41, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x46, 0x30, 0x77, 0x57, 0x34, 0x41, 0x55, 0x47, 0x75, 0x33, 0x2B, 0x51, 0x54, 0x6D, 0x51, 0x74, 0x43, 0x52, 0x5A, 0x76, 0x67, 0x48, 0x79, 0x55, 0x74, 0x56, 0x46, + 0x39, 0x6C, 0x6F, 0x35, 0x33, 0x42, 0x47, 0x68, 0x4F, 0x4B, 0x51, 0x32, 0x4D, 0x44, 0x51, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x5A, 0x53, 0x4D, 0x52, 0x49, 0x77, 0x45, 0x41, 0x59, + 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x6C, 0x45, 0x61, 0x47, 0x6C, 0x74, 0x65, 0x57, 0x39, 0x30, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x43, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, + 0x70, 0x5A, 0x32, 0x35, 0x68, 0x67, 0x67, 0x6B, 0x41, 0x2F, 0x74, 0x7A, 0x6A, 0x41, 0x51, 0x2F, 0x4A, 0x0A, 0x53, 0x50, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, + 0x45, 0x47, 0x4D, 0x42, 0x45, 0x47, 0x43, 0x57, 0x43, 0x47, 0x53, 0x41, 0x47, 0x47, 0x2B, 0x45, 0x49, 0x42, 0x41, 0x51, 0x51, 0x45, 0x41, 0x77, 0x49, 0x41, 0x42, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x45, 0x41, 0x0A, 0x68, 0x51, 0x4D, 0x65, 0x6B, 0x6E, 0x48, 0x32, 0x51, 0x71, 0x2F, 0x68, 0x6F, 0x32, 0x47, 0x65, 0x36, 0x2F, 0x50, 0x41, 0x44, 0x2F, 0x4B, 0x6C, 0x31, + 0x4E, 0x71, 0x56, 0x35, 0x74, 0x61, 0x2B, 0x61, 0x44, 0x59, 0x39, 0x66, 0x6D, 0x34, 0x66, 0x54, 0x49, 0x72, 0x76, 0x30, 0x51, 0x38, 0x68, 0x62, 0x56, 0x36, 0x6C, 0x55, 0x6D, 0x50, 0x4F, 0x45, 0x76, 0x6A, 0x76, 0x4B, 0x74, 0x70, 0x76, 0x36, + 0x7A, 0x66, 0x2B, 0x45, 0x77, 0x4C, 0x48, 0x79, 0x7A, 0x73, 0x2B, 0x0A, 0x49, 0x6D, 0x76, 0x61, 0x59, 0x53, 0x35, 0x2F, 0x31, 0x48, 0x49, 0x39, 0x33, 0x54, 0x44, 0x68, 0x48, 0x6B, 0x78, 0x41, 0x47, 0x59, 0x77, 0x50, 0x31, 0x35, 0x7A, 0x52, + 0x67, 0x7A, 0x42, 0x37, 0x6D, 0x46, 0x6E, 0x63, 0x66, 0x63, 0x61, 0x35, 0x44, 0x43, 0x6C, 0x4D, 0x6F, 0x54, 0x4F, 0x69, 0x36, 0x32, 0x63, 0x36, 0x5A, 0x59, 0x54, 0x54, 0x6C, 0x75, 0x4C, 0x74, 0x64, 0x6B, 0x56, 0x77, 0x6A, 0x37, 0x55, 0x72, + 0x33, 0x76, 0x6B, 0x6A, 0x31, 0x6B, 0x6C, 0x75, 0x0A, 0x50, 0x42, 0x53, 0x31, 0x78, 0x70, 0x38, 0x31, 0x48, 0x6C, 0x44, 0x51, 0x77, 0x59, 0x39, 0x71, 0x63, 0x45, 0x51, 0x43, 0x59, 0x73, 0x75, 0x75, 0x48, 0x57, 0x68, 0x42, 0x70, 0x36, 0x70, + 0x58, 0x36, 0x46, 0x4F, 0x71, 0x42, 0x39, 0x49, 0x47, 0x39, 0x74, 0x55, 0x55, 0x42, 0x67, 0x75, 0x52, 0x41, 0x33, 0x55, 0x73, 0x62, 0x48, 0x4B, 0x31, 0x59, 0x5A, 0x57, 0x61, 0x44, 0x59, 0x75, 0x35, 0x44, 0x65, 0x66, 0x31, 0x33, 0x31, 0x54, + 0x4E, 0x33, 0x75, 0x62, 0x59, 0x0A, 0x31, 0x67, 0x6B, 0x49, 0x6C, 0x32, 0x50, 0x6C, 0x77, 0x53, 0x36, 0x77, 0x74, 0x30, 0x51, 0x6D, 0x77, 0x43, 0x62, 0x41, 0x72, 0x31, 0x55, 0x77, 0x6E, 0x6A, 0x76, 0x56, 0x4E, 0x69, 0x6F, 0x5A, 0x42, 0x50, + 0x52, 0x63, 0x48, 0x76, 0x2F, 0x50, 0x4C, 0x4C, 0x66, 0x2F, 0x30, 0x50, 0x32, 0x48, 0x51, 0x42, 0x48, 0x56, 0x45, 0x53, 0x4F, 0x37, 0x53, 0x4D, 0x41, 0x68, 0x71, 0x61, 0x51, 0x6F, 0x4C, 0x66, 0x30, 0x56, 0x2B, 0x4C, 0x42, 0x4F, 0x4B, 0x2F, + 0x51, 0x77, 0x0A, 0x57, 0x79, 0x48, 0x38, 0x45, 0x5A, 0x45, 0x30, 0x76, 0x6B, 0x48, 0x76, 0x65, 0x35, 0x32, 0x58, 0x64, 0x66, 0x2B, 0x58, 0x6C, 0x63, 0x43, 0x57, 0x57, 0x43, 0x2F, 0x71, 0x75, 0x30, 0x62, 0x58, 0x75, 0x2B, 0x54, 0x5A, 0x4C, + 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x65, 0x50, 0x4B, 0x49, 0x20, 0x52, 0x6F, 0x6F, 0x74, + 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x73, 0x44, 0x43, 0x43, 0x41, 0x35, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x46, 0x63, 0x69, 0x39, 0x5A, 0x55, 0x64, 0x63, 0x72, 0x37, 0x69, 0x58, 0x41, + 0x46, 0x37, 0x6B, 0x42, 0x74, 0x4B, 0x38, 0x6E, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x44, 0x42, 0x65, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, + 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x55, 0x56, 0x7A, 0x45, 0x6A, 0x4D, 0x43, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x61, 0x51, 0x32, 0x68, 0x31, 0x62, 0x6D, 0x64, 0x6F, 0x64, 0x32, 0x45, 0x67, 0x56, 0x47, 0x56, 0x73, + 0x5A, 0x57, 0x4E, 0x76, 0x62, 0x53, 0x42, 0x44, 0x62, 0x79, 0x34, 0x73, 0x49, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x4B, 0x6A, 0x41, 0x6F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x49, 0x57, 0x56, 0x51, 0x53, 0x30, 0x6B, 0x67, + 0x0A, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x41, + 0x65, 0x46, 0x77, 0x30, 0x77, 0x4E, 0x44, 0x45, 0x79, 0x4D, 0x6A, 0x41, 0x77, 0x4D, 0x6A, 0x4D, 0x78, 0x4D, 0x6A, 0x64, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4E, 0x44, 0x45, 0x79, 0x4D, 0x6A, 0x41, 0x77, 0x4D, 0x6A, 0x4D, 0x78, 0x0A, 0x4D, 0x6A, + 0x64, 0x61, 0x4D, 0x46, 0x34, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x52, 0x58, 0x4D, 0x53, 0x4D, 0x77, 0x49, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x70, 0x44, 0x61, 0x48, + 0x56, 0x75, 0x5A, 0x32, 0x68, 0x33, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x78, 0x6C, 0x59, 0x32, 0x39, 0x74, 0x49, 0x45, 0x4E, 0x76, 0x4C, 0x69, 0x77, 0x67, 0x54, 0x48, 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x71, 0x0A, 0x4D, 0x43, 0x67, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x77, 0x77, 0x68, 0x5A, 0x56, 0x42, 0x4C, 0x53, 0x53, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, + 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x0A, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, + 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x34, 0x53, 0x55, 0x50, 0x37, 0x6F, 0x33, 0x62, 0x69, 0x44, 0x4E, 0x31, 0x5A, 0x38, 0x32, 0x74, 0x48, 0x33, 0x30, 0x36, 0x54, 0x6D, 0x32, 0x64, + 0x30, 0x79, 0x38, 0x55, 0x38, 0x32, 0x4E, 0x30, 0x79, 0x77, 0x45, 0x68, 0x61, 0x6A, 0x66, 0x71, 0x68, 0x46, 0x41, 0x48, 0x53, 0x79, 0x5A, 0x62, 0x43, 0x55, 0x4E, 0x73, 0x0A, 0x49, 0x5A, 0x35, 0x71, 0x79, 0x4E, 0x55, 0x44, 0x39, 0x57, 0x42, + 0x70, 0x6A, 0x38, 0x7A, 0x77, 0x49, 0x75, 0x51, 0x66, 0x35, 0x2F, 0x64, 0x71, 0x49, 0x6A, 0x47, 0x33, 0x4C, 0x42, 0x58, 0x79, 0x34, 0x50, 0x34, 0x41, 0x61, 0x6B, 0x50, 0x2F, 0x68, 0x32, 0x58, 0x47, 0x74, 0x52, 0x72, 0x42, 0x70, 0x30, 0x78, + 0x74, 0x49, 0x6E, 0x41, 0x68, 0x69, 0x6A, 0x48, 0x79, 0x6C, 0x33, 0x53, 0x4A, 0x43, 0x52, 0x49, 0x6D, 0x48, 0x4A, 0x37, 0x4B, 0x32, 0x52, 0x4B, 0x69, 0x0A, 0x6C, 0x54, 0x7A, 0x61, 0x36, 0x57, 0x65, 0x2F, 0x43, 0x4B, 0x42, 0x6B, 0x34, 0x39, + 0x5A, 0x43, 0x74, 0x30, 0x58, 0x76, 0x6C, 0x2F, 0x54, 0x32, 0x39, 0x64, 0x65, 0x31, 0x53, 0x68, 0x55, 0x43, 0x57, 0x48, 0x32, 0x59, 0x57, 0x45, 0x74, 0x67, 0x76, 0x4D, 0x33, 0x58, 0x44, 0x5A, 0x6F, 0x54, 0x4D, 0x31, 0x50, 0x52, 0x59, 0x66, + 0x6C, 0x36, 0x31, 0x64, 0x64, 0x34, 0x73, 0x35, 0x6F, 0x7A, 0x39, 0x77, 0x43, 0x47, 0x7A, 0x68, 0x31, 0x4E, 0x6C, 0x44, 0x69, 0x76, 0x0A, 0x71, 0x4F, 0x78, 0x34, 0x55, 0x58, 0x43, 0x4B, 0x58, 0x42, 0x43, 0x44, 0x55, 0x53, 0x48, 0x33, 0x45, + 0x54, 0x30, 0x30, 0x68, 0x6C, 0x37, 0x6C, 0x53, 0x4D, 0x32, 0x58, 0x67, 0x59, 0x49, 0x31, 0x54, 0x42, 0x6E, 0x73, 0x5A, 0x66, 0x5A, 0x72, 0x78, 0x51, 0x57, 0x68, 0x37, 0x6B, 0x63, 0x54, 0x31, 0x72, 0x4D, 0x68, 0x4A, 0x35, 0x51, 0x51, 0x43, + 0x74, 0x6B, 0x6B, 0x4F, 0x37, 0x71, 0x2B, 0x52, 0x42, 0x4E, 0x47, 0x4D, 0x44, 0x2B, 0x58, 0x50, 0x4E, 0x6A, 0x58, 0x0A, 0x31, 0x32, 0x72, 0x75, 0x4F, 0x7A, 0x6A, 0x6A, 0x4B, 0x39, 0x53, 0x58, 0x44, 0x72, 0x6B, 0x62, 0x35, 0x77, 0x64, 0x4A, + 0x66, 0x7A, 0x63, 0x71, 0x2B, 0x58, 0x64, 0x34, 0x7A, 0x31, 0x54, 0x74, 0x57, 0x30, 0x61, 0x64, 0x6F, 0x34, 0x41, 0x4F, 0x6B, 0x55, 0x50, 0x42, 0x31, 0x6C, 0x74, 0x66, 0x46, 0x4C, 0x71, 0x66, 0x70, 0x6F, 0x30, 0x6B, 0x52, 0x30, 0x42, 0x5A, + 0x76, 0x33, 0x49, 0x34, 0x73, 0x6A, 0x5A, 0x73, 0x4E, 0x2F, 0x2B, 0x5A, 0x30, 0x56, 0x30, 0x4F, 0x0A, 0x57, 0x51, 0x71, 0x72, 0x61, 0x66, 0x66, 0x41, 0x73, 0x67, 0x52, 0x46, 0x65, 0x6C, 0x51, 0x41, 0x72, 0x72, 0x35, 0x54, 0x39, 0x72, 0x58, + 0x6E, 0x34, 0x66, 0x67, 0x38, 0x6F, 0x7A, 0x48, 0x53, 0x71, 0x66, 0x34, 0x68, 0x55, 0x6D, 0x54, 0x46, 0x70, 0x6D, 0x66, 0x77, 0x64, 0x51, 0x63, 0x47, 0x6C, 0x42, 0x53, 0x42, 0x56, 0x63, 0x59, 0x6E, 0x35, 0x41, 0x47, 0x50, 0x46, 0x38, 0x46, + 0x71, 0x63, 0x64, 0x65, 0x2B, 0x53, 0x2F, 0x75, 0x55, 0x57, 0x48, 0x31, 0x2B, 0x0A, 0x45, 0x54, 0x4F, 0x78, 0x51, 0x76, 0x64, 0x69, 0x62, 0x42, 0x6A, 0x57, 0x7A, 0x77, 0x6C, 0x6F, 0x50, 0x6E, 0x39, 0x73, 0x39, 0x68, 0x36, 0x50, 0x59, 0x71, + 0x32, 0x6C, 0x59, 0x39, 0x73, 0x4A, 0x70, 0x78, 0x38, 0x69, 0x51, 0x6B, 0x45, 0x65, 0x62, 0x35, 0x6D, 0x4B, 0x50, 0x74, 0x66, 0x35, 0x50, 0x30, 0x42, 0x36, 0x65, 0x62, 0x43, 0x6C, 0x41, 0x5A, 0x4C, 0x53, 0x6E, 0x54, 0x30, 0x49, 0x46, 0x61, + 0x55, 0x51, 0x41, 0x53, 0x32, 0x7A, 0x4D, 0x6E, 0x61, 0x6F, 0x0A, 0x6C, 0x51, 0x32, 0x7A, 0x65, 0x70, 0x72, 0x37, 0x42, 0x78, 0x42, 0x34, 0x45, 0x57, 0x2F, 0x68, 0x6A, 0x38, 0x65, 0x36, 0x44, 0x79, 0x55, 0x61, 0x64, 0x43, 0x72, 0x6C, 0x48, + 0x4A, 0x68, 0x42, 0x6D, 0x64, 0x38, 0x68, 0x68, 0x2B, 0x69, 0x56, 0x42, 0x6D, 0x6F, 0x4B, 0x73, 0x32, 0x70, 0x48, 0x64, 0x6D, 0x58, 0x32, 0x4F, 0x73, 0x2B, 0x50, 0x59, 0x68, 0x63, 0x5A, 0x65, 0x77, 0x6F, 0x6F, 0x7A, 0x52, 0x72, 0x53, 0x67, + 0x78, 0x34, 0x68, 0x78, 0x79, 0x79, 0x2F, 0x0A, 0x76, 0x76, 0x39, 0x68, 0x61, 0x4C, 0x64, 0x6E, 0x47, 0x37, 0x74, 0x34, 0x54, 0x59, 0x33, 0x4F, 0x5A, 0x2B, 0x58, 0x6B, 0x77, 0x59, 0x36, 0x33, 0x49, 0x32, 0x62, 0x69, 0x6E, 0x5A, 0x42, 0x31, + 0x4E, 0x4A, 0x69, 0x70, 0x4E, 0x69, 0x75, 0x4B, 0x6D, 0x70, 0x53, 0x35, 0x6E, 0x65, 0x7A, 0x4D, 0x69, 0x72, 0x48, 0x34, 0x4A, 0x59, 0x6C, 0x63, 0x57, 0x72, 0x59, 0x76, 0x6A, 0x42, 0x39, 0x74, 0x65, 0x53, 0x53, 0x6E, 0x55, 0x6D, 0x6A, 0x44, + 0x68, 0x44, 0x58, 0x69, 0x0A, 0x5A, 0x6F, 0x31, 0x6A, 0x44, 0x69, 0x56, 0x4E, 0x31, 0x52, 0x6D, 0x79, 0x35, 0x6E, 0x6B, 0x33, 0x70, 0x79, 0x4B, 0x64, 0x56, 0x44, 0x45, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x71, 0x4D, 0x47, 0x67, + 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x42, 0x34, 0x4D, 0x39, 0x37, 0x5A, 0x6E, 0x38, 0x75, 0x47, 0x53, 0x4A, 0x67, 0x6C, 0x46, 0x77, 0x46, 0x55, 0x35, 0x4C, 0x6E, 0x63, 0x2F, 0x51, 0x6B, 0x71, + 0x69, 0x0A, 0x4D, 0x41, 0x77, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x4F, 0x51, 0x59, 0x45, 0x5A, 0x79, 0x6F, 0x48, 0x41, 0x41, 0x51, 0x78, 0x4D, 0x43, 0x38, 0x77, 0x4C, 0x51, + 0x49, 0x42, 0x41, 0x44, 0x41, 0x4A, 0x42, 0x67, 0x55, 0x72, 0x44, 0x67, 0x4D, 0x43, 0x47, 0x67, 0x55, 0x41, 0x4D, 0x41, 0x63, 0x47, 0x42, 0x57, 0x63, 0x71, 0x41, 0x77, 0x41, 0x41, 0x42, 0x42, 0x52, 0x46, 0x73, 0x4D, 0x4C, 0x48, 0x0A, 0x43, + 0x6C, 0x5A, 0x38, 0x37, 0x6C, 0x74, 0x34, 0x44, 0x4A, 0x58, 0x35, 0x47, 0x46, 0x50, 0x42, 0x70, 0x68, 0x7A, 0x59, 0x45, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, + 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x43, 0x62, 0x4F, 0x44, 0x55, 0x31, 0x6B, 0x42, 0x50, 0x70, 0x56, 0x4A, 0x75, 0x66, 0x47, 0x42, 0x75, 0x76, 0x6C, 0x32, 0x49, 0x43, 0x4F, 0x31, 0x4A, 0x32, 0x42, 0x30, 0x0A, 0x31, 0x47, 0x71, 0x5A, + 0x4E, 0x46, 0x35, 0x73, 0x41, 0x46, 0x50, 0x5A, 0x6E, 0x2F, 0x4B, 0x6D, 0x73, 0x53, 0x51, 0x48, 0x52, 0x47, 0x6F, 0x71, 0x78, 0x71, 0x57, 0x4F, 0x65, 0x42, 0x4C, 0x6F, 0x52, 0x39, 0x6C, 0x59, 0x47, 0x78, 0x4D, 0x71, 0x58, 0x6E, 0x6D, 0x62, + 0x6E, 0x77, 0x6F, 0x71, 0x5A, 0x36, 0x59, 0x6C, 0x50, 0x77, 0x5A, 0x70, 0x56, 0x6E, 0x50, 0x44, 0x69, 0x6D, 0x5A, 0x49, 0x2B, 0x79, 0x6D, 0x42, 0x56, 0x33, 0x51, 0x47, 0x79, 0x70, 0x7A, 0x71, 0x0A, 0x4B, 0x4F, 0x67, 0x34, 0x5A, 0x79, 0x59, + 0x72, 0x38, 0x64, 0x57, 0x31, 0x50, 0x32, 0x57, 0x54, 0x2B, 0x44, 0x5A, 0x64, 0x6A, 0x6F, 0x32, 0x4E, 0x51, 0x43, 0x43, 0x48, 0x47, 0x65, 0x72, 0x76, 0x4A, 0x38, 0x41, 0x39, 0x74, 0x44, 0x6B, 0x50, 0x4A, 0x58, 0x74, 0x6F, 0x55, 0x48, 0x52, + 0x56, 0x6E, 0x41, 0x78, 0x5A, 0x66, 0x56, 0x6F, 0x39, 0x51, 0x5A, 0x51, 0x6C, 0x55, 0x67, 0x6A, 0x67, 0x52, 0x79, 0x77, 0x56, 0x4D, 0x52, 0x6E, 0x56, 0x76, 0x77, 0x64, 0x56, 0x0A, 0x78, 0x72, 0x73, 0x53, 0x74, 0x5A, 0x66, 0x30, 0x58, 0x34, + 0x4F, 0x46, 0x75, 0x6E, 0x48, 0x42, 0x32, 0x57, 0x79, 0x42, 0x45, 0x58, 0x59, 0x4B, 0x43, 0x72, 0x43, 0x2F, 0x67, 0x70, 0x66, 0x33, 0x36, 0x6A, 0x33, 0x36, 0x2B, 0x75, 0x77, 0x74, 0x71, 0x53, 0x69, 0x55, 0x4F, 0x31, 0x62, 0x64, 0x30, 0x6C, + 0x45, 0x75, 0x72, 0x73, 0x43, 0x39, 0x43, 0x42, 0x57, 0x4D, 0x64, 0x31, 0x49, 0x30, 0x6C, 0x74, 0x61, 0x62, 0x72, 0x4E, 0x4D, 0x64, 0x6A, 0x6D, 0x45, 0x50, 0x0A, 0x4E, 0x58, 0x75, 0x62, 0x72, 0x6A, 0x6C, 0x70, 0x43, 0x32, 0x4A, 0x67, 0x51, + 0x43, 0x41, 0x32, 0x6A, 0x36, 0x2F, 0x37, 0x4E, 0x75, 0x34, 0x74, 0x43, 0x45, 0x6F, 0x64, 0x75, 0x4C, 0x2B, 0x62, 0x58, 0x50, 0x6A, 0x71, 0x70, 0x52, 0x75, 0x67, 0x63, 0x36, 0x62, 0x59, 0x2B, 0x47, 0x37, 0x67, 0x4D, 0x77, 0x52, 0x66, 0x61, + 0x4B, 0x6F, 0x6E, 0x68, 0x2B, 0x33, 0x5A, 0x77, 0x5A, 0x43, 0x63, 0x37, 0x62, 0x33, 0x6A, 0x61, 0x6A, 0x57, 0x76, 0x59, 0x39, 0x2B, 0x72, 0x0A, 0x47, 0x4E, 0x6D, 0x36, 0x35, 0x75, 0x6C, 0x4B, 0x36, 0x6C, 0x43, 0x4B, 0x44, 0x32, 0x47, 0x54, + 0x48, 0x75, 0x49, 0x74, 0x47, 0x65, 0x49, 0x77, 0x6C, 0x44, 0x57, 0x53, 0x58, 0x51, 0x36, 0x32, 0x42, 0x36, 0x38, 0x5A, 0x67, 0x49, 0x39, 0x48, 0x6B, 0x46, 0x46, 0x4C, 0x4C, 0x6B, 0x33, 0x64, 0x68, 0x65, 0x4C, 0x53, 0x43, 0x6C, 0x49, 0x4B, + 0x46, 0x35, 0x72, 0x38, 0x47, 0x72, 0x42, 0x51, 0x41, 0x75, 0x55, 0x42, 0x6F, 0x32, 0x4D, 0x33, 0x49, 0x55, 0x78, 0x45, 0x0A, 0x78, 0x4A, 0x74, 0x52, 0x6D, 0x52, 0x45, 0x4F, 0x63, 0x35, 0x77, 0x47, 0x6A, 0x31, 0x51, 0x75, 0x70, 0x79, 0x68, + 0x65, 0x52, 0x44, 0x6D, 0x48, 0x56, 0x69, 0x30, 0x33, 0x76, 0x59, 0x56, 0x45, 0x6C, 0x4F, 0x45, 0x4D, 0x53, 0x79, 0x79, 0x63, 0x77, 0x35, 0x4B, 0x46, 0x4E, 0x47, 0x48, 0x4C, 0x44, 0x37, 0x69, 0x62, 0x53, 0x6B, 0x4E, 0x53, 0x2F, 0x6A, 0x51, + 0x36, 0x66, 0x62, 0x6A, 0x70, 0x4B, 0x64, 0x78, 0x32, 0x71, 0x63, 0x67, 0x77, 0x2B, 0x42, 0x52, 0x78, 0x0A, 0x67, 0x4D, 0x59, 0x65, 0x4E, 0x6B, 0x68, 0x30, 0x49, 0x6B, 0x46, 0x63, 0x68, 0x34, 0x4C, 0x6F, 0x47, 0x48, 0x47, 0x4C, 0x51, 0x59, + 0x6C, 0x45, 0x35, 0x33, 0x35, 0x59, 0x57, 0x36, 0x69, 0x34, 0x6A, 0x52, 0x50, 0x70, 0x70, 0x32, 0x7A, 0x44, 0x52, 0x2B, 0x32, 0x7A, 0x47, 0x70, 0x31, 0x69, 0x72, 0x6F, 0x32, 0x43, 0x36, 0x70, 0x53, 0x65, 0x33, 0x56, 0x6B, 0x51, 0x77, 0x36, + 0x33, 0x64, 0x34, 0x6B, 0x33, 0x6A, 0x4D, 0x64, 0x58, 0x48, 0x37, 0x4F, 0x6A, 0x79, 0x0A, 0x73, 0x50, 0x36, 0x53, 0x48, 0x68, 0x59, 0x4B, 0x47, 0x76, 0x7A, 0x5A, 0x38, 0x2F, 0x67, 0x6E, 0x74, 0x73, 0x6D, 0x2B, 0x48, 0x62, 0x52, 0x73, 0x5A, + 0x4A, 0x42, 0x2F, 0x39, 0x4F, 0x54, 0x45, 0x57, 0x39, 0x63, 0x33, 0x72, 0x6B, 0x49, 0x4F, 0x33, 0x61, 0x51, 0x61, 0x62, 0x33, 0x79, 0x49, 0x56, 0x4D, 0x55, 0x57, 0x62, 0x75, 0x46, 0x36, 0x61, 0x43, 0x37, 0x34, 0x4F, 0x72, 0x38, 0x4E, 0x70, + 0x44, 0x79, 0x4A, 0x4F, 0x33, 0x69, 0x6E, 0x54, 0x6D, 0x4F, 0x44, 0x0A, 0x42, 0x43, 0x45, 0x49, 0x5A, 0x34, 0x33, 0x79, 0x67, 0x6B, 0x6E, 0x51, 0x57, 0x2F, 0x32, 0x78, 0x7A, 0x51, 0x2B, 0x44, 0x68, 0x4E, 0x51, 0x2B, 0x49, 0x49, 0x58, 0x33, + 0x53, 0x6A, 0x30, 0x72, 0x6E, 0x50, 0x30, 0x71, 0x43, 0x67, 0x6C, 0x4E, 0x36, 0x6F, 0x48, 0x34, 0x45, 0x5A, 0x77, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x63, 0x65, 0x72, 0x74, 0x53, 0x49, 0x47, 0x4E, 0x20, 0x52, 0x4F, 0x4F, 0x54, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x4F, 0x44, 0x43, 0x43, 0x41, 0x69, + 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x47, 0x49, 0x41, 0x59, 0x46, 0x46, 0x6E, 0x41, 0x43, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x4D, 0x44, + 0x73, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x4A, 0x50, 0x4D, 0x52, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x68, 0x6A, 0x5A, 0x58, 0x4A, 0x30, 0x55, + 0x30, 0x6C, 0x48, 0x54, 0x6A, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x51, 0x59, 0x32, 0x56, 0x79, 0x64, 0x46, 0x4E, 0x4A, 0x52, 0x30, 0x34, 0x67, 0x55, 0x6B, 0x39, 0x50, 0x56, 0x43, 0x42, 0x44, 0x51, + 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x77, 0x4E, 0x6A, 0x41, 0x33, 0x4D, 0x44, 0x51, 0x78, 0x4E, 0x7A, 0x49, 0x77, 0x4D, 0x44, 0x52, 0x61, 0x0A, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x54, 0x41, 0x33, 0x4D, 0x44, 0x51, 0x78, 0x4E, 0x7A, 0x49, 0x77, + 0x4D, 0x44, 0x52, 0x61, 0x4D, 0x44, 0x73, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x4A, 0x50, 0x4D, 0x52, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x68, 0x6A, + 0x5A, 0x58, 0x4A, 0x30, 0x55, 0x30, 0x6C, 0x48, 0x54, 0x6A, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x78, 0x4D, 0x51, 0x59, 0x32, 0x56, 0x79, 0x64, 0x46, 0x4E, 0x4A, 0x52, 0x30, 0x34, 0x67, 0x55, 0x6B, 0x39, + 0x50, 0x56, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, + 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4C, 0x63, 0x7A, 0x75, 0x58, 0x37, 0x49, 0x0A, 0x4A, 0x55, 0x71, 0x4F, 0x74, 0x64, 0x75, 0x30, 0x4B, 0x42, 0x75, 0x71, 0x56, 0x35, 0x44, 0x6F, 0x30, 0x53, 0x4C, 0x54, 0x5A, 0x4C, + 0x72, 0x54, 0x6B, 0x2B, 0x6A, 0x55, 0x72, 0x49, 0x5A, 0x68, 0x51, 0x47, 0x70, 0x67, 0x56, 0x32, 0x68, 0x55, 0x68, 0x45, 0x32, 0x38, 0x61, 0x6C, 0x51, 0x43, 0x42, 0x66, 0x2F, 0x66, 0x6D, 0x35, 0x6F, 0x71, 0x72, 0x6C, 0x30, 0x48, 0x6A, 0x30, + 0x72, 0x44, 0x4B, 0x48, 0x2F, 0x76, 0x2B, 0x79, 0x76, 0x36, 0x65, 0x66, 0x48, 0x48, 0x0A, 0x72, 0x66, 0x41, 0x51, 0x55, 0x79, 0x53, 0x51, 0x69, 0x32, 0x62, 0x4A, 0x71, 0x49, 0x69, 0x72, 0x72, 0x31, 0x71, 0x6A, 0x41, 0x4F, 0x6D, 0x2B, 0x75, + 0x6B, 0x62, 0x75, 0x57, 0x33, 0x4E, 0x37, 0x4C, 0x42, 0x65, 0x43, 0x67, 0x56, 0x35, 0x69, 0x4C, 0x4B, 0x45, 0x43, 0x5A, 0x62, 0x4F, 0x39, 0x78, 0x53, 0x73, 0x41, 0x66, 0x73, 0x54, 0x38, 0x41, 0x7A, 0x4E, 0x58, 0x44, 0x65, 0x33, 0x69, 0x2B, + 0x73, 0x35, 0x64, 0x52, 0x64, 0x59, 0x34, 0x7A, 0x54, 0x57, 0x32, 0x0A, 0x73, 0x73, 0x48, 0x51, 0x6E, 0x49, 0x46, 0x4B, 0x71, 0x75, 0x53, 0x79, 0x41, 0x56, 0x77, 0x64, 0x6A, 0x31, 0x2B, 0x5A, 0x78, 0x4C, 0x47, 0x74, 0x32, 0x34, 0x67, 0x68, + 0x36, 0x35, 0x41, 0x49, 0x67, 0x6F, 0x44, 0x7A, 0x4D, 0x4B, 0x4E, 0x44, 0x35, 0x70, 0x43, 0x43, 0x72, 0x6C, 0x55, 0x6F, 0x53, 0x65, 0x31, 0x62, 0x31, 0x36, 0x6B, 0x51, 0x4F, 0x41, 0x37, 0x2B, 0x6A, 0x30, 0x78, 0x62, 0x6D, 0x30, 0x62, 0x71, + 0x51, 0x66, 0x57, 0x77, 0x43, 0x48, 0x54, 0x44, 0x0A, 0x30, 0x49, 0x67, 0x7A, 0x74, 0x6E, 0x7A, 0x58, 0x64, 0x4E, 0x2F, 0x63, 0x68, 0x4E, 0x46, 0x44, 0x44, 0x6E, 0x55, 0x35, 0x6F, 0x53, 0x56, 0x41, 0x4B, 0x4F, 0x70, 0x34, 0x79, 0x77, 0x34, + 0x73, 0x4C, 0x6A, 0x6D, 0x64, 0x6A, 0x49, 0x74, 0x75, 0x46, 0x68, 0x77, 0x76, 0x4A, 0x6F, 0x49, 0x51, 0x34, 0x75, 0x4E, 0x6C, 0x6C, 0x41, 0x6F, 0x45, 0x77, 0x46, 0x37, 0x33, 0x58, 0x56, 0x76, 0x34, 0x45, 0x4F, 0x4C, 0x51, 0x75, 0x6E, 0x70, + 0x4C, 0x2B, 0x39, 0x34, 0x33, 0x0A, 0x41, 0x41, 0x41, 0x61, 0x57, 0x79, 0x6A, 0x6A, 0x30, 0x70, 0x78, 0x7A, 0x50, 0x6A, 0x4B, 0x48, 0x6D, 0x4B, 0x48, 0x4A, 0x55, 0x53, 0x2F, 0x58, 0x33, 0x71, 0x77, 0x7A, 0x73, 0x30, 0x38, 0x43, 0x41, 0x77, + 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, + 0x38, 0x42, 0x0A, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x63, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4F, 0x43, 0x4D, 0x6D, 0x39, 0x73, 0x6C, 0x53, 0x62, 0x50, 0x78, 0x66, + 0x49, 0x62, 0x57, 0x73, 0x6B, 0x4B, 0x48, 0x43, 0x39, 0x42, 0x72, 0x6F, 0x4E, 0x6E, 0x6B, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x0A, + 0x41, 0x51, 0x41, 0x2B, 0x30, 0x68, 0x79, 0x4A, 0x4C, 0x6A, 0x58, 0x38, 0x2B, 0x48, 0x58, 0x64, 0x35, 0x6E, 0x39, 0x6C, 0x69, 0x50, 0x52, 0x79, 0x54, 0x4D, 0x6B, 0x73, 0x31, 0x7A, 0x4A, 0x4F, 0x38, 0x39, 0x30, 0x5A, 0x65, 0x55, 0x65, 0x39, + 0x6A, 0x6A, 0x74, 0x62, 0x6B, 0x77, 0x39, 0x51, 0x53, 0x53, 0x51, 0x54, 0x61, 0x78, 0x51, 0x47, 0x63, 0x75, 0x38, 0x4A, 0x30, 0x36, 0x47, 0x68, 0x34, 0x30, 0x43, 0x45, 0x79, 0x65, 0x63, 0x59, 0x4D, 0x6E, 0x51, 0x38, 0x0A, 0x53, 0x47, 0x34, + 0x50, 0x6E, 0x30, 0x76, 0x55, 0x39, 0x78, 0x37, 0x54, 0x6B, 0x34, 0x5A, 0x6B, 0x56, 0x4A, 0x64, 0x6A, 0x63, 0x6C, 0x44, 0x56, 0x56, 0x63, 0x2F, 0x36, 0x49, 0x4A, 0x4D, 0x43, 0x6F, 0x70, 0x76, 0x44, 0x49, 0x35, 0x4E, 0x4F, 0x46, 0x6C, 0x56, + 0x32, 0x6F, 0x48, 0x42, 0x35, 0x62, 0x63, 0x30, 0x68, 0x48, 0x38, 0x38, 0x76, 0x4C, 0x62, 0x77, 0x5A, 0x34, 0x34, 0x67, 0x78, 0x2B, 0x46, 0x6B, 0x61, 0x67, 0x51, 0x6E, 0x49, 0x6C, 0x36, 0x5A, 0x30, 0x0A, 0x78, 0x32, 0x44, 0x45, 0x57, 0x38, + 0x78, 0x58, 0x6A, 0x72, 0x4A, 0x31, 0x2F, 0x52, 0x73, 0x43, 0x43, 0x64, 0x74, 0x5A, 0x62, 0x33, 0x4B, 0x54, 0x61, 0x66, 0x63, 0x78, 0x51, 0x64, 0x61, 0x49, 0x4F, 0x4C, 0x2B, 0x48, 0x73, 0x72, 0x30, 0x57, 0x65, 0x66, 0x6D, 0x71, 0x35, 0x4C, + 0x36, 0x49, 0x4A, 0x64, 0x31, 0x68, 0x4A, 0x79, 0x4D, 0x63, 0x74, 0x54, 0x45, 0x48, 0x42, 0x44, 0x61, 0x30, 0x47, 0x70, 0x43, 0x39, 0x6F, 0x48, 0x52, 0x78, 0x55, 0x49, 0x6C, 0x74, 0x0A, 0x76, 0x42, 0x54, 0x6A, 0x44, 0x34, 0x61, 0x75, 0x38, + 0x61, 0x73, 0x2B, 0x78, 0x36, 0x41, 0x4A, 0x7A, 0x4B, 0x4E, 0x49, 0x30, 0x65, 0x44, 0x62, 0x5A, 0x4F, 0x65, 0x53, 0x74, 0x63, 0x2B, 0x76, 0x63, 0x6B, 0x4E, 0x77, 0x69, 0x2F, 0x6E, 0x44, 0x68, 0x44, 0x77, 0x54, 0x71, 0x6E, 0x36, 0x53, 0x6D, + 0x31, 0x64, 0x54, 0x6B, 0x2F, 0x70, 0x77, 0x77, 0x70, 0x45, 0x4F, 0x4D, 0x66, 0x6D, 0x62, 0x5A, 0x31, 0x33, 0x70, 0x6C, 0x6A, 0x68, 0x65, 0x58, 0x37, 0x4E, 0x7A, 0x0A, 0x54, 0x6F, 0x67, 0x56, 0x5A, 0x39, 0x36, 0x65, 0x64, 0x68, 0x42, 0x69, + 0x49, 0x4C, 0x35, 0x56, 0x61, 0x5A, 0x56, 0x44, 0x41, 0x44, 0x6C, 0x4E, 0x39, 0x75, 0x36, 0x77, 0x57, 0x6B, 0x35, 0x4A, 0x52, 0x46, 0x52, 0x59, 0x58, 0x30, 0x4B, 0x44, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4E, 0x65, 0x74, 0x4C, 0x6F, 0x63, 0x6B, 0x20, 0x41, 0x72, 0x61, 0x6E, 0x79, 0x20, 0x28, 0x43, 0x6C, 0x61, 0x73, 0x73, 0x20, 0x47, 0x6F, 0x6C, + 0x64, 0x29, 0x20, 0x46, 0xC5, 0x91, 0x74, 0x61, 0x6E, 0xC3, 0xBA, 0x73, 0xC3, 0xAD, 0x74, 0x76, 0xC3, 0xA1, 0x6E, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x46, 0x54, 0x43, 0x43, 0x41, 0x76, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x47, 0x53, 0x55, 0x45, 0x73, 0x35, 0x41, 0x41, 0x51, 0x4D, 0x41, + 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x49, 0x47, 0x6E, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x49, 0x56, 0x54, + 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x77, 0x77, 0x49, 0x51, 0x6E, 0x56, 0x6B, 0x59, 0x58, 0x42, 0x6C, 0x63, 0x33, 0x51, 0x78, 0x46, 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, + 0x45, 0x35, 0x6C, 0x64, 0x45, 0x78, 0x76, 0x59, 0x32, 0x73, 0x67, 0x53, 0x32, 0x5A, 0x30, 0x4C, 0x6A, 0x45, 0x33, 0x4D, 0x44, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x77, 0x77, 0x75, 0x56, 0x47, 0x46, 0x75, 0x77, 0x37, 0x70, 0x7A, 0x77, + 0x36, 0x31, 0x30, 0x0A, 0x64, 0x73, 0x4F, 0x68, 0x62, 0x6E, 0x6C, 0x72, 0x61, 0x57, 0x46, 0x6B, 0x77, 0x37, 0x4E, 0x72, 0x49, 0x43, 0x68, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, + 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x4B, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x73, 0x54, 0x6D, 0x56, 0x30, 0x54, 0x47, 0x39, 0x6A, 0x61, 0x79, 0x42, 0x42, + 0x0A, 0x63, 0x6D, 0x46, 0x75, 0x65, 0x53, 0x41, 0x6F, 0x51, 0x32, 0x78, 0x68, 0x63, 0x33, 0x4D, 0x67, 0x52, 0x32, 0x39, 0x73, 0x5A, 0x43, 0x6B, 0x67, 0x52, 0x73, 0x57, 0x52, 0x64, 0x47, 0x46, 0x75, 0x77, 0x37, 0x70, 0x7A, 0x77, 0x36, 0x31, + 0x30, 0x64, 0x73, 0x4F, 0x68, 0x62, 0x6E, 0x6B, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x67, 0x78, 0x4D, 0x6A, 0x45, 0x78, 0x4D, 0x54, 0x55, 0x77, 0x4F, 0x44, 0x49, 0x78, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x67, 0x78, 0x0A, 0x4D, 0x6A, + 0x41, 0x32, 0x4D, 0x54, 0x55, 0x77, 0x4F, 0x44, 0x49, 0x78, 0x57, 0x6A, 0x43, 0x42, 0x70, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x46, 0x55, 0x78, 0x45, 0x54, 0x41, 0x50, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x43, 0x45, 0x4A, 0x31, 0x5A, 0x47, 0x46, 0x77, 0x5A, 0x58, 0x4E, 0x30, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x78, 0x4F, 0x0A, 0x5A, 0x58, 0x52, 0x4D, 0x62, + 0x32, 0x4E, 0x72, 0x49, 0x45, 0x74, 0x6D, 0x64, 0x43, 0x34, 0x78, 0x4E, 0x7A, 0x41, 0x31, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x4C, 0x6C, 0x52, 0x68, 0x62, 0x73, 0x4F, 0x36, 0x63, 0x38, 0x4F, 0x74, 0x64, 0x48, 0x62, 0x44, 0x6F, + 0x57, 0x35, 0x35, 0x61, 0x32, 0x6C, 0x68, 0x5A, 0x4D, 0x4F, 0x7A, 0x61, 0x79, 0x41, 0x6F, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x0A, 0x62, 0x69, 0x42, 0x54, 0x5A, 0x58, 0x4A, 0x32, + 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x79, 0x6B, 0x78, 0x4E, 0x54, 0x41, 0x7A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x4C, 0x45, 0x35, 0x6C, 0x64, 0x45, 0x78, 0x76, 0x59, 0x32, 0x73, 0x67, 0x51, 0x58, 0x4A, 0x68, 0x62, 0x6E, 0x6B, 0x67, + 0x4B, 0x45, 0x4E, 0x73, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x45, 0x64, 0x76, 0x62, 0x47, 0x51, 0x70, 0x49, 0x45, 0x62, 0x46, 0x6B, 0x58, 0x52, 0x68, 0x62, 0x73, 0x4F, 0x36, 0x0A, 0x63, 0x38, 0x4F, 0x74, 0x64, 0x48, 0x62, 0x44, 0x6F, 0x57, 0x35, + 0x35, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, + 0x43, 0x41, 0x51, 0x45, 0x41, 0x78, 0x43, 0x52, 0x65, 0x63, 0x37, 0x35, 0x4C, 0x62, 0x52, 0x54, 0x44, 0x6F, 0x66, 0x54, 0x6A, 0x6C, 0x35, 0x42, 0x75, 0x0A, 0x30, 0x6A, 0x42, 0x46, 0x48, 0x6A, 0x7A, 0x75, 0x5A, 0x39, 0x6C, 0x6B, 0x34, 0x42, + 0x71, 0x4B, 0x66, 0x38, 0x6F, 0x77, 0x79, 0x6F, 0x50, 0x6A, 0x49, 0x4D, 0x48, 0x6A, 0x39, 0x44, 0x72, 0x54, 0x6C, 0x46, 0x38, 0x61, 0x66, 0x46, 0x74, 0x74, 0x76, 0x7A, 0x42, 0x50, 0x68, 0x43, 0x66, 0x32, 0x6E, 0x78, 0x39, 0x4A, 0x76, 0x4D, + 0x61, 0x5A, 0x43, 0x70, 0x44, 0x79, 0x44, 0x2F, 0x56, 0x2F, 0x51, 0x34, 0x51, 0x33, 0x59, 0x31, 0x47, 0x4C, 0x65, 0x71, 0x56, 0x77, 0x0A, 0x2F, 0x48, 0x70, 0x59, 0x7A, 0x59, 0x36, 0x62, 0x37, 0x63, 0x4E, 0x47, 0x62, 0x49, 0x52, 0x77, 0x58, + 0x64, 0x72, 0x7A, 0x41, 0x5A, 0x41, 0x6A, 0x2F, 0x45, 0x34, 0x77, 0x71, 0x58, 0x37, 0x68, 0x4A, 0x32, 0x50, 0x6E, 0x37, 0x57, 0x51, 0x38, 0x6F, 0x4C, 0x6A, 0x4A, 0x4D, 0x32, 0x50, 0x2B, 0x46, 0x70, 0x44, 0x2F, 0x73, 0x4C, 0x6A, 0x39, 0x31, + 0x36, 0x6A, 0x41, 0x77, 0x4A, 0x52, 0x44, 0x43, 0x37, 0x62, 0x56, 0x57, 0x61, 0x61, 0x65, 0x56, 0x74, 0x41, 0x6B, 0x0A, 0x48, 0x33, 0x42, 0x35, 0x72, 0x39, 0x73, 0x35, 0x56, 0x41, 0x31, 0x6C, 0x64, 0x64, 0x6B, 0x56, 0x51, 0x5A, 0x51, 0x42, + 0x72, 0x31, 0x37, 0x73, 0x39, 0x6F, 0x33, 0x78, 0x2F, 0x36, 0x31, 0x6B, 0x2F, 0x69, 0x43, 0x61, 0x31, 0x31, 0x7A, 0x72, 0x2F, 0x71, 0x59, 0x66, 0x43, 0x47, 0x53, 0x6A, 0x69, 0x33, 0x5A, 0x56, 0x72, 0x52, 0x34, 0x37, 0x4B, 0x47, 0x41, 0x75, + 0x68, 0x79, 0x58, 0x6F, 0x71, 0x71, 0x38, 0x66, 0x78, 0x6D, 0x52, 0x47, 0x49, 0x4C, 0x64, 0x77, 0x0A, 0x66, 0x7A, 0x7A, 0x65, 0x53, 0x4E, 0x75, 0x57, 0x55, 0x37, 0x63, 0x35, 0x64, 0x2B, 0x51, 0x61, 0x34, 0x73, 0x63, 0x57, 0x68, 0x48, 0x61, + 0x58, 0x57, 0x79, 0x2B, 0x37, 0x47, 0x52, 0x57, 0x46, 0x2B, 0x47, 0x6D, 0x46, 0x39, 0x5A, 0x6D, 0x6E, 0x71, 0x66, 0x49, 0x30, 0x70, 0x36, 0x6D, 0x32, 0x70, 0x67, 0x50, 0x38, 0x62, 0x34, 0x59, 0x39, 0x56, 0x48, 0x78, 0x32, 0x42, 0x4A, 0x74, + 0x72, 0x2B, 0x55, 0x42, 0x64, 0x41, 0x44, 0x54, 0x48, 0x4C, 0x70, 0x6C, 0x31, 0x0A, 0x6E, 0x65, 0x57, 0x49, 0x41, 0x36, 0x70, 0x4E, 0x2B, 0x41, 0x50, 0x53, 0x51, 0x6E, 0x62, 0x41, 0x47, 0x77, 0x49, 0x44, 0x41, 0x4B, 0x69, 0x4C, 0x6F, 0x30, + 0x55, 0x77, 0x51, 0x7A, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x43, 0x44, 0x41, 0x47, 0x41, 0x51, 0x48, 0x2F, 0x41, 0x67, 0x45, 0x45, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, + 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x0A, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x7A, 0x50, 0x70, 0x6E, 0x6B, 0x2F, 0x43, 0x32, 0x75, 0x4E, 0x43, 0x6C, 0x77, + 0x42, 0x37, 0x7A, 0x55, 0x2F, 0x32, 0x4D, 0x55, 0x39, 0x2B, 0x44, 0x31, 0x35, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, + 0x4B, 0x74, 0x2F, 0x37, 0x68, 0x77, 0x57, 0x0A, 0x71, 0x5A, 0x77, 0x38, 0x55, 0x51, 0x43, 0x67, 0x77, 0x42, 0x45, 0x49, 0x42, 0x61, 0x65, 0x5A, 0x35, 0x6D, 0x38, 0x42, 0x69, 0x46, 0x52, 0x68, 0x62, 0x76, 0x47, 0x35, 0x47, 0x4B, 0x31, 0x4B, + 0x72, 0x66, 0x36, 0x42, 0x51, 0x43, 0x4F, 0x55, 0x4C, 0x2F, 0x74, 0x31, 0x66, 0x43, 0x38, 0x6F, 0x53, 0x32, 0x49, 0x6B, 0x67, 0x59, 0x49, 0x4C, 0x39, 0x57, 0x48, 0x78, 0x48, 0x47, 0x36, 0x34, 0x59, 0x54, 0x6A, 0x72, 0x67, 0x66, 0x70, 0x69, + 0x6F, 0x54, 0x74, 0x61, 0x0A, 0x59, 0x74, 0x4F, 0x55, 0x5A, 0x63, 0x54, 0x68, 0x35, 0x6D, 0x32, 0x43, 0x2B, 0x43, 0x38, 0x6C, 0x63, 0x4C, 0x49, 0x68, 0x4A, 0x73, 0x46, 0x79, 0x55, 0x52, 0x2B, 0x4D, 0x4C, 0x4D, 0x4F, 0x45, 0x6B, 0x4D, 0x4E, + 0x61, 0x6A, 0x37, 0x72, 0x50, 0x39, 0x4B, 0x64, 0x6C, 0x70, 0x65, 0x75, 0x59, 0x30, 0x66, 0x73, 0x46, 0x73, 0x6B, 0x5A, 0x31, 0x46, 0x53, 0x4E, 0x71, 0x62, 0x34, 0x56, 0x6A, 0x4D, 0x49, 0x44, 0x77, 0x31, 0x5A, 0x34, 0x66, 0x4B, 0x52, 0x7A, + 0x43, 0x0A, 0x62, 0x4C, 0x42, 0x51, 0x57, 0x56, 0x32, 0x51, 0x57, 0x7A, 0x75, 0x6F, 0x44, 0x54, 0x44, 0x50, 0x76, 0x33, 0x31, 0x2F, 0x7A, 0x76, 0x47, 0x64, 0x67, 0x37, 0x33, 0x4A, 0x52, 0x6D, 0x34, 0x67, 0x70, 0x76, 0x6C, 0x68, 0x55, 0x62, + 0x6F, 0x68, 0x4C, 0x33, 0x75, 0x2B, 0x70, 0x52, 0x56, 0x6A, 0x6F, 0x64, 0x53, 0x56, 0x68, 0x2F, 0x47, 0x65, 0x75, 0x66, 0x4F, 0x4A, 0x38, 0x7A, 0x32, 0x46, 0x75, 0x4C, 0x6A, 0x62, 0x76, 0x72, 0x57, 0x35, 0x4B, 0x66, 0x6E, 0x61, 0x0A, 0x4E, + 0x77, 0x55, 0x41, 0x53, 0x5A, 0x51, 0x44, 0x68, 0x45, 0x54, 0x6E, 0x76, 0x30, 0x4D, 0x78, 0x7A, 0x33, 0x57, 0x4C, 0x4A, 0x64, 0x48, 0x30, 0x70, 0x6D, 0x54, 0x31, 0x6B, 0x76, 0x61, 0x72, 0x42, 0x65, 0x73, 0x39, 0x36, 0x61, 0x55, 0x4C, 0x4E, + 0x6D, 0x4C, 0x61, 0x7A, 0x41, 0x5A, 0x66, 0x4E, 0x6F, 0x75, 0x32, 0x58, 0x6A, 0x47, 0x34, 0x4B, 0x76, 0x74, 0x65, 0x39, 0x6E, 0x48, 0x66, 0x52, 0x43, 0x61, 0x65, 0x78, 0x4F, 0x59, 0x4E, 0x6B, 0x62, 0x51, 0x75, 0x0A, 0x64, 0x5A, 0x57, 0x41, + 0x55, 0x57, 0x70, 0x4C, 0x4D, 0x4B, 0x61, 0x77, 0x59, 0x71, 0x47, 0x54, 0x38, 0x5A, 0x76, 0x59, 0x7A, 0x73, 0x52, 0x6A, 0x64, 0x54, 0x39, 0x5A, 0x52, 0x37, 0x45, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x65, 0x63, 0x20, 0x65, 0x2D, 0x53, 0x7A, 0x69, 0x67, 0x6E, 0x6F, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, + 0x41, 0x20, 0x32, 0x30, 0x30, 0x39, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x43, 0x6A, 0x43, 0x43, 0x41, 0x76, 0x4B, 0x67, 0x41, 0x77, + 0x49, 0x42, 0x41, 0x67, 0x49, 0x4A, 0x41, 0x4D, 0x4A, 0x2B, 0x51, 0x77, 0x52, 0x4F, 0x52, 0x7A, 0x38, 0x5A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x49, + 0x47, 0x43, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x49, 0x56, 0x54, 0x45, 0x52, 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x77, 0x77, 0x49, 0x51, 0x6E, 0x56, 0x6B, 0x59, + 0x58, 0x42, 0x6C, 0x63, 0x33, 0x51, 0x78, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, 0x55, 0x31, 0x70, 0x59, 0x33, 0x4A, 0x76, 0x63, 0x32, 0x56, 0x6A, 0x49, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x4A, + 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x6B, 0x31, 0x70, 0x59, 0x33, 0x4A, 0x76, 0x0A, 0x63, 0x32, 0x56, 0x6A, 0x49, 0x47, 0x55, 0x74, 0x55, 0x33, 0x70, 0x70, 0x5A, 0x32, 0x35, 0x76, 0x49, 0x46, 0x4A, 0x76, + 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x77, 0x4F, 0x54, 0x45, 0x66, 0x4D, 0x42, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x4A, 0x41, 0x52, 0x59, 0x51, 0x61, 0x57, 0x35, 0x6D, + 0x62, 0x30, 0x42, 0x6C, 0x4C, 0x58, 0x4E, 0x36, 0x61, 0x57, 0x64, 0x75, 0x62, 0x79, 0x35, 0x6F, 0x0A, 0x64, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x77, 0x4F, 0x54, 0x41, 0x32, 0x4D, 0x54, 0x59, 0x78, 0x4D, 0x54, 0x4D, 0x77, 0x4D, 0x54, 0x68, + 0x61, 0x46, 0x77, 0x30, 0x79, 0x4F, 0x54, 0x45, 0x79, 0x4D, 0x7A, 0x41, 0x78, 0x4D, 0x54, 0x4D, 0x77, 0x4D, 0x54, 0x68, 0x61, 0x4D, 0x49, 0x47, 0x43, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, + 0x49, 0x56, 0x54, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x77, 0x77, 0x49, 0x51, 0x6E, 0x56, 0x6B, 0x59, 0x58, 0x42, 0x6C, 0x63, 0x33, 0x51, 0x78, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, + 0x6F, 0x4D, 0x44, 0x55, 0x31, 0x70, 0x59, 0x33, 0x4A, 0x76, 0x63, 0x32, 0x56, 0x6A, 0x49, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x6B, 0x31, 0x70, 0x59, 0x33, + 0x4A, 0x76, 0x63, 0x32, 0x56, 0x6A, 0x49, 0x47, 0x55, 0x74, 0x0A, 0x55, 0x33, 0x70, 0x70, 0x5A, 0x32, 0x35, 0x76, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x77, 0x4F, 0x54, 0x45, 0x66, 0x4D, + 0x42, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x4A, 0x41, 0x52, 0x59, 0x51, 0x61, 0x57, 0x35, 0x6D, 0x62, 0x30, 0x42, 0x6C, 0x4C, 0x58, 0x4E, 0x36, 0x61, 0x57, 0x64, 0x75, 0x62, 0x79, 0x35, 0x6F, 0x64, + 0x54, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, + 0x67, 0x67, 0x45, 0x42, 0x41, 0x4F, 0x6E, 0x34, 0x6A, 0x2F, 0x4E, 0x6A, 0x72, 0x64, 0x71, 0x47, 0x32, 0x4B, 0x66, 0x67, 0x51, 0x76, 0x76, 0x50, 0x6B, 0x64, 0x36, 0x6D, 0x4A, 0x76, 0x69, 0x5A, 0x70, 0x57, 0x4E, 0x77, 0x72, 0x5A, 0x75, 0x75, + 0x79, 0x6A, 0x4E, 0x41, 0x0A, 0x66, 0x57, 0x32, 0x57, 0x62, 0x71, 0x45, 0x4F, 0x52, 0x4F, 0x37, 0x68, 0x45, 0x35, 0x32, 0x55, 0x51, 0x6C, 0x4B, 0x61, 0x76, 0x58, 0x57, 0x46, 0x64, 0x43, 0x79, 0x6F, 0x44, 0x68, 0x32, 0x54, 0x74, 0x68, 0x69, + 0x33, 0x6A, 0x43, 0x79, 0x6F, 0x7A, 0x2F, 0x74, 0x63, 0x63, 0x62, 0x6E, 0x61, 0x37, 0x50, 0x37, 0x6F, 0x66, 0x6F, 0x2F, 0x6B, 0x4C, 0x78, 0x32, 0x79, 0x71, 0x48, 0x57, 0x48, 0x32, 0x4C, 0x65, 0x68, 0x35, 0x54, 0x76, 0x50, 0x6D, 0x55, 0x70, + 0x47, 0x0A, 0x30, 0x49, 0x4D, 0x5A, 0x66, 0x63, 0x43, 0x68, 0x45, 0x68, 0x79, 0x56, 0x62, 0x55, 0x72, 0x30, 0x32, 0x4D, 0x65, 0x6C, 0x54, 0x54, 0x4D, 0x75, 0x68, 0x54, 0x6C, 0x41, 0x64, 0x58, 0x34, 0x55, 0x66, 0x49, 0x41, 0x53, 0x6D, 0x46, + 0x44, 0x48, 0x51, 0x57, 0x65, 0x34, 0x6F, 0x49, 0x42, 0x68, 0x56, 0x4B, 0x5A, 0x73, 0x54, 0x68, 0x2F, 0x67, 0x6E, 0x51, 0x34, 0x48, 0x36, 0x63, 0x6D, 0x36, 0x4D, 0x2B, 0x66, 0x2B, 0x77, 0x46, 0x55, 0x6F, 0x4C, 0x41, 0x4B, 0x41, 0x0A, 0x70, + 0x78, 0x6E, 0x31, 0x6E, 0x74, 0x78, 0x56, 0x55, 0x77, 0x4F, 0x58, 0x65, 0x77, 0x64, 0x49, 0x2F, 0x35, 0x6E, 0x37, 0x4E, 0x34, 0x6F, 0x6B, 0x78, 0x46, 0x6E, 0x4D, 0x55, 0x42, 0x42, 0x6A, 0x6A, 0x71, 0x71, 0x70, 0x47, 0x72, 0x43, 0x45, 0x47, + 0x6F, 0x62, 0x35, 0x58, 0x37, 0x75, 0x78, 0x55, 0x47, 0x36, 0x6B, 0x30, 0x51, 0x72, 0x4D, 0x31, 0x58, 0x46, 0x2B, 0x48, 0x36, 0x63, 0x62, 0x66, 0x50, 0x56, 0x54, 0x62, 0x69, 0x4A, 0x66, 0x79, 0x79, 0x76, 0x6D, 0x0A, 0x31, 0x48, 0x78, 0x64, + 0x72, 0x74, 0x62, 0x43, 0x78, 0x6B, 0x7A, 0x6C, 0x42, 0x51, 0x48, 0x5A, 0x37, 0x56, 0x66, 0x38, 0x77, 0x53, 0x4E, 0x35, 0x2F, 0x50, 0x72, 0x49, 0x4A, 0x49, 0x4F, 0x56, 0x38, 0x37, 0x56, 0x71, 0x55, 0x51, 0x48, 0x51, 0x64, 0x39, 0x62, 0x70, + 0x45, 0x71, 0x48, 0x35, 0x47, 0x6F, 0x50, 0x37, 0x67, 0x68, 0x75, 0x35, 0x73, 0x4A, 0x66, 0x30, 0x64, 0x67, 0x59, 0x7A, 0x51, 0x30, 0x6D, 0x67, 0x2F, 0x77, 0x75, 0x31, 0x2B, 0x72, 0x55, 0x43, 0x0A, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4F, + 0x42, 0x67, 0x44, 0x42, 0x2B, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, + 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x4C, 0x44, 0x38, 0x62, 0x66, 0x0A, 0x51, 0x6B, 0x50, 0x4D, 0x50, 0x63, 0x75, 0x31, 0x53, 0x43, + 0x4F, 0x68, 0x47, 0x6E, 0x71, 0x6D, 0x4B, 0x72, 0x73, 0x30, 0x61, 0x44, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x54, 0x4C, 0x44, 0x38, 0x62, 0x66, 0x51, 0x6B, 0x50, 0x4D, 0x50, 0x63, + 0x75, 0x31, 0x53, 0x43, 0x4F, 0x68, 0x47, 0x6E, 0x71, 0x6D, 0x4B, 0x72, 0x73, 0x30, 0x61, 0x44, 0x41, 0x62, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x45, 0x45, 0x0A, 0x46, 0x44, 0x41, 0x53, 0x67, 0x52, 0x42, 0x70, 0x62, 0x6D, 0x5A, 0x76, 0x51, + 0x47, 0x55, 0x74, 0x63, 0x33, 0x70, 0x70, 0x5A, 0x32, 0x35, 0x76, 0x4C, 0x6D, 0x68, 0x31, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, + 0x51, 0x44, 0x4A, 0x30, 0x51, 0x35, 0x65, 0x4C, 0x74, 0x58, 0x4D, 0x73, 0x33, 0x77, 0x2B, 0x79, 0x2F, 0x77, 0x39, 0x2F, 0x77, 0x30, 0x6F, 0x0A, 0x6C, 0x5A, 0x4D, 0x45, 0x79, 0x4C, 0x2F, 0x61, 0x7A, 0x58, 0x6D, 0x34, 0x51, 0x35, 0x44, 0x77, + 0x70, 0x4C, 0x37, 0x76, 0x38, 0x75, 0x38, 0x68, 0x6D, 0x4C, 0x7A, 0x55, 0x31, 0x46, 0x30, 0x47, 0x39, 0x75, 0x35, 0x43, 0x37, 0x44, 0x42, 0x73, 0x6F, 0x4B, 0x71, 0x70, 0x79, 0x76, 0x47, 0x76, 0x69, 0x76, 0x6F, 0x2F, 0x43, 0x33, 0x4E, 0x71, + 0x50, 0x75, 0x6F, 0x75, 0x51, 0x48, 0x34, 0x66, 0x72, 0x6C, 0x52, 0x68, 0x65, 0x65, 0x73, 0x75, 0x43, 0x44, 0x66, 0x58, 0x0A, 0x49, 0x2F, 0x4F, 0x4D, 0x6E, 0x37, 0x34, 0x64, 0x73, 0x65, 0x47, 0x6B, 0x64, 0x64, 0x75, 0x67, 0x34, 0x6C, 0x51, + 0x55, 0x73, 0x62, 0x6F, 0x63, 0x4B, 0x61, 0x51, 0x59, 0x39, 0x68, 0x4B, 0x36, 0x6F, 0x68, 0x51, 0x55, 0x34, 0x7A, 0x45, 0x31, 0x79, 0x45, 0x44, 0x2F, 0x74, 0x2B, 0x41, 0x46, 0x64, 0x6C, 0x66, 0x42, 0x48, 0x46, 0x6E, 0x79, 0x2B, 0x4C, 0x2F, + 0x6B, 0x37, 0x53, 0x56, 0x69, 0x58, 0x49, 0x54, 0x77, 0x66, 0x6E, 0x34, 0x66, 0x73, 0x37, 0x37, 0x35, 0x0A, 0x74, 0x79, 0x45, 0x52, 0x7A, 0x41, 0x4D, 0x42, 0x56, 0x6E, 0x43, 0x6E, 0x45, 0x4A, 0x49, 0x65, 0x47, 0x7A, 0x53, 0x42, 0x48, 0x71, + 0x32, 0x63, 0x47, 0x73, 0x4D, 0x45, 0x50, 0x4F, 0x30, 0x43, 0x59, 0x64, 0x59, 0x65, 0x42, 0x76, 0x4E, 0x66, 0x4F, 0x6F, 0x66, 0x79, 0x4B, 0x2F, 0x46, 0x46, 0x68, 0x2B, 0x55, 0x39, 0x72, 0x4E, 0x48, 0x48, 0x56, 0x34, 0x53, 0x39, 0x61, 0x36, + 0x37, 0x63, 0x32, 0x50, 0x6D, 0x32, 0x47, 0x32, 0x4A, 0x77, 0x43, 0x7A, 0x30, 0x32, 0x0A, 0x79, 0x55, 0x4C, 0x79, 0x4D, 0x74, 0x64, 0x36, 0x59, 0x65, 0x62, 0x53, 0x32, 0x7A, 0x33, 0x50, 0x79, 0x4B, 0x6E, 0x4A, 0x6D, 0x39, 0x7A, 0x62, 0x57, + 0x45, 0x54, 0x58, 0x62, 0x7A, 0x69, 0x76, 0x66, 0x33, 0x6A, 0x54, 0x6F, 0x36, 0x30, 0x61, 0x64, 0x62, 0x6F, 0x63, 0x77, 0x54, 0x5A, 0x38, 0x6A, 0x78, 0x35, 0x74, 0x48, 0x4D, 0x4E, 0x31, 0x52, 0x71, 0x34, 0x31, 0x42, 0x61, 0x62, 0x32, 0x58, + 0x44, 0x30, 0x68, 0x37, 0x6C, 0x62, 0x77, 0x79, 0x59, 0x49, 0x69, 0x0A, 0x4C, 0x58, 0x70, 0x55, 0x71, 0x33, 0x44, 0x44, 0x66, 0x53, 0x4A, 0x6C, 0x67, 0x6E, 0x43, 0x57, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x52, 0x33, 0x0A, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x58, 0x7A, 0x43, 0x43, 0x41, 0x6B, 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4C, 0x42, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, + 0x49, 0x56, 0x68, 0x54, 0x43, 0x4B, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x54, 0x44, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x43, 0x78, 0x4D, 0x58, 0x52, 0x32, 0x78, 0x76, 0x0A, 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x55, 0x6A, 0x4D, 0x78, 0x45, 0x7A, 0x41, + 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, 0x6B, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x43, 0x6B, 0x64, + 0x73, 0x62, 0x32, 0x4A, 0x68, 0x0A, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x6B, 0x77, 0x4D, 0x7A, 0x45, 0x34, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x6A, + 0x6B, 0x77, 0x4D, 0x7A, 0x45, 0x34, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x4D, 0x4D, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x64, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, + 0x78, 0x54, 0x0A, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4C, 0x53, 0x42, 0x53, 0x4D, 0x7A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4B, 0x52, + 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x6A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4B, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, 0x6C, 0x6E, 0x0A, + 0x62, 0x6A, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, + 0x67, 0x67, 0x45, 0x42, 0x41, 0x4D, 0x77, 0x6C, 0x64, 0x70, 0x42, 0x35, 0x42, 0x6E, 0x67, 0x69, 0x46, 0x76, 0x58, 0x41, 0x67, 0x37, 0x61, 0x45, 0x79, 0x69, 0x69, 0x65, 0x2F, 0x51, 0x56, 0x32, 0x45, 0x63, 0x57, 0x74, 0x0A, 0x69, 0x48, 0x4C, + 0x38, 0x52, 0x67, 0x4A, 0x44, 0x78, 0x37, 0x4B, 0x4B, 0x6E, 0x51, 0x52, 0x66, 0x4A, 0x4D, 0x73, 0x75, 0x53, 0x2B, 0x46, 0x67, 0x67, 0x6B, 0x62, 0x68, 0x55, 0x71, 0x73, 0x4D, 0x67, 0x55, 0x64, 0x77, 0x62, 0x4E, 0x31, 0x6B, 0x30, 0x65, 0x76, + 0x31, 0x4C, 0x4B, 0x4D, 0x50, 0x67, 0x6A, 0x30, 0x4D, 0x4B, 0x36, 0x36, 0x58, 0x31, 0x37, 0x59, 0x55, 0x68, 0x68, 0x42, 0x35, 0x75, 0x7A, 0x73, 0x54, 0x67, 0x48, 0x65, 0x4D, 0x43, 0x4F, 0x46, 0x4A, 0x0A, 0x30, 0x6D, 0x70, 0x69, 0x4C, 0x78, + 0x39, 0x65, 0x2B, 0x70, 0x5A, 0x6F, 0x33, 0x34, 0x6B, 0x6E, 0x6C, 0x54, 0x69, 0x66, 0x42, 0x74, 0x63, 0x2B, 0x79, 0x63, 0x73, 0x6D, 0x57, 0x51, 0x31, 0x7A, 0x33, 0x72, 0x44, 0x49, 0x36, 0x53, 0x59, 0x4F, 0x67, 0x78, 0x58, 0x47, 0x37, 0x31, + 0x75, 0x4C, 0x30, 0x67, 0x52, 0x67, 0x79, 0x6B, 0x6D, 0x6D, 0x4B, 0x50, 0x5A, 0x70, 0x4F, 0x2F, 0x62, 0x4C, 0x79, 0x43, 0x69, 0x52, 0x35, 0x5A, 0x32, 0x4B, 0x59, 0x56, 0x63, 0x33, 0x0A, 0x72, 0x48, 0x51, 0x55, 0x33, 0x48, 0x54, 0x67, 0x4F, + 0x75, 0x35, 0x79, 0x4C, 0x79, 0x36, 0x63, 0x2B, 0x39, 0x43, 0x37, 0x76, 0x2F, 0x55, 0x39, 0x41, 0x4F, 0x45, 0x47, 0x4D, 0x2B, 0x69, 0x43, 0x4B, 0x36, 0x35, 0x54, 0x70, 0x6A, 0x6F, 0x57, 0x63, 0x34, 0x7A, 0x64, 0x51, 0x51, 0x34, 0x67, 0x4F, + 0x73, 0x43, 0x30, 0x70, 0x36, 0x48, 0x70, 0x73, 0x6B, 0x2B, 0x51, 0x4C, 0x6A, 0x4A, 0x67, 0x36, 0x56, 0x66, 0x4C, 0x75, 0x51, 0x53, 0x53, 0x61, 0x47, 0x6A, 0x6C, 0x0A, 0x4F, 0x43, 0x5A, 0x67, 0x64, 0x62, 0x4B, 0x66, 0x64, 0x2F, 0x2B, 0x52, + 0x46, 0x4F, 0x2B, 0x75, 0x49, 0x45, 0x6E, 0x38, 0x72, 0x55, 0x41, 0x56, 0x53, 0x4E, 0x45, 0x43, 0x4D, 0x57, 0x45, 0x5A, 0x58, 0x72, 0x69, 0x58, 0x37, 0x36, 0x31, 0x33, 0x74, 0x32, 0x53, 0x61, 0x65, 0x72, 0x39, 0x66, 0x77, 0x52, 0x50, 0x76, + 0x6D, 0x32, 0x4C, 0x37, 0x44, 0x57, 0x7A, 0x67, 0x56, 0x47, 0x6B, 0x57, 0x71, 0x51, 0x50, 0x61, 0x62, 0x75, 0x6D, 0x44, 0x6B, 0x33, 0x46, 0x32, 0x0A, 0x78, 0x6D, 0x6D, 0x46, 0x67, 0x68, 0x63, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, + 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, + 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x0A, 0x46, 0x49, 0x2F, 0x77, 0x53, 0x33, 0x2B, 0x6F, 0x4C, 0x6B, 0x55, 0x6B, 0x72, 0x6B, 0x31, 0x51, 0x2B, 0x6D, + 0x4F, 0x61, 0x69, 0x39, 0x37, 0x69, 0x33, 0x52, 0x75, 0x38, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x42, 0x4C, 0x51, 0x4E, + 0x76, 0x41, 0x55, 0x4B, 0x72, 0x2B, 0x79, 0x41, 0x7A, 0x76, 0x39, 0x35, 0x5A, 0x55, 0x52, 0x55, 0x6D, 0x37, 0x0A, 0x6C, 0x67, 0x41, 0x4A, 0x51, 0x61, 0x79, 0x7A, 0x45, 0x34, 0x61, 0x47, 0x4B, 0x41, 0x63, 0x7A, 0x79, 0x6D, 0x76, 0x6D, 0x64, + 0x4C, 0x6D, 0x36, 0x41, 0x43, 0x32, 0x75, 0x70, 0x41, 0x72, 0x54, 0x39, 0x66, 0x48, 0x78, 0x44, 0x34, 0x71, 0x2F, 0x63, 0x32, 0x64, 0x4B, 0x67, 0x38, 0x64, 0x45, 0x65, 0x33, 0x6A, 0x67, 0x72, 0x32, 0x35, 0x73, 0x62, 0x77, 0x4D, 0x70, 0x6A, + 0x6A, 0x4D, 0x35, 0x52, 0x63, 0x4F, 0x4F, 0x35, 0x4C, 0x6C, 0x58, 0x62, 0x4B, 0x72, 0x38, 0x0A, 0x45, 0x70, 0x62, 0x73, 0x55, 0x38, 0x59, 0x74, 0x35, 0x43, 0x52, 0x73, 0x75, 0x5A, 0x52, 0x6A, 0x2B, 0x39, 0x78, 0x54, 0x61, 0x47, 0x64, 0x57, + 0x50, 0x6F, 0x4F, 0x34, 0x7A, 0x7A, 0x55, 0x68, 0x77, 0x38, 0x6C, 0x6F, 0x2F, 0x73, 0x37, 0x61, 0x77, 0x6C, 0x4F, 0x71, 0x7A, 0x4A, 0x43, 0x4B, 0x36, 0x66, 0x42, 0x64, 0x52, 0x6F, 0x79, 0x56, 0x33, 0x58, 0x70, 0x59, 0x4B, 0x42, 0x6F, 0x76, + 0x48, 0x64, 0x37, 0x4E, 0x41, 0x44, 0x64, 0x42, 0x6A, 0x2B, 0x31, 0x45, 0x0A, 0x62, 0x64, 0x64, 0x54, 0x4B, 0x4A, 0x64, 0x2B, 0x38, 0x32, 0x63, 0x45, 0x48, 0x68, 0x58, 0x58, 0x69, 0x70, 0x61, 0x30, 0x30, 0x39, 0x35, 0x4D, 0x4A, 0x36, 0x52, + 0x4D, 0x47, 0x33, 0x4E, 0x7A, 0x64, 0x76, 0x51, 0x58, 0x6D, 0x63, 0x49, 0x66, 0x65, 0x67, 0x37, 0x6A, 0x4C, 0x51, 0x69, 0x74, 0x43, 0x68, 0x77, 0x73, 0x2F, 0x7A, 0x79, 0x72, 0x56, 0x51, 0x34, 0x50, 0x6B, 0x58, 0x34, 0x32, 0x36, 0x38, 0x4E, + 0x58, 0x53, 0x62, 0x37, 0x68, 0x4C, 0x69, 0x31, 0x38, 0x0A, 0x59, 0x49, 0x76, 0x44, 0x51, 0x56, 0x45, 0x54, 0x49, 0x35, 0x33, 0x4F, 0x39, 0x7A, 0x4A, 0x72, 0x6C, 0x41, 0x47, 0x6F, 0x6D, 0x65, 0x63, 0x73, 0x4D, 0x78, 0x38, 0x36, 0x4F, 0x79, + 0x58, 0x53, 0x68, 0x6B, 0x44, 0x4F, 0x4F, 0x79, 0x79, 0x47, 0x65, 0x4D, 0x6C, 0x68, 0x4C, 0x78, 0x53, 0x36, 0x37, 0x74, 0x74, 0x56, 0x62, 0x39, 0x2B, 0x45, 0x37, 0x67, 0x55, 0x4A, 0x54, 0x62, 0x30, 0x6F, 0x32, 0x48, 0x4C, 0x4F, 0x30, 0x32, + 0x4A, 0x51, 0x5A, 0x52, 0x37, 0x72, 0x0A, 0x6B, 0x70, 0x65, 0x44, 0x4D, 0x64, 0x6D, 0x7A, 0x74, 0x63, 0x70, 0x48, 0x57, 0x44, 0x39, 0x66, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x49, 0x7A, 0x65, 0x6E, 0x70, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x38, 0x54, 0x43, 0x43, 0x41, 0x39, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x51, 0x41, 0x4C, 0x43, 0x33, 0x57, 0x68, 0x5A, 0x49, 0x58, 0x37, 0x2F, 0x68, 0x79, 0x2F, 0x57, 0x4C, 0x31, 0x78, 0x6E, 0x6D, 0x66, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, + 0x46, 0x41, 0x44, 0x41, 0x34, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x46, 0x55, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x4C, 0x53, 0x56, + 0x70, 0x46, 0x54, 0x6C, 0x42, 0x46, 0x49, 0x46, 0x4D, 0x75, 0x51, 0x53, 0x34, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x43, 0x6B, 0x6C, 0x36, 0x5A, 0x57, 0x35, 0x77, 0x5A, 0x53, 0x35, 0x6A, 0x62, 0x32, + 0x30, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x63, 0x78, 0x4D, 0x6A, 0x45, 0x7A, 0x0A, 0x4D, 0x54, 0x4D, 0x77, 0x4F, 0x44, 0x49, 0x34, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x63, 0x78, 0x4D, 0x6A, 0x45, 0x7A, 0x4D, 0x44, 0x67, 0x79, 0x4E, + 0x7A, 0x49, 0x31, 0x57, 0x6A, 0x41, 0x34, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x46, 0x55, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x4C, 0x53, + 0x56, 0x70, 0x46, 0x54, 0x6C, 0x42, 0x46, 0x49, 0x46, 0x4D, 0x75, 0x0A, 0x51, 0x53, 0x34, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x43, 0x6B, 0x6C, 0x36, 0x5A, 0x57, 0x35, 0x77, 0x5A, 0x53, 0x35, 0x6A, + 0x62, 0x32, 0x30, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, + 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x4A, 0x0A, 0x30, 0x33, 0x72, 0x4B, 0x44, 0x78, 0x36, 0x73, 0x70, 0x34, 0x62, 0x6F, 0x46, 0x6D, 0x56, 0x71, 0x73, 0x63, 0x49, 0x62, 0x52, 0x54, 0x4A, 0x78, 0x6C, 0x64, 0x6E, 0x2B, 0x45, 0x46, 0x76, + 0x4D, 0x72, 0x2B, 0x65, 0x6C, 0x65, 0x51, 0x47, 0x50, 0x69, 0x63, 0x50, 0x4B, 0x38, 0x6C, 0x56, 0x78, 0x39, 0x33, 0x65, 0x2B, 0x64, 0x35, 0x54, 0x7A, 0x63, 0x71, 0x51, 0x73, 0x52, 0x4E, 0x69, 0x65, 0x6B, 0x70, 0x73, 0x55, 0x4F, 0x71, 0x48, + 0x6E, 0x4A, 0x4A, 0x41, 0x4B, 0x0A, 0x43, 0x6C, 0x61, 0x4F, 0x78, 0x64, 0x67, 0x6D, 0x6C, 0x4F, 0x48, 0x5A, 0x53, 0x4F, 0x45, 0x74, 0x50, 0x74, 0x6F, 0x4B, 0x63, 0x74, 0x32, 0x6A, 0x6D, 0x52, 0x58, 0x61, 0x67, 0x61, 0x4B, 0x48, 0x39, 0x48, + 0x74, 0x75, 0x4A, 0x6E, 0x65, 0x4A, 0x57, 0x4B, 0x33, 0x57, 0x36, 0x77, 0x79, 0x79, 0x51, 0x58, 0x70, 0x7A, 0x62, 0x6D, 0x33, 0x62, 0x65, 0x6E, 0x68, 0x42, 0x36, 0x51, 0x69, 0x49, 0x45, 0x6E, 0x36, 0x48, 0x4C, 0x6D, 0x59, 0x52, 0x59, 0x32, + 0x78, 0x55, 0x0A, 0x2B, 0x7A, 0x79, 0x64, 0x63, 0x73, 0x43, 0x38, 0x4C, 0x76, 0x2F, 0x43, 0x74, 0x39, 0x30, 0x4E, 0x64, 0x75, 0x4D, 0x36, 0x31, 0x2F, 0x65, 0x30, 0x61, 0x4C, 0x36, 0x69, 0x39, 0x65, 0x4F, 0x42, 0x62, 0x73, 0x46, 0x47, 0x62, + 0x31, 0x32, 0x4E, 0x34, 0x45, 0x33, 0x47, 0x56, 0x46, 0x57, 0x4A, 0x47, 0x6A, 0x4D, 0x78, 0x43, 0x72, 0x46, 0x58, 0x75, 0x61, 0x4F, 0x4B, 0x6D, 0x4D, 0x50, 0x73, 0x4F, 0x7A, 0x54, 0x46, 0x6C, 0x55, 0x46, 0x70, 0x66, 0x6E, 0x58, 0x43, 0x0A, + 0x50, 0x43, 0x44, 0x46, 0x59, 0x62, 0x70, 0x52, 0x52, 0x36, 0x41, 0x67, 0x6B, 0x4A, 0x4F, 0x68, 0x6B, 0x45, 0x76, 0x7A, 0x54, 0x6E, 0x79, 0x46, 0x52, 0x56, 0x53, 0x61, 0x30, 0x51, 0x55, 0x6D, 0x51, 0x62, 0x43, 0x31, 0x54, 0x52, 0x30, 0x7A, + 0x76, 0x73, 0x51, 0x44, 0x79, 0x43, 0x56, 0x38, 0x77, 0x58, 0x44, 0x62, 0x4F, 0x2F, 0x51, 0x4A, 0x4C, 0x56, 0x51, 0x6E, 0x53, 0x4B, 0x77, 0x76, 0x34, 0x63, 0x53, 0x73, 0x50, 0x73, 0x6A, 0x4C, 0x6B, 0x6B, 0x78, 0x54, 0x0A, 0x4F, 0x54, 0x63, + 0x6A, 0x37, 0x4E, 0x4D, 0x42, 0x2B, 0x65, 0x41, 0x4A, 0x52, 0x45, 0x31, 0x4E, 0x5A, 0x4D, 0x44, 0x68, 0x44, 0x56, 0x71, 0x48, 0x49, 0x72, 0x79, 0x74, 0x47, 0x36, 0x50, 0x2B, 0x4A, 0x72, 0x55, 0x56, 0x38, 0x36, 0x66, 0x38, 0x68, 0x42, 0x6E, + 0x70, 0x37, 0x4B, 0x47, 0x49, 0x74, 0x45, 0x52, 0x70, 0x68, 0x49, 0x50, 0x7A, 0x69, 0x64, 0x46, 0x30, 0x42, 0x71, 0x6E, 0x4D, 0x43, 0x39, 0x62, 0x43, 0x33, 0x69, 0x65, 0x46, 0x55, 0x43, 0x62, 0x4B, 0x0A, 0x46, 0x37, 0x6A, 0x4A, 0x65, 0x6F, + 0x64, 0x57, 0x4C, 0x42, 0x6F, 0x42, 0x48, 0x6D, 0x79, 0x2B, 0x45, 0x36, 0x30, 0x51, 0x72, 0x4C, 0x55, 0x6B, 0x39, 0x54, 0x69, 0x52, 0x6F, 0x64, 0x5A, 0x4C, 0x32, 0x76, 0x47, 0x37, 0x30, 0x74, 0x35, 0x48, 0x74, 0x66, 0x47, 0x38, 0x67, 0x66, + 0x5A, 0x5A, 0x61, 0x38, 0x38, 0x5A, 0x55, 0x2B, 0x6D, 0x4E, 0x46, 0x63, 0x74, 0x4B, 0x79, 0x36, 0x6C, 0x76, 0x52, 0x4F, 0x55, 0x62, 0x51, 0x63, 0x2F, 0x68, 0x68, 0x71, 0x66, 0x4B, 0x0A, 0x30, 0x47, 0x71, 0x66, 0x76, 0x45, 0x79, 0x4E, 0x42, + 0x6A, 0x4E, 0x61, 0x6F, 0x6F, 0x58, 0x6C, 0x6B, 0x44, 0x57, 0x67, 0x59, 0x6C, 0x77, 0x57, 0x54, 0x76, 0x44, 0x6A, 0x6F, 0x76, 0x6F, 0x44, 0x47, 0x72, 0x51, 0x73, 0x63, 0x62, 0x4E, 0x59, 0x4C, 0x4E, 0x35, 0x37, 0x43, 0x39, 0x73, 0x61, 0x44, + 0x2B, 0x76, 0x65, 0x49, 0x52, 0x38, 0x47, 0x64, 0x77, 0x59, 0x44, 0x73, 0x4D, 0x6E, 0x76, 0x6D, 0x66, 0x7A, 0x41, 0x75, 0x55, 0x38, 0x4C, 0x68, 0x69, 0x6A, 0x2B, 0x0A, 0x30, 0x72, 0x6E, 0x71, 0x34, 0x39, 0x71, 0x6C, 0x77, 0x30, 0x64, 0x70, + 0x45, 0x75, 0x44, 0x62, 0x38, 0x50, 0x59, 0x5A, 0x69, 0x2B, 0x31, 0x37, 0x63, 0x4E, 0x63, 0x43, 0x31, 0x75, 0x32, 0x48, 0x47, 0x43, 0x67, 0x73, 0x42, 0x43, 0x52, 0x4D, 0x64, 0x2B, 0x52, 0x49, 0x69, 0x68, 0x72, 0x47, 0x4F, 0x35, 0x72, 0x55, + 0x44, 0x38, 0x72, 0x36, 0x64, 0x64, 0x49, 0x42, 0x51, 0x46, 0x71, 0x4E, 0x65, 0x62, 0x2B, 0x4C, 0x7A, 0x30, 0x76, 0x50, 0x71, 0x68, 0x62, 0x42, 0x0A, 0x6C, 0x65, 0x53, 0x74, 0x54, 0x49, 0x6F, 0x2B, 0x46, 0x35, 0x48, 0x55, 0x73, 0x57, 0x4C, + 0x6C, 0x67, 0x75, 0x57, 0x41, 0x42, 0x4B, 0x51, 0x44, 0x66, 0x6F, 0x32, 0x2F, 0x32, 0x6E, 0x2B, 0x69, 0x44, 0x35, 0x64, 0x50, 0x44, 0x4E, 0x4D, 0x4E, 0x2B, 0x39, 0x66, 0x52, 0x35, 0x58, 0x4A, 0x2B, 0x48, 0x4D, 0x68, 0x33, 0x2F, 0x31, 0x75, + 0x61, 0x44, 0x37, 0x65, 0x75, 0x42, 0x55, 0x62, 0x6C, 0x38, 0x61, 0x67, 0x57, 0x37, 0x45, 0x65, 0x6B, 0x46, 0x77, 0x49, 0x44, 0x0A, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x34, 0x48, 0x32, 0x4D, 0x49, 0x48, 0x7A, 0x4D, 0x49, 0x47, 0x77, 0x42, 0x67, + 0x4E, 0x56, 0x48, 0x52, 0x45, 0x45, 0x67, 0x61, 0x67, 0x77, 0x67, 0x61, 0x57, 0x42, 0x44, 0x32, 0x6C, 0x75, 0x5A, 0x6D, 0x39, 0x41, 0x61, 0x58, 0x70, 0x6C, 0x62, 0x6E, 0x42, 0x6C, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x61, 0x53, 0x42, 0x6B, 0x54, + 0x43, 0x42, 0x6A, 0x6A, 0x46, 0x48, 0x4D, 0x45, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x2B, 0x0A, 0x53, 0x56, 0x70, 0x46, 0x54, 0x6C, 0x42, 0x46, 0x49, 0x46, 0x4D, 0x75, 0x51, 0x53, 0x34, 0x67, 0x4C, 0x53, 0x42, 0x44, 0x53, + 0x55, 0x59, 0x67, 0x51, 0x54, 0x41, 0x78, 0x4D, 0x7A, 0x4D, 0x33, 0x4D, 0x6A, 0x59, 0x77, 0x4C, 0x56, 0x4A, 0x4E, 0x5A, 0x58, 0x4A, 0x6A, 0x4C, 0x6C, 0x5A, 0x70, 0x64, 0x47, 0x39, 0x79, 0x61, 0x57, 0x45, 0x74, 0x52, 0x32, 0x46, 0x7A, 0x64, + 0x47, 0x56, 0x70, 0x65, 0x69, 0x42, 0x55, 0x4D, 0x54, 0x41, 0x31, 0x4E, 0x53, 0x42, 0x47, 0x0A, 0x4E, 0x6A, 0x49, 0x67, 0x55, 0x7A, 0x67, 0x78, 0x51, 0x7A, 0x42, 0x42, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6B, 0x4D, 0x4F, 0x6B, 0x46, 0x32, + 0x5A, 0x47, 0x45, 0x67, 0x5A, 0x47, 0x56, 0x73, 0x49, 0x45, 0x31, 0x6C, 0x5A, 0x47, 0x6C, 0x30, 0x5A, 0x58, 0x4A, 0x79, 0x59, 0x57, 0x35, 0x6C, 0x62, 0x79, 0x42, 0x46, 0x64, 0x47, 0x39, 0x79, 0x59, 0x6D, 0x6C, 0x6B, 0x5A, 0x57, 0x45, 0x67, + 0x4D, 0x54, 0x51, 0x67, 0x4C, 0x53, 0x41, 0x77, 0x4D, 0x54, 0x41, 0x78, 0x0A, 0x4D, 0x43, 0x42, 0x57, 0x61, 0x58, 0x52, 0x76, 0x63, 0x6D, 0x6C, 0x68, 0x4C, 0x55, 0x64, 0x68, 0x63, 0x33, 0x52, 0x6C, 0x61, 0x58, 0x6F, 0x77, 0x44, 0x77, 0x59, + 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, + 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x0A, 0x42, 0x42, 0x59, 0x45, 0x46, 0x42, 0x30, 0x63, 0x5A, 0x51, 0x36, 0x6F, 0x38, 0x69, 0x56, 0x37, 0x74, 0x4A, 0x48, 0x50, 0x35, 0x4C, 0x47, 0x78, 0x35, 0x72, 0x31, 0x56, 0x64, 0x47, + 0x77, 0x46, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x42, 0x34, 0x70, 0x67, 0x77, 0x57, 0x53, 0x70, 0x39, 0x4D, 0x69, 0x44, + 0x72, 0x41, 0x79, 0x77, 0x36, 0x6C, 0x0A, 0x46, 0x6E, 0x32, 0x66, 0x75, 0x55, 0x68, 0x66, 0x47, 0x49, 0x38, 0x4E, 0x59, 0x6A, 0x62, 0x32, 0x7A, 0x52, 0x6C, 0x72, 0x72, 0x4B, 0x76, 0x56, 0x39, 0x70, 0x46, 0x39, 0x72, 0x6E, 0x48, 0x7A, 0x50, + 0x37, 0x4D, 0x4F, 0x65, 0x49, 0x57, 0x62, 0x6C, 0x61, 0x51, 0x6E, 0x49, 0x55, 0x64, 0x43, 0x53, 0x6E, 0x78, 0x49, 0x4F, 0x76, 0x56, 0x46, 0x66, 0x4C, 0x4D, 0x4D, 0x6A, 0x6C, 0x46, 0x34, 0x72, 0x4A, 0x55, 0x54, 0x33, 0x73, 0x62, 0x39, 0x66, + 0x62, 0x67, 0x61, 0x0A, 0x6B, 0x45, 0x79, 0x72, 0x6B, 0x67, 0x50, 0x48, 0x37, 0x55, 0x49, 0x42, 0x7A, 0x67, 0x2F, 0x59, 0x73, 0x66, 0x71, 0x69, 0x6B, 0x75, 0x46, 0x67, 0x62, 0x61, 0x35, 0x36, 0x61, 0x77, 0x6D, 0x71, 0x78, 0x69, 0x6E, 0x75, + 0x61, 0x45, 0x6C, 0x6E, 0x4D, 0x49, 0x41, 0x6B, 0x65, 0x6A, 0x45, 0x57, 0x4F, 0x56, 0x74, 0x2B, 0x38, 0x52, 0x77, 0x75, 0x33, 0x57, 0x77, 0x4A, 0x72, 0x66, 0x49, 0x78, 0x77, 0x59, 0x4A, 0x4F, 0x75, 0x62, 0x76, 0x35, 0x76, 0x72, 0x38, 0x71, + 0x0A, 0x68, 0x54, 0x2F, 0x41, 0x51, 0x4B, 0x4D, 0x36, 0x57, 0x66, 0x78, 0x5A, 0x53, 0x7A, 0x77, 0x6F, 0x4A, 0x4E, 0x75, 0x30, 0x46, 0x58, 0x57, 0x75, 0x44, 0x59, 0x69, 0x36, 0x4C, 0x6E, 0x50, 0x41, 0x76, 0x56, 0x69, 0x48, 0x35, 0x55, 0x4C, + 0x79, 0x36, 0x31, 0x37, 0x75, 0x48, 0x6A, 0x41, 0x69, 0x6D, 0x63, 0x73, 0x33, 0x30, 0x63, 0x51, 0x68, 0x62, 0x49, 0x48, 0x73, 0x76, 0x6D, 0x30, 0x6D, 0x35, 0x68, 0x7A, 0x6B, 0x51, 0x69, 0x43, 0x65, 0x52, 0x37, 0x43, 0x73, 0x0A, 0x67, 0x31, + 0x6C, 0x77, 0x4C, 0x44, 0x58, 0x57, 0x72, 0x7A, 0x59, 0x30, 0x74, 0x4D, 0x30, 0x37, 0x2B, 0x44, 0x4B, 0x6F, 0x37, 0x2B, 0x4E, 0x34, 0x69, 0x66, 0x75, 0x4E, 0x52, 0x53, 0x7A, 0x61, 0x6E, 0x4C, 0x68, 0x2B, 0x51, 0x42, 0x78, 0x68, 0x35, 0x7A, + 0x36, 0x69, 0x6B, 0x69, 0x78, 0x4C, 0x38, 0x73, 0x33, 0x36, 0x6D, 0x4C, 0x59, 0x70, 0x2F, 0x2F, 0x50, 0x79, 0x65, 0x36, 0x6B, 0x66, 0x4C, 0x71, 0x43, 0x54, 0x56, 0x79, 0x76, 0x65, 0x68, 0x51, 0x50, 0x35, 0x0A, 0x61, 0x54, 0x66, 0x4C, 0x6E, + 0x6E, 0x68, 0x71, 0x42, 0x62, 0x54, 0x46, 0x4D, 0x58, 0x69, 0x4A, 0x37, 0x48, 0x71, 0x6E, 0x68, 0x65, 0x47, 0x35, 0x65, 0x7A, 0x7A, 0x65, 0x76, 0x68, 0x35, 0x35, 0x68, 0x4D, 0x36, 0x66, 0x63, 0x41, 0x35, 0x5A, 0x77, 0x6A, 0x55, 0x75, 0x6B, + 0x43, 0x6F, 0x78, 0x32, 0x65, 0x52, 0x46, 0x65, 0x6B, 0x47, 0x6B, 0x4C, 0x68, 0x4F, 0x62, 0x4E, 0x41, 0x35, 0x6D, 0x65, 0x30, 0x6D, 0x72, 0x5A, 0x4A, 0x66, 0x51, 0x52, 0x73, 0x4E, 0x35, 0x0A, 0x6E, 0x58, 0x4A, 0x51, 0x59, 0x36, 0x61, 0x59, + 0x57, 0x77, 0x61, 0x39, 0x53, 0x47, 0x33, 0x59, 0x4F, 0x59, 0x4E, 0x77, 0x36, 0x44, 0x58, 0x77, 0x42, 0x64, 0x47, 0x71, 0x76, 0x4F, 0x50, 0x62, 0x79, 0x41, 0x4C, 0x71, 0x66, 0x50, 0x32, 0x43, 0x32, 0x73, 0x4A, 0x62, 0x55, 0x6A, 0x57, 0x75, + 0x6D, 0x44, 0x71, 0x74, 0x75, 0x6A, 0x57, 0x54, 0x49, 0x36, 0x63, 0x66, 0x53, 0x4E, 0x30, 0x31, 0x52, 0x70, 0x69, 0x79, 0x45, 0x47, 0x6A, 0x6B, 0x70, 0x54, 0x48, 0x43, 0x0A, 0x43, 0x6C, 0x67, 0x75, 0x47, 0x59, 0x45, 0x51, 0x79, 0x56, 0x42, + 0x31, 0x2F, 0x4F, 0x70, 0x61, 0x46, 0x73, 0x34, 0x52, 0x31, 0x2B, 0x37, 0x76, 0x55, 0x49, 0x67, 0x74, 0x59, 0x66, 0x38, 0x2F, 0x51, 0x6E, 0x4D, 0x46, 0x6C, 0x45, 0x50, 0x56, 0x6A, 0x6A, 0x78, 0x4F, 0x41, 0x54, 0x6F, 0x5A, 0x70, 0x52, 0x39, + 0x47, 0x54, 0x6E, 0x66, 0x51, 0x58, 0x65, 0x57, 0x42, 0x49, 0x69, 0x47, 0x48, 0x2F, 0x70, 0x52, 0x39, 0x68, 0x4E, 0x69, 0x54, 0x72, 0x64, 0x5A, 0x6F, 0x0A, 0x51, 0x30, 0x69, 0x79, 0x32, 0x2B, 0x74, 0x7A, 0x4A, 0x4F, 0x65, 0x52, 0x66, 0x31, + 0x53, 0x6B, 0x74, 0x6F, 0x41, 0x2B, 0x6E, 0x61, 0x4D, 0x38, 0x54, 0x48, 0x4C, 0x43, 0x56, 0x38, 0x53, 0x67, 0x31, 0x4D, 0x77, 0x34, 0x4A, 0x38, 0x37, 0x56, 0x42, 0x70, 0x36, 0x69, 0x53, 0x4E, 0x6E, 0x70, 0x6E, 0x38, 0x36, 0x43, 0x63, 0x44, + 0x61, 0x54, 0x6D, 0x6A, 0x76, 0x66, 0x6C, 0x69, 0x48, 0x6A, 0x57, 0x62, 0x63, 0x4D, 0x32, 0x70, 0x45, 0x33, 0x38, 0x50, 0x31, 0x5A, 0x0A, 0x57, 0x72, 0x4F, 0x5A, 0x79, 0x47, 0x6C, 0x73, 0x51, 0x79, 0x59, 0x42, 0x4E, 0x57, 0x4E, 0x67, 0x56, + 0x59, 0x6B, 0x44, 0x4F, 0x6E, 0x58, 0x59, 0x75, 0x6B, 0x72, 0x5A, 0x56, 0x50, 0x2F, 0x75, 0x33, 0x6F, 0x44, 0x59, 0x4C, 0x64, 0x45, 0x34, 0x31, 0x56, 0x34, 0x74, 0x43, 0x35, 0x68, 0x39, 0x50, 0x6D, 0x7A, 0x62, 0x2F, 0x43, 0x61, 0x49, 0x78, + 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x6F, 0x20, 0x44, 0x61, 0x64, 0x64, 0x79, 0x20, + 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2D, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x78, 0x54, 0x43, 0x43, 0x41, 0x71, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x42, 0x41, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x67, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, + 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x0A, 0x42, 0x30, 0x46, 0x79, 0x61, 0x58, 0x70, 0x76, 0x62, 0x6D, 0x45, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, + 0x63, 0x54, 0x43, 0x6C, 0x4E, 0x6A, 0x62, 0x33, 0x52, 0x30, 0x63, 0x32, 0x52, 0x68, 0x62, 0x47, 0x55, 0x78, 0x47, 0x6A, 0x41, 0x59, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x55, 0x64, 0x76, 0x52, 0x47, 0x46, 0x6B, 0x5A, 0x48, + 0x6B, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x0A, 0x4D, 0x54, 0x45, 0x77, 0x4C, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x68, 0x48, 0x62, 0x79, 0x42, 0x45, 0x59, 0x57, 0x52, 0x6B, 0x65, + 0x53, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x6C, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x41, 0x74, 0x49, + 0x45, 0x63, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x35, 0x0A, 0x4D, 0x44, 0x6B, 0x77, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x33, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x49, 0x7A, + 0x4E, 0x54, 0x6B, 0x31, 0x4F, 0x56, 0x6F, 0x77, 0x67, 0x59, 0x4D, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x49, + 0x45, 0x77, 0x64, 0x42, 0x63, 0x6D, 0x6C, 0x36, 0x0A, 0x62, 0x32, 0x35, 0x68, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x45, 0x77, 0x70, 0x54, 0x59, 0x32, 0x39, 0x30, 0x64, 0x48, 0x4E, 0x6B, 0x59, 0x57, 0x78, + 0x6C, 0x4D, 0x52, 0x6F, 0x77, 0x47, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x46, 0x48, 0x62, 0x30, 0x52, 0x68, 0x5A, 0x47, 0x52, 0x35, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, + 0x78, 0x4D, 0x43, 0x38, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x6F, 0x52, 0x32, 0x38, 0x67, 0x52, 0x47, 0x46, 0x6B, 0x5A, 0x48, 0x6B, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, + 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x5A, 0x53, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x4C, 0x53, 0x42, 0x48, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, + 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4C, 0x39, 0x78, 0x59, 0x67, 0x6A, 0x78, 0x2B, + 0x6C, 0x6B, 0x30, 0x39, 0x78, 0x76, 0x4A, 0x47, 0x4B, 0x50, 0x33, 0x67, 0x45, 0x6C, 0x59, 0x36, 0x53, 0x4B, 0x44, 0x45, 0x36, 0x62, 0x46, 0x49, 0x45, 0x4D, 0x42, 0x4F, 0x34, 0x54, 0x78, 0x35, 0x6F, 0x56, 0x4A, 0x6E, 0x79, 0x66, 0x71, 0x0A, + 0x39, 0x6F, 0x51, 0x62, 0x54, 0x71, 0x43, 0x30, 0x32, 0x33, 0x43, 0x59, 0x78, 0x7A, 0x49, 0x42, 0x73, 0x51, 0x55, 0x2B, 0x42, 0x30, 0x37, 0x75, 0x39, 0x50, 0x70, 0x50, 0x4C, 0x31, 0x6B, 0x77, 0x49, 0x75, 0x65, 0x72, 0x47, 0x56, 0x5A, 0x72, + 0x34, 0x6F, 0x41, 0x48, 0x2F, 0x50, 0x4D, 0x57, 0x64, 0x59, 0x41, 0x35, 0x55, 0x58, 0x76, 0x6C, 0x2B, 0x54, 0x57, 0x32, 0x64, 0x45, 0x36, 0x70, 0x6A, 0x59, 0x49, 0x54, 0x35, 0x4C, 0x59, 0x2F, 0x71, 0x51, 0x4F, 0x44, 0x0A, 0x2B, 0x71, 0x4B, + 0x2B, 0x69, 0x68, 0x56, 0x71, 0x66, 0x39, 0x34, 0x4C, 0x77, 0x37, 0x59, 0x5A, 0x46, 0x41, 0x58, 0x4B, 0x36, 0x73, 0x4F, 0x6F, 0x42, 0x4A, 0x51, 0x37, 0x52, 0x6E, 0x77, 0x79, 0x44, 0x66, 0x4D, 0x41, 0x5A, 0x69, 0x4C, 0x49, 0x6A, 0x57, 0x6C, + 0x74, 0x4E, 0x6F, 0x77, 0x52, 0x47, 0x4C, 0x66, 0x54, 0x73, 0x68, 0x78, 0x67, 0x74, 0x44, 0x6A, 0x36, 0x41, 0x6F, 0x7A, 0x4F, 0x30, 0x39, 0x31, 0x47, 0x42, 0x39, 0x34, 0x4B, 0x50, 0x75, 0x74, 0x64, 0x0A, 0x66, 0x4D, 0x68, 0x38, 0x2B, 0x37, + 0x41, 0x72, 0x55, 0x36, 0x53, 0x53, 0x59, 0x6D, 0x6C, 0x52, 0x4A, 0x51, 0x56, 0x68, 0x47, 0x6B, 0x53, 0x42, 0x6A, 0x43, 0x79, 0x70, 0x51, 0x35, 0x59, 0x6A, 0x33, 0x36, 0x77, 0x36, 0x67, 0x5A, 0x6F, 0x4F, 0x4B, 0x63, 0x55, 0x63, 0x71, 0x65, + 0x6C, 0x64, 0x48, 0x72, 0x61, 0x65, 0x6E, 0x6A, 0x41, 0x4B, 0x4F, 0x63, 0x37, 0x78, 0x69, 0x49, 0x44, 0x37, 0x53, 0x31, 0x33, 0x4D, 0x4D, 0x75, 0x79, 0x46, 0x59, 0x6B, 0x4D, 0x6C, 0x0A, 0x4E, 0x41, 0x4A, 0x57, 0x4A, 0x77, 0x47, 0x52, 0x74, + 0x44, 0x74, 0x77, 0x4B, 0x6A, 0x39, 0x75, 0x73, 0x65, 0x69, 0x63, 0x69, 0x41, 0x46, 0x39, 0x6E, 0x39, 0x54, 0x35, 0x32, 0x31, 0x4E, 0x74, 0x59, 0x4A, 0x32, 0x2F, 0x4C, 0x4F, 0x64, 0x59, 0x71, 0x37, 0x68, 0x66, 0x52, 0x76, 0x7A, 0x4F, 0x78, + 0x42, 0x73, 0x44, 0x50, 0x41, 0x6E, 0x72, 0x53, 0x54, 0x46, 0x63, 0x61, 0x55, 0x61, 0x7A, 0x34, 0x45, 0x63, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x0A, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, + 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, + 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x44, 0x71, 0x61, 0x68, 0x51, 0x63, 0x51, 0x5A, 0x79, 0x69, 0x32, 0x37, 0x2F, 0x61, 0x39, 0x0A, 0x42, 0x55, 0x46, 0x75, 0x49, 0x4D, 0x47, 0x55, 0x32, 0x67, 0x2F, 0x65, 0x4D, 0x41, 0x30, + 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, 0x5A, 0x32, 0x31, 0x31, 0x35, 0x31, 0x66, 0x6D, 0x58, 0x57, 0x57, 0x63, 0x44, 0x59, 0x66, 0x46, + 0x2B, 0x4F, 0x77, 0x59, 0x78, 0x64, 0x53, 0x32, 0x68, 0x49, 0x49, 0x35, 0x50, 0x5A, 0x59, 0x65, 0x30, 0x39, 0x36, 0x61, 0x63, 0x0A, 0x76, 0x4E, 0x6A, 0x70, 0x4C, 0x39, 0x44, 0x62, 0x57, 0x75, 0x37, 0x50, 0x64, 0x49, 0x78, 0x7A, 0x74, 0x44, + 0x68, 0x43, 0x32, 0x67, 0x56, 0x37, 0x2B, 0x41, 0x4A, 0x31, 0x75, 0x50, 0x32, 0x6C, 0x73, 0x64, 0x65, 0x75, 0x39, 0x74, 0x66, 0x65, 0x45, 0x38, 0x74, 0x54, 0x45, 0x48, 0x36, 0x4B, 0x52, 0x74, 0x47, 0x58, 0x2B, 0x72, 0x63, 0x75, 0x4B, 0x78, + 0x47, 0x72, 0x6B, 0x4C, 0x41, 0x6E, 0x67, 0x50, 0x6E, 0x6F, 0x6E, 0x31, 0x72, 0x70, 0x4E, 0x35, 0x2B, 0x72, 0x0A, 0x35, 0x4E, 0x39, 0x73, 0x73, 0x34, 0x55, 0x58, 0x6E, 0x54, 0x33, 0x5A, 0x4A, 0x45, 0x39, 0x35, 0x6B, 0x54, 0x58, 0x57, 0x58, + 0x77, 0x54, 0x72, 0x67, 0x49, 0x4F, 0x72, 0x6D, 0x67, 0x49, 0x74, 0x74, 0x52, 0x44, 0x30, 0x32, 0x4A, 0x44, 0x48, 0x42, 0x48, 0x4E, 0x41, 0x37, 0x58, 0x49, 0x6C, 0x6F, 0x4B, 0x6D, 0x66, 0x37, 0x4A, 0x36, 0x72, 0x61, 0x42, 0x4B, 0x5A, 0x56, + 0x38, 0x61, 0x50, 0x45, 0x6A, 0x6F, 0x4A, 0x70, 0x4C, 0x31, 0x45, 0x2F, 0x51, 0x59, 0x56, 0x0A, 0x4E, 0x38, 0x47, 0x62, 0x35, 0x44, 0x4B, 0x6A, 0x37, 0x54, 0x6A, 0x6F, 0x32, 0x47, 0x54, 0x7A, 0x4C, 0x48, 0x34, 0x55, 0x2F, 0x41, 0x4C, 0x71, + 0x6E, 0x38, 0x33, 0x2F, 0x42, 0x32, 0x67, 0x58, 0x32, 0x79, 0x4B, 0x51, 0x4F, 0x43, 0x31, 0x36, 0x6A, 0x64, 0x46, 0x55, 0x38, 0x57, 0x6E, 0x6A, 0x58, 0x7A, 0x50, 0x4B, 0x65, 0x6A, 0x31, 0x37, 0x43, 0x75, 0x50, 0x4B, 0x66, 0x31, 0x38, 0x35, + 0x35, 0x65, 0x4A, 0x31, 0x75, 0x73, 0x56, 0x32, 0x47, 0x44, 0x50, 0x4F, 0x0A, 0x4C, 0x50, 0x41, 0x76, 0x54, 0x4B, 0x33, 0x33, 0x73, 0x65, 0x66, 0x4F, 0x54, 0x36, 0x6A, 0x45, 0x6D, 0x30, 0x70, 0x55, 0x42, 0x73, 0x56, 0x2F, 0x66, 0x64, 0x55, + 0x49, 0x44, 0x2B, 0x49, 0x63, 0x2F, 0x6E, 0x34, 0x58, 0x75, 0x4B, 0x78, 0x65, 0x39, 0x74, 0x51, 0x57, 0x73, 0x6B, 0x4D, 0x4A, 0x44, 0x45, 0x33, 0x32, 0x70, 0x32, 0x75, 0x30, 0x6D, 0x59, 0x52, 0x6C, 0x79, 0x6E, 0x71, 0x49, 0x34, 0x75, 0x4A, + 0x45, 0x76, 0x6C, 0x7A, 0x33, 0x36, 0x68, 0x7A, 0x31, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x74, 0x61, + 0x72, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2D, 0x20, 0x47, 0x32, 0x0A, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x33, 0x54, 0x43, 0x43, 0x41, 0x73, 0x57, + 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x6A, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x0A, 0x42, 0x30, 0x46, 0x79, 0x61, 0x58, 0x70, 0x76, 0x62, 0x6D, 0x45, 0x78, 0x45, 0x7A, + 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x6C, 0x4E, 0x6A, 0x62, 0x33, 0x52, 0x30, 0x63, 0x32, 0x52, 0x68, 0x62, 0x47, 0x55, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x46, + 0x4E, 0x30, 0x59, 0x58, 0x4A, 0x6D, 0x61, 0x57, 0x56, 0x73, 0x5A, 0x43, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x0A, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4D, + 0x6A, 0x41, 0x77, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4B, 0x56, 0x4E, 0x30, 0x59, 0x58, 0x4A, 0x6D, 0x61, 0x57, 0x56, 0x73, 0x5A, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, + 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x6C, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x0A, 0x65, 0x53, 0x41, 0x74, 0x49, 0x45, 0x63, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x35, 0x4D, 0x44, 0x6B, 0x77, + 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x33, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x49, 0x7A, 0x4E, 0x54, 0x6B, 0x31, 0x4F, 0x56, 0x6F, 0x77, 0x67, 0x59, 0x38, 0x78, 0x43, 0x7A, 0x41, 0x4A, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x41, 0x77, 0x0A, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x49, 0x45, 0x77, 0x64, 0x42, 0x63, 0x6D, 0x6C, 0x36, 0x62, 0x32, 0x35, 0x68, 0x4D, 0x52, 0x4D, + 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x45, 0x77, 0x70, 0x54, 0x59, 0x32, 0x39, 0x30, 0x64, 0x48, 0x4E, 0x6B, 0x59, 0x57, 0x78, 0x6C, 0x4D, 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x78, + 0x54, 0x64, 0x47, 0x46, 0x79, 0x5A, 0x6D, 0x6C, 0x6C, 0x62, 0x47, 0x51, 0x67, 0x0A, 0x56, 0x47, 0x56, 0x6A, 0x61, 0x47, 0x35, 0x76, 0x62, 0x47, 0x39, 0x6E, 0x61, 0x57, 0x56, 0x7A, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x4D, 0x54, + 0x49, 0x77, 0x4D, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x6C, 0x54, 0x64, 0x47, 0x46, 0x79, 0x5A, 0x6D, 0x6C, 0x6C, 0x62, 0x47, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, + 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x5A, 0x53, 0x42, 0x42, 0x0A, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x4C, 0x53, 0x42, 0x48, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, + 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4C, 0x33, 0x74, 0x77, 0x51, 0x50, 0x38, 0x39, + 0x6F, 0x2F, 0x38, 0x41, 0x72, 0x46, 0x76, 0x0A, 0x57, 0x35, 0x39, 0x49, 0x32, 0x5A, 0x31, 0x35, 0x34, 0x71, 0x4B, 0x33, 0x41, 0x32, 0x46, 0x57, 0x47, 0x4D, 0x4E, 0x48, 0x74, 0x74, 0x66, 0x4B, 0x50, 0x54, 0x55, 0x75, 0x69, 0x55, 0x50, 0x33, + 0x6F, 0x57, 0x6D, 0x62, 0x33, 0x6F, 0x6F, 0x61, 0x2F, 0x52, 0x4D, 0x67, 0x6E, 0x4C, 0x52, 0x4A, 0x64, 0x7A, 0x49, 0x70, 0x56, 0x76, 0x32, 0x35, 0x37, 0x49, 0x7A, 0x64, 0x49, 0x76, 0x70, 0x79, 0x33, 0x43, 0x64, 0x68, 0x6C, 0x2B, 0x37, 0x32, + 0x57, 0x6F, 0x54, 0x73, 0x0A, 0x62, 0x68, 0x6D, 0x35, 0x69, 0x53, 0x7A, 0x63, 0x68, 0x46, 0x76, 0x56, 0x64, 0x50, 0x74, 0x72, 0x58, 0x38, 0x57, 0x4A, 0x70, 0x52, 0x42, 0x53, 0x69, 0x55, 0x5A, 0x56, 0x39, 0x4C, 0x68, 0x31, 0x48, 0x4F, 0x5A, + 0x2F, 0x35, 0x46, 0x53, 0x75, 0x53, 0x2F, 0x68, 0x56, 0x63, 0x6C, 0x63, 0x43, 0x47, 0x66, 0x67, 0x58, 0x63, 0x56, 0x6E, 0x72, 0x48, 0x69, 0x67, 0x48, 0x64, 0x4D, 0x57, 0x64, 0x53, 0x4C, 0x35, 0x73, 0x74, 0x50, 0x53, 0x6B, 0x73, 0x50, 0x4E, + 0x6B, 0x0A, 0x4E, 0x33, 0x6D, 0x53, 0x77, 0x4F, 0x78, 0x47, 0x58, 0x6E, 0x2F, 0x68, 0x62, 0x56, 0x4E, 0x4D, 0x59, 0x71, 0x2F, 0x4E, 0x48, 0x77, 0x74, 0x6A, 0x75, 0x7A, 0x71, 0x64, 0x2B, 0x2F, 0x78, 0x35, 0x41, 0x4A, 0x68, 0x68, 0x64, 0x4D, + 0x38, 0x6D, 0x67, 0x6B, 0x42, 0x6A, 0x38, 0x37, 0x4A, 0x79, 0x61, 0x68, 0x6B, 0x4E, 0x6D, 0x63, 0x72, 0x55, 0x44, 0x6E, 0x58, 0x4D, 0x4E, 0x2F, 0x75, 0x4C, 0x69, 0x63, 0x46, 0x5A, 0x38, 0x57, 0x4A, 0x2F, 0x58, 0x37, 0x4E, 0x66, 0x0A, 0x5A, + 0x54, 0x44, 0x34, 0x70, 0x37, 0x64, 0x4E, 0x64, 0x6C, 0x6F, 0x65, 0x64, 0x6C, 0x34, 0x30, 0x77, 0x4F, 0x69, 0x57, 0x56, 0x70, 0x6D, 0x4B, 0x73, 0x2F, 0x42, 0x2F, 0x70, 0x4D, 0x32, 0x39, 0x33, 0x44, 0x49, 0x78, 0x66, 0x4A, 0x48, 0x50, 0x34, + 0x46, 0x38, 0x52, 0x2B, 0x47, 0x75, 0x71, 0x53, 0x56, 0x7A, 0x52, 0x6D, 0x5A, 0x54, 0x52, 0x6F, 0x75, 0x4E, 0x6A, 0x57, 0x77, 0x6C, 0x32, 0x74, 0x56, 0x5A, 0x69, 0x34, 0x55, 0x74, 0x30, 0x48, 0x5A, 0x62, 0x55, 0x0A, 0x4A, 0x74, 0x51, 0x49, + 0x42, 0x46, 0x6E, 0x51, 0x6D, 0x41, 0x34, 0x4F, 0x35, 0x74, 0x37, 0x38, 0x77, 0x2B, 0x77, 0x66, 0x6B, 0x50, 0x45, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, + 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x0A, 0x41, 0x51, 0x59, 0x77, 0x48, 0x51, 0x59, + 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x48, 0x77, 0x4D, 0x4D, 0x68, 0x2B, 0x6E, 0x32, 0x54, 0x42, 0x2F, 0x78, 0x48, 0x31, 0x6F, 0x6F, 0x32, 0x4B, 0x6F, 0x6F, 0x63, 0x36, 0x72, 0x42, 0x31, 0x73, 0x6E, 0x4D, 0x41, 0x30, + 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x41, 0x52, 0x57, 0x66, 0x6F, 0x6C, 0x0A, 0x54, 0x77, 0x4E, 0x76, 0x6C, 0x4A, 0x6B, 0x37, 0x6D, 0x68, + 0x2B, 0x43, 0x68, 0x54, 0x6E, 0x55, 0x64, 0x67, 0x57, 0x55, 0x58, 0x75, 0x45, 0x6F, 0x6B, 0x32, 0x31, 0x69, 0x58, 0x51, 0x6E, 0x43, 0x6F, 0x4B, 0x6A, 0x55, 0x73, 0x48, 0x55, 0x34, 0x38, 0x54, 0x52, 0x71, 0x6E, 0x65, 0x53, 0x66, 0x69, 0x6F, + 0x59, 0x6D, 0x55, 0x65, 0x59, 0x73, 0x30, 0x63, 0x59, 0x74, 0x62, 0x70, 0x55, 0x67, 0x53, 0x70, 0x49, 0x42, 0x37, 0x4C, 0x69, 0x4B, 0x5A, 0x33, 0x73, 0x78, 0x0A, 0x34, 0x6D, 0x63, 0x75, 0x6A, 0x4A, 0x55, 0x44, 0x4A, 0x69, 0x35, 0x44, 0x6E, + 0x55, 0x6F, 0x78, 0x39, 0x67, 0x36, 0x31, 0x44, 0x4C, 0x75, 0x33, 0x34, 0x6A, 0x64, 0x2F, 0x49, 0x72, 0x6F, 0x41, 0x6F, 0x77, 0x35, 0x37, 0x55, 0x76, 0x74, 0x72, 0x75, 0x7A, 0x76, 0x45, 0x30, 0x33, 0x6C, 0x52, 0x54, 0x73, 0x32, 0x51, 0x39, + 0x47, 0x63, 0x48, 0x47, 0x63, 0x67, 0x38, 0x52, 0x6E, 0x6F, 0x4E, 0x41, 0x58, 0x33, 0x46, 0x57, 0x4F, 0x64, 0x74, 0x35, 0x6F, 0x55, 0x77, 0x0A, 0x46, 0x35, 0x6F, 0x6B, 0x78, 0x42, 0x44, 0x67, 0x42, 0x50, 0x66, 0x67, 0x38, 0x6E, 0x2F, 0x55, + 0x71, 0x67, 0x72, 0x2F, 0x51, 0x68, 0x30, 0x33, 0x37, 0x5A, 0x54, 0x6C, 0x5A, 0x46, 0x6B, 0x53, 0x49, 0x48, 0x63, 0x34, 0x30, 0x7A, 0x49, 0x2B, 0x4F, 0x49, 0x46, 0x31, 0x6C, 0x6E, 0x50, 0x36, 0x61, 0x49, 0x2B, 0x78, 0x79, 0x38, 0x34, 0x66, + 0x78, 0x65, 0x7A, 0x36, 0x6E, 0x48, 0x37, 0x50, 0x66, 0x72, 0x48, 0x78, 0x42, 0x79, 0x32, 0x32, 0x2F, 0x4C, 0x2F, 0x4B, 0x0A, 0x70, 0x4C, 0x2F, 0x51, 0x6C, 0x77, 0x56, 0x4B, 0x76, 0x4F, 0x6F, 0x59, 0x4B, 0x41, 0x4B, 0x51, 0x76, 0x56, 0x52, + 0x34, 0x43, 0x53, 0x46, 0x78, 0x30, 0x39, 0x46, 0x39, 0x48, 0x64, 0x6B, 0x57, 0x73, 0x4B, 0x6C, 0x68, 0x50, 0x64, 0x41, 0x4B, 0x41, 0x43, 0x4C, 0x38, 0x78, 0x33, 0x76, 0x4C, 0x43, 0x57, 0x52, 0x46, 0x43, 0x7A, 0x74, 0x41, 0x67, 0x66, 0x64, + 0x39, 0x66, 0x44, 0x4C, 0x31, 0x6D, 0x4D, 0x70, 0x59, 0x6A, 0x6E, 0x30, 0x71, 0x37, 0x70, 0x42, 0x5A, 0x0A, 0x63, 0x32, 0x54, 0x35, 0x4E, 0x6E, 0x52, 0x65, 0x4A, 0x61, 0x48, 0x31, 0x5A, 0x67, 0x55, 0x75, 0x66, 0x7A, 0x6B, 0x56, 0x71, 0x53, + 0x72, 0x37, 0x55, 0x49, 0x75, 0x4F, 0x68, 0x57, 0x6E, 0x30, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x74, + 0x61, 0x72, 0x66, 0x69, 0x65, 0x6C, 0x64, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, + 0x69, 0x74, 0x79, 0x20, 0x2D, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x37, 0x7A, 0x43, 0x43, 0x41, 0x74, 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, + 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x6D, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x67, 0x54, 0x0A, 0x42, 0x30, 0x46, 0x79, 0x61, 0x58, 0x70, 0x76, 0x62, 0x6D, 0x45, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x6C, 0x4E, 0x6A, 0x62, 0x33, 0x52, 0x30, 0x63, 0x32, 0x52, + 0x68, 0x62, 0x47, 0x55, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x46, 0x4E, 0x30, 0x59, 0x58, 0x4A, 0x6D, 0x61, 0x57, 0x56, 0x73, 0x5A, 0x43, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, + 0x73, 0x0A, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4F, 0x7A, 0x41, 0x35, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4D, 0x6C, 0x4E, 0x30, 0x59, 0x58, 0x4A, 0x6D, 0x61, 0x57, + 0x56, 0x73, 0x5A, 0x43, 0x42, 0x54, 0x5A, 0x58, 0x4A, 0x32, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x6C, 0x0A, 0x49, + 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x41, 0x74, 0x49, 0x45, 0x63, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x35, 0x4D, 0x44, 0x6B, 0x77, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, + 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x33, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x49, 0x7A, 0x4E, 0x54, 0x6B, 0x31, 0x4F, 0x56, 0x6F, 0x77, 0x67, 0x5A, 0x67, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x59, 0x54, + 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x49, 0x45, 0x77, 0x64, 0x42, 0x63, 0x6D, 0x6C, 0x36, 0x62, 0x32, 0x35, 0x68, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, + 0x45, 0x77, 0x70, 0x54, 0x59, 0x32, 0x39, 0x30, 0x64, 0x48, 0x4E, 0x6B, 0x59, 0x57, 0x78, 0x6C, 0x4D, 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x78, 0x54, 0x0A, 0x64, 0x47, 0x46, 0x79, 0x5A, 0x6D, 0x6C, + 0x6C, 0x62, 0x47, 0x51, 0x67, 0x56, 0x47, 0x56, 0x6A, 0x61, 0x47, 0x35, 0x76, 0x62, 0x47, 0x39, 0x6E, 0x61, 0x57, 0x56, 0x7A, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x4D, 0x54, 0x73, 0x77, 0x4F, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x44, 0x45, 0x7A, 0x4A, 0x54, 0x64, 0x47, 0x46, 0x79, 0x5A, 0x6D, 0x6C, 0x6C, 0x62, 0x47, 0x51, 0x67, 0x55, 0x32, 0x56, 0x79, 0x64, 0x6D, 0x6C, 0x6A, 0x5A, 0x58, 0x4D, 0x67, 0x0A, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, + 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x5A, 0x53, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x4C, 0x53, 0x42, 0x48, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, + 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x0A, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4E, 0x55, 0x4D, 0x4F, + 0x73, 0x51, 0x71, 0x2B, 0x55, 0x37, 0x69, 0x39, 0x62, 0x34, 0x5A, 0x6C, 0x31, 0x2B, 0x4F, 0x69, 0x46, 0x4F, 0x78, 0x48, 0x7A, 0x2F, 0x4C, 0x7A, 0x35, 0x38, 0x67, 0x45, 0x32, 0x30, 0x70, 0x4F, 0x73, 0x67, 0x50, 0x66, 0x54, 0x7A, 0x33, 0x61, + 0x33, 0x59, 0x34, 0x59, 0x39, 0x6B, 0x32, 0x59, 0x4B, 0x69, 0x62, 0x58, 0x6C, 0x77, 0x41, 0x67, 0x4C, 0x49, 0x76, 0x57, 0x58, 0x2F, 0x32, 0x0A, 0x68, 0x2F, 0x6B, 0x6C, 0x51, 0x34, 0x62, 0x6E, 0x61, 0x52, 0x74, 0x53, 0x6D, 0x70, 0x44, 0x68, + 0x63, 0x65, 0x50, 0x59, 0x4C, 0x51, 0x31, 0x4F, 0x62, 0x2F, 0x62, 0x49, 0x53, 0x64, 0x6D, 0x32, 0x38, 0x78, 0x70, 0x57, 0x72, 0x69, 0x75, 0x32, 0x64, 0x42, 0x54, 0x72, 0x7A, 0x2F, 0x73, 0x6D, 0x34, 0x78, 0x71, 0x36, 0x48, 0x5A, 0x59, 0x75, + 0x61, 0x6A, 0x74, 0x59, 0x6C, 0x49, 0x6C, 0x48, 0x56, 0x76, 0x38, 0x6C, 0x6F, 0x4A, 0x4E, 0x77, 0x55, 0x34, 0x50, 0x61, 0x0A, 0x68, 0x48, 0x51, 0x55, 0x77, 0x32, 0x65, 0x65, 0x42, 0x47, 0x67, 0x36, 0x33, 0x34, 0x35, 0x41, 0x57, 0x68, 0x31, + 0x4B, 0x54, 0x73, 0x39, 0x44, 0x6B, 0x54, 0x76, 0x6E, 0x56, 0x74, 0x59, 0x41, 0x63, 0x4D, 0x74, 0x53, 0x37, 0x6E, 0x74, 0x39, 0x72, 0x6A, 0x72, 0x6E, 0x76, 0x44, 0x48, 0x35, 0x52, 0x66, 0x62, 0x43, 0x59, 0x4D, 0x38, 0x54, 0x57, 0x51, 0x49, + 0x72, 0x67, 0x4D, 0x77, 0x30, 0x52, 0x39, 0x2B, 0x35, 0x33, 0x70, 0x42, 0x6C, 0x62, 0x51, 0x4C, 0x50, 0x0A, 0x4C, 0x4A, 0x47, 0x6D, 0x70, 0x75, 0x66, 0x65, 0x68, 0x52, 0x68, 0x4A, 0x66, 0x47, 0x5A, 0x4F, 0x6F, 0x7A, 0x70, 0x74, 0x71, 0x62, + 0x58, 0x75, 0x4E, 0x43, 0x36, 0x36, 0x44, 0x51, 0x4F, 0x34, 0x4D, 0x39, 0x39, 0x48, 0x36, 0x37, 0x46, 0x72, 0x6A, 0x53, 0x58, 0x5A, 0x6D, 0x38, 0x36, 0x42, 0x30, 0x55, 0x56, 0x47, 0x4D, 0x70, 0x5A, 0x77, 0x68, 0x39, 0x34, 0x43, 0x44, 0x6B, + 0x6C, 0x44, 0x68, 0x62, 0x5A, 0x73, 0x63, 0x37, 0x74, 0x6B, 0x36, 0x6D, 0x46, 0x42, 0x0A, 0x72, 0x4D, 0x6E, 0x55, 0x56, 0x4E, 0x2B, 0x48, 0x4C, 0x38, 0x63, 0x69, 0x73, 0x69, 0x62, 0x4D, 0x6E, 0x31, 0x6C, 0x55, 0x61, 0x4A, 0x2F, 0x38, 0x76, + 0x69, 0x6F, 0x76, 0x78, 0x46, 0x55, 0x63, 0x64, 0x55, 0x42, 0x67, 0x46, 0x34, 0x55, 0x43, 0x56, 0x54, 0x6D, 0x4C, 0x66, 0x77, 0x55, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, + 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x0A, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, + 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4A, 0x78, 0x66, 0x41, 0x4E, 0x2B, 0x71, 0x41, 0x64, 0x63, 0x77, 0x4B, 0x7A, 0x69, 0x49, 0x6F, 0x72, 0x68, 0x74, 0x53, 0x70, 0x7A, 0x79, 0x45, 0x5A, 0x47, 0x44, + 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x0A, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x42, 0x4C, 0x4E, 0x71, 0x61, 0x45, 0x64, 0x32, 0x6E, 0x64, 0x4F, 0x78, 0x6D, + 0x66, 0x5A, 0x79, 0x4D, 0x49, 0x62, 0x77, 0x35, 0x68, 0x79, 0x66, 0x32, 0x45, 0x33, 0x46, 0x2F, 0x59, 0x4E, 0x6F, 0x48, 0x4E, 0x32, 0x42, 0x74, 0x42, 0x4C, 0x5A, 0x39, 0x67, 0x33, 0x63, 0x63, 0x61, 0x61, 0x4E, 0x6E, 0x52, 0x62, 0x6F, 0x62, + 0x68, 0x69, 0x43, 0x50, 0x50, 0x0A, 0x45, 0x39, 0x35, 0x44, 0x7A, 0x2B, 0x49, 0x30, 0x73, 0x77, 0x53, 0x64, 0x48, 0x79, 0x6E, 0x56, 0x76, 0x2F, 0x68, 0x65, 0x79, 0x4E, 0x58, 0x42, 0x76, 0x65, 0x36, 0x53, 0x62, 0x7A, 0x4A, 0x30, 0x38, 0x70, + 0x47, 0x43, 0x4C, 0x37, 0x32, 0x43, 0x51, 0x6E, 0x71, 0x74, 0x4B, 0x72, 0x63, 0x67, 0x66, 0x55, 0x32, 0x38, 0x65, 0x6C, 0x55, 0x53, 0x77, 0x68, 0x58, 0x71, 0x76, 0x66, 0x64, 0x71, 0x6C, 0x53, 0x35, 0x73, 0x64, 0x4A, 0x2F, 0x50, 0x48, 0x4C, + 0x54, 0x79, 0x0A, 0x78, 0x51, 0x47, 0x6A, 0x68, 0x64, 0x42, 0x79, 0x50, 0x71, 0x31, 0x7A, 0x71, 0x77, 0x75, 0x62, 0x64, 0x51, 0x78, 0x74, 0x52, 0x62, 0x65, 0x4F, 0x6C, 0x4B, 0x79, 0x57, 0x4E, 0x37, 0x57, 0x67, 0x30, 0x49, 0x38, 0x56, 0x52, + 0x77, 0x37, 0x6A, 0x36, 0x49, 0x50, 0x64, 0x6A, 0x2F, 0x33, 0x76, 0x51, 0x51, 0x46, 0x33, 0x7A, 0x43, 0x65, 0x70, 0x59, 0x6F, 0x55, 0x7A, 0x38, 0x6A, 0x63, 0x49, 0x37, 0x33, 0x48, 0x50, 0x64, 0x77, 0x62, 0x65, 0x79, 0x42, 0x6B, 0x64, 0x0A, + 0x69, 0x45, 0x44, 0x50, 0x66, 0x55, 0x59, 0x64, 0x2F, 0x78, 0x37, 0x48, 0x34, 0x63, 0x37, 0x2F, 0x49, 0x39, 0x76, 0x47, 0x2B, 0x6F, 0x31, 0x56, 0x54, 0x71, 0x6B, 0x43, 0x35, 0x30, 0x63, 0x52, 0x52, 0x6A, 0x37, 0x30, 0x2F, 0x62, 0x31, 0x37, + 0x4B, 0x53, 0x61, 0x37, 0x71, 0x57, 0x46, 0x69, 0x4E, 0x79, 0x69, 0x32, 0x4C, 0x53, 0x72, 0x32, 0x45, 0x49, 0x5A, 0x6B, 0x79, 0x58, 0x43, 0x6E, 0x30, 0x71, 0x32, 0x33, 0x4B, 0x58, 0x42, 0x35, 0x36, 0x6A, 0x7A, 0x61, 0x0A, 0x59, 0x79, 0x57, + 0x66, 0x2F, 0x57, 0x69, 0x33, 0x4D, 0x4F, 0x78, 0x77, 0x2B, 0x33, 0x57, 0x4B, 0x74, 0x32, 0x31, 0x67, 0x5A, 0x37, 0x49, 0x65, 0x79, 0x4C, 0x6E, 0x70, 0x32, 0x4B, 0x68, 0x76, 0x41, 0x6F, 0x74, 0x6E, 0x44, 0x55, 0x30, 0x6D, 0x56, 0x33, 0x48, + 0x61, 0x49, 0x50, 0x7A, 0x42, 0x53, 0x6C, 0x43, 0x4E, 0x73, 0x53, 0x69, 0x36, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x0A, 0x41, 0x66, 0x66, 0x69, 0x72, 0x6D, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x43, 0x6F, 0x6D, 0x6D, 0x65, 0x72, 0x63, 0x69, 0x61, 0x6C, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x54, + 0x44, 0x43, 0x43, 0x41, 0x6A, 0x53, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x64, 0x33, 0x63, 0x47, 0x4A, 0x79, 0x61, 0x70, 0x73, 0x58, 0x77, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, + 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x52, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, 0x53, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, + 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x62, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x5A, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, + 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x44, 0x62, 0x32, 0x31, 0x74, 0x5A, 0x58, 0x4A, 0x6A, 0x61, 0x57, 0x46, 0x73, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x77, 0x0A, 0x4D, 0x44, 0x45, 0x79, 0x4F, 0x54, 0x45, 0x30, 0x4D, 0x44, 0x59, + 0x77, 0x4E, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x77, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x45, 0x30, 0x4D, 0x44, 0x59, 0x77, 0x4E, 0x6C, 0x6F, 0x77, 0x52, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, + 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x0A, 0x62, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x5A, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x44, 0x62, 0x32, 0x31, 0x74, 0x5A, 0x58, 0x4A, 0x6A, 0x61, 0x57, 0x46, 0x73, 0x4D, 0x49, + 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x0A, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, + 0x51, 0x45, 0x41, 0x39, 0x68, 0x74, 0x50, 0x5A, 0x77, 0x63, 0x72, 0x6F, 0x52, 0x58, 0x31, 0x42, 0x69, 0x4C, 0x4C, 0x48, 0x77, 0x47, 0x79, 0x34, 0x33, 0x4E, 0x46, 0x42, 0x6B, 0x52, 0x4A, 0x4C, 0x4C, 0x74, 0x4A, 0x4A, 0x52, 0x54, 0x57, 0x7A, + 0x73, 0x4F, 0x33, 0x71, 0x79, 0x78, 0x50, 0x78, 0x6B, 0x45, 0x79, 0x6C, 0x46, 0x66, 0x36, 0x45, 0x71, 0x64, 0x62, 0x0A, 0x44, 0x75, 0x4B, 0x50, 0x48, 0x78, 0x36, 0x47, 0x47, 0x61, 0x65, 0x71, 0x74, 0x53, 0x32, 0x35, 0x58, 0x77, 0x32, 0x4B, + 0x77, 0x71, 0x2B, 0x46, 0x4E, 0x58, 0x6B, 0x79, 0x4C, 0x62, 0x73, 0x63, 0x59, 0x6A, 0x66, 0x79, 0x73, 0x56, 0x74, 0x4B, 0x50, 0x63, 0x72, 0x4E, 0x63, 0x56, 0x2F, 0x70, 0x51, 0x72, 0x36, 0x55, 0x36, 0x4D, 0x6A, 0x65, 0x2B, 0x53, 0x4A, 0x49, + 0x5A, 0x4D, 0x62, 0x6C, 0x71, 0x38, 0x59, 0x72, 0x62, 0x61, 0x30, 0x46, 0x38, 0x50, 0x72, 0x56, 0x0A, 0x43, 0x38, 0x2B, 0x61, 0x35, 0x66, 0x42, 0x51, 0x70, 0x49, 0x73, 0x37, 0x52, 0x36, 0x55, 0x6A, 0x57, 0x33, 0x70, 0x36, 0x2B, 0x44, 0x4D, + 0x2F, 0x75, 0x4F, 0x2B, 0x5A, 0x6C, 0x2B, 0x4D, 0x67, 0x77, 0x64, 0x59, 0x6F, 0x69, 0x63, 0x2B, 0x55, 0x2B, 0x37, 0x6C, 0x46, 0x37, 0x65, 0x4E, 0x41, 0x46, 0x78, 0x48, 0x55, 0x64, 0x50, 0x41, 0x4C, 0x4D, 0x65, 0x49, 0x72, 0x4A, 0x6D, 0x71, + 0x62, 0x54, 0x46, 0x65, 0x75, 0x72, 0x43, 0x41, 0x2B, 0x75, 0x6B, 0x56, 0x36, 0x0A, 0x42, 0x66, 0x4F, 0x39, 0x6D, 0x32, 0x6B, 0x56, 0x72, 0x6E, 0x31, 0x4F, 0x49, 0x47, 0x50, 0x45, 0x4E, 0x58, 0x59, 0x36, 0x42, 0x77, 0x4C, 0x4A, 0x4E, 0x2F, + 0x33, 0x48, 0x52, 0x2B, 0x37, 0x6F, 0x38, 0x58, 0x59, 0x64, 0x63, 0x78, 0x58, 0x79, 0x6C, 0x36, 0x53, 0x31, 0x79, 0x48, 0x70, 0x35, 0x32, 0x55, 0x4B, 0x71, 0x4B, 0x33, 0x39, 0x63, 0x2F, 0x73, 0x34, 0x6D, 0x54, 0x36, 0x4E, 0x6D, 0x67, 0x54, + 0x57, 0x76, 0x52, 0x4C, 0x70, 0x55, 0x48, 0x68, 0x77, 0x77, 0x0A, 0x4D, 0x6D, 0x57, 0x64, 0x35, 0x6A, 0x79, 0x54, 0x58, 0x6C, 0x42, 0x4F, 0x65, 0x75, 0x4D, 0x36, 0x31, 0x47, 0x37, 0x4D, 0x47, 0x76, 0x76, 0x35, 0x30, 0x6A, 0x65, 0x75, 0x4A, + 0x43, 0x71, 0x72, 0x56, 0x77, 0x4D, 0x69, 0x4B, 0x41, 0x31, 0x4A, 0x64, 0x58, 0x2B, 0x33, 0x4B, 0x4E, 0x70, 0x31, 0x76, 0x34, 0x37, 0x6A, 0x33, 0x41, 0x35, 0x35, 0x4D, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, + 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x6E, 0x5A, 0x50, 0x47, 0x55, 0x34, 0x74, 0x65, 0x79, 0x71, 0x38, 0x2F, 0x6E, 0x78, 0x34, 0x50, 0x35, 0x5A, 0x6D, 0x56, 0x76, 0x43, 0x54, 0x32, + 0x6C, 0x49, 0x38, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, + 0x42, 0x41, 0x4D, 0x43, 0x0A, 0x41, 0x51, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x46, 0x69, 0x73, 0x39, 0x41, 0x51, + 0x4F, 0x7A, 0x63, 0x41, 0x4E, 0x2F, 0x77, 0x72, 0x39, 0x31, 0x4C, 0x6F, 0x57, 0x58, 0x79, 0x6D, 0x39, 0x65, 0x32, 0x69, 0x5A, 0x57, 0x45, 0x6E, 0x53, 0x74, 0x42, 0x30, 0x33, 0x54, 0x58, 0x38, 0x6E, 0x66, 0x55, 0x59, 0x47, 0x58, 0x55, 0x50, + 0x47, 0x0A, 0x68, 0x69, 0x34, 0x2B, 0x63, 0x37, 0x49, 0x6D, 0x66, 0x55, 0x2B, 0x54, 0x71, 0x62, 0x62, 0x45, 0x4B, 0x70, 0x71, 0x72, 0x49, 0x5A, 0x63, 0x55, 0x73, 0x64, 0x36, 0x4D, 0x30, 0x36, 0x75, 0x4A, 0x46, 0x64, 0x68, 0x72, 0x4A, 0x4E, + 0x54, 0x78, 0x46, 0x71, 0x37, 0x59, 0x70, 0x46, 0x7A, 0x55, 0x66, 0x31, 0x47, 0x4F, 0x37, 0x52, 0x67, 0x42, 0x73, 0x5A, 0x4E, 0x6A, 0x76, 0x62, 0x7A, 0x34, 0x59, 0x59, 0x43, 0x61, 0x6E, 0x72, 0x48, 0x4F, 0x51, 0x6E, 0x44, 0x69, 0x0A, 0x71, + 0x58, 0x30, 0x47, 0x4A, 0x58, 0x30, 0x6E, 0x6F, 0x66, 0x35, 0x76, 0x37, 0x4C, 0x4D, 0x65, 0x4A, 0x4E, 0x72, 0x6A, 0x53, 0x31, 0x55, 0x61, 0x41, 0x44, 0x73, 0x31, 0x74, 0x44, 0x76, 0x5A, 0x31, 0x31, 0x30, 0x77, 0x2F, 0x59, 0x45, 0x54, 0x69, + 0x66, 0x4C, 0x43, 0x42, 0x69, 0x76, 0x74, 0x5A, 0x38, 0x53, 0x4F, 0x79, 0x55, 0x4F, 0x79, 0x58, 0x47, 0x73, 0x56, 0x69, 0x51, 0x4B, 0x38, 0x59, 0x76, 0x78, 0x4F, 0x38, 0x72, 0x55, 0x7A, 0x71, 0x72, 0x4A, 0x76, 0x0A, 0x30, 0x77, 0x71, 0x69, + 0x55, 0x4F, 0x50, 0x32, 0x4F, 0x2B, 0x67, 0x75, 0x52, 0x4D, 0x4C, 0x62, 0x5A, 0x6A, 0x69, 0x70, 0x4D, 0x31, 0x5A, 0x49, 0x38, 0x57, 0x30, 0x62, 0x4D, 0x34, 0x30, 0x4E, 0x6A, 0x44, 0x39, 0x67, 0x4E, 0x35, 0x33, 0x54, 0x79, 0x6D, 0x31, 0x2B, + 0x4E, 0x48, 0x34, 0x4E, 0x6E, 0x33, 0x4A, 0x32, 0x69, 0x78, 0x75, 0x66, 0x63, 0x76, 0x31, 0x53, 0x4E, 0x55, 0x46, 0x46, 0x41, 0x70, 0x59, 0x76, 0x48, 0x4C, 0x4B, 0x61, 0x63, 0x30, 0x6B, 0x68, 0x0A, 0x73, 0x55, 0x6C, 0x48, 0x52, 0x55, 0x65, + 0x30, 0x37, 0x32, 0x6F, 0x30, 0x45, 0x63, 0x6C, 0x4E, 0x6D, 0x73, 0x78, 0x5A, 0x74, 0x39, 0x59, 0x43, 0x6E, 0x6C, 0x70, 0x4F, 0x5A, 0x62, 0x57, 0x55, 0x72, 0x68, 0x76, 0x66, 0x4B, 0x62, 0x41, 0x57, 0x38, 0x62, 0x38, 0x41, 0x6E, 0x67, 0x63, + 0x36, 0x46, 0x32, 0x53, 0x31, 0x42, 0x4C, 0x55, 0x6A, 0x49, 0x5A, 0x6B, 0x4B, 0x6C, 0x54, 0x75, 0x58, 0x66, 0x4F, 0x38, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x66, 0x66, 0x69, 0x72, 0x6D, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x4E, 0x65, 0x74, 0x77, 0x6F, 0x72, 0x6B, 0x69, 0x6E, 0x67, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x54, 0x44, 0x43, 0x43, 0x41, 0x6A, 0x53, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x66, 0x45, 0x38, 0x45, 0x4F, 0x52, 0x7A, 0x55, 0x6D, 0x53, 0x30, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, + 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x77, 0x52, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, 0x53, 0x0A, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x62, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x5A, 0x42, + 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x4F, 0x5A, 0x58, 0x52, 0x33, 0x62, 0x33, 0x4A, 0x72, 0x61, 0x57, 0x35, 0x6E, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x77, 0x0A, 0x4D, 0x44, 0x45, + 0x79, 0x4F, 0x54, 0x45, 0x30, 0x4D, 0x44, 0x67, 0x79, 0x4E, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x77, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x45, 0x30, 0x4D, 0x44, 0x67, 0x79, 0x4E, 0x46, 0x6F, 0x77, 0x52, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x0A, 0x62, 0x56, 0x52, 0x79, 0x64, 0x58, + 0x4E, 0x30, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x5A, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x4F, 0x5A, 0x58, 0x52, 0x33, 0x62, 0x33, + 0x4A, 0x72, 0x61, 0x57, 0x35, 0x6E, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x0A, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, + 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x74, 0x49, 0x54, 0x4D, 0x4D, 0x78, 0x63, 0x75, 0x61, 0x35, 0x52, 0x73, 0x61, 0x32, 0x46, 0x53, 0x6F, 0x4F, 0x75, 0x6A, 0x7A, 0x33, 0x6D, 0x55, 0x54, 0x4F, 0x57, 0x55, 0x67, + 0x4A, 0x6E, 0x4C, 0x56, 0x57, 0x52, 0x45, 0x5A, 0x59, 0x39, 0x6E, 0x5A, 0x4F, 0x49, 0x47, 0x34, 0x31, 0x77, 0x33, 0x53, 0x66, 0x59, 0x76, 0x6D, 0x34, 0x53, 0x45, 0x0A, 0x48, 0x69, 0x33, 0x79, 0x59, 0x4A, 0x30, 0x77, 0x54, 0x73, 0x79, 0x45, + 0x68, 0x65, 0x49, 0x73, 0x7A, 0x78, 0x36, 0x65, 0x2F, 0x6A, 0x61, 0x72, 0x4D, 0x33, 0x63, 0x31, 0x52, 0x4E, 0x67, 0x31, 0x6C, 0x68, 0x6F, 0x39, 0x4E, 0x75, 0x68, 0x36, 0x44, 0x74, 0x6A, 0x56, 0x52, 0x36, 0x46, 0x71, 0x61, 0x59, 0x76, 0x5A, + 0x2F, 0x4C, 0x73, 0x36, 0x72, 0x6E, 0x6C, 0x61, 0x31, 0x66, 0x54, 0x57, 0x63, 0x62, 0x75, 0x61, 0x6B, 0x43, 0x4E, 0x72, 0x6D, 0x72, 0x65, 0x49, 0x0A, 0x64, 0x49, 0x63, 0x4D, 0x48, 0x6C, 0x2B, 0x35, 0x6E, 0x69, 0x33, 0x36, 0x71, 0x31, 0x4D, + 0x72, 0x33, 0x4C, 0x74, 0x32, 0x50, 0x70, 0x4E, 0x4D, 0x43, 0x41, 0x69, 0x4D, 0x48, 0x71, 0x49, 0x6A, 0x48, 0x4E, 0x52, 0x71, 0x72, 0x53, 0x4B, 0x36, 0x6D, 0x51, 0x45, 0x75, 0x62, 0x57, 0x58, 0x4C, 0x76, 0x69, 0x52, 0x6D, 0x56, 0x53, 0x52, + 0x4C, 0x51, 0x45, 0x53, 0x78, 0x47, 0x39, 0x66, 0x68, 0x77, 0x6F, 0x58, 0x41, 0x33, 0x68, 0x41, 0x2F, 0x50, 0x65, 0x32, 0x34, 0x0A, 0x2F, 0x50, 0x48, 0x78, 0x49, 0x31, 0x50, 0x63, 0x76, 0x32, 0x57, 0x58, 0x62, 0x39, 0x6E, 0x35, 0x51, 0x48, + 0x47, 0x4E, 0x66, 0x62, 0x32, 0x56, 0x31, 0x4D, 0x36, 0x2B, 0x6F, 0x46, 0x34, 0x6E, 0x49, 0x39, 0x37, 0x39, 0x70, 0x74, 0x41, 0x6D, 0x44, 0x67, 0x41, 0x70, 0x36, 0x7A, 0x78, 0x47, 0x38, 0x44, 0x31, 0x67, 0x76, 0x7A, 0x39, 0x51, 0x30, 0x74, + 0x77, 0x6D, 0x51, 0x56, 0x47, 0x65, 0x46, 0x44, 0x64, 0x43, 0x42, 0x4B, 0x4E, 0x77, 0x56, 0x36, 0x67, 0x62, 0x0A, 0x68, 0x2B, 0x30, 0x74, 0x2B, 0x6E, 0x76, 0x75, 0x6A, 0x41, 0x72, 0x6A, 0x71, 0x57, 0x61, 0x4A, 0x47, 0x63, 0x74, 0x42, 0x2B, + 0x64, 0x31, 0x45, 0x4E, 0x6D, 0x48, 0x50, 0x34, 0x6E, 0x64, 0x47, 0x79, 0x48, 0x33, 0x32, 0x39, 0x4A, 0x4B, 0x42, 0x4E, 0x76, 0x33, 0x62, 0x4E, 0x50, 0x46, 0x79, 0x66, 0x76, 0x4D, 0x4D, 0x46, 0x72, 0x32, 0x30, 0x46, 0x51, 0x49, 0x44, 0x41, + 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x42, 0x78, 0x2F, 0x53, 0x35, 0x35, 0x7A, 0x61, 0x77, 0x6D, 0x36, 0x69, 0x51, 0x4C, 0x53, 0x77, + 0x65, 0x6C, 0x41, 0x51, 0x55, 0x48, 0x54, 0x45, 0x79, 0x4C, 0x30, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, + 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x0A, 0x41, 0x51, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, + 0x42, 0x41, 0x49, 0x6C, 0x58, 0x73, 0x68, 0x5A, 0x36, 0x71, 0x4D, 0x4C, 0x39, 0x31, 0x74, 0x6D, 0x62, 0x6D, 0x7A, 0x54, 0x43, 0x6E, 0x4C, 0x51, 0x79, 0x46, 0x45, 0x32, 0x6E, 0x70, 0x4E, 0x2F, 0x73, 0x76, 0x71, 0x65, 0x2B, 0x2B, 0x45, 0x50, + 0x62, 0x6B, 0x54, 0x66, 0x4F, 0x74, 0x44, 0x49, 0x75, 0x0A, 0x55, 0x46, 0x55, 0x61, 0x4E, 0x55, 0x35, 0x32, 0x51, 0x33, 0x45, 0x67, 0x37, 0x35, 0x4E, 0x33, 0x54, 0x68, 0x56, 0x77, 0x4C, 0x6F, 0x66, 0x44, 0x77, 0x52, 0x31, 0x74, 0x33, 0x4D, + 0x75, 0x31, 0x4A, 0x39, 0x51, 0x73, 0x56, 0x74, 0x46, 0x53, 0x55, 0x7A, 0x70, 0x45, 0x30, 0x6E, 0x50, 0x49, 0x78, 0x42, 0x73, 0x46, 0x5A, 0x56, 0x70, 0x69, 0x6B, 0x70, 0x7A, 0x75, 0x51, 0x59, 0x30, 0x78, 0x32, 0x2B, 0x63, 0x30, 0x36, 0x6C, + 0x6B, 0x68, 0x31, 0x51, 0x46, 0x36, 0x0A, 0x31, 0x32, 0x53, 0x34, 0x5A, 0x44, 0x6E, 0x4E, 0x79, 0x65, 0x32, 0x76, 0x37, 0x55, 0x73, 0x44, 0x53, 0x4B, 0x65, 0x67, 0x6D, 0x51, 0x47, 0x41, 0x33, 0x47, 0x57, 0x6A, 0x4E, 0x71, 0x35, 0x6C, 0x57, + 0x55, 0x68, 0x50, 0x67, 0x6B, 0x76, 0x49, 0x5A, 0x66, 0x46, 0x58, 0x48, 0x65, 0x56, 0x5A, 0x4C, 0x67, 0x6F, 0x2F, 0x62, 0x4E, 0x6A, 0x52, 0x39, 0x65, 0x55, 0x4A, 0x74, 0x47, 0x78, 0x55, 0x41, 0x41, 0x72, 0x67, 0x46, 0x55, 0x32, 0x48, 0x64, + 0x57, 0x32, 0x33, 0x0A, 0x57, 0x4A, 0x5A, 0x61, 0x33, 0x57, 0x33, 0x53, 0x41, 0x4B, 0x44, 0x30, 0x6D, 0x30, 0x69, 0x2B, 0x77, 0x7A, 0x65, 0x6B, 0x75, 0x6A, 0x62, 0x67, 0x66, 0x49, 0x65, 0x46, 0x6C, 0x78, 0x6F, 0x56, 0x6F, 0x74, 0x34, 0x75, + 0x6F, 0x6C, 0x75, 0x39, 0x72, 0x78, 0x6A, 0x35, 0x6B, 0x46, 0x44, 0x4E, 0x63, 0x46, 0x6E, 0x34, 0x4A, 0x32, 0x64, 0x48, 0x79, 0x38, 0x65, 0x67, 0x42, 0x7A, 0x70, 0x39, 0x30, 0x53, 0x78, 0x64, 0x62, 0x42, 0x6B, 0x36, 0x5A, 0x72, 0x56, 0x39, + 0x0A, 0x2F, 0x5A, 0x46, 0x76, 0x67, 0x72, 0x47, 0x2B, 0x43, 0x4A, 0x50, 0x62, 0x46, 0x45, 0x66, 0x78, 0x6F, 0x6A, 0x66, 0x48, 0x52, 0x5A, 0x34, 0x38, 0x78, 0x33, 0x65, 0x76, 0x5A, 0x4B, 0x69, 0x54, 0x33, 0x2F, 0x5A, 0x70, 0x67, 0x34, 0x4A, + 0x67, 0x38, 0x6B, 0x6C, 0x43, 0x4E, 0x4F, 0x31, 0x61, 0x41, 0x46, 0x53, 0x46, 0x48, 0x42, 0x59, 0x32, 0x6B, 0x67, 0x78, 0x63, 0x2B, 0x71, 0x61, 0x74, 0x76, 0x39, 0x73, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x66, 0x66, 0x69, 0x72, 0x6D, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x50, 0x72, 0x65, 0x6D, 0x69, 0x75, 0x6D, 0x0A, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x52, 0x6A, 0x43, 0x43, 0x41, 0x79, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x62, 0x59, 0x77, 0x55, 0x52, 0x72, 0x47, 0x6D, 0x43, 0x75, 0x34, 0x77, 0x44, 0x51, 0x59, + 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x77, 0x51, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, + 0x53, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x62, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x52, 0x77, 0x77, 0x47, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, + 0x4E, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x51, 0x63, 0x6D, 0x56, 0x74, 0x61, 0x58, 0x56, 0x74, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x77, 0x4D, 0x44, 0x45, 0x79, 0x0A, 0x4F, + 0x54, 0x45, 0x30, 0x4D, 0x54, 0x41, 0x7A, 0x4E, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x45, 0x30, 0x4D, 0x54, 0x41, 0x7A, 0x4E, 0x6C, 0x6F, 0x77, 0x51, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x62, 0x56, 0x52, 0x79, 0x0A, 0x64, 0x58, 0x4E, 0x30, + 0x4D, 0x52, 0x77, 0x77, 0x47, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x4E, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x51, 0x63, 0x6D, 0x56, 0x74, 0x61, 0x58, 0x56, 0x74, + 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, 0x41, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, + 0x43, 0x41, 0x67, 0x45, 0x41, 0x78, 0x42, 0x4C, 0x66, 0x71, 0x56, 0x2F, 0x2B, 0x51, 0x64, 0x33, 0x64, 0x39, 0x5A, 0x2B, 0x4B, 0x34, 0x2F, 0x61, 0x73, 0x34, 0x54, 0x78, 0x34, 0x6D, 0x72, 0x7A, 0x59, 0x38, 0x48, 0x39, 0x36, 0x6F, 0x44, 0x4D, + 0x71, 0x33, 0x49, 0x30, 0x67, 0x57, 0x36, 0x34, 0x74, 0x62, 0x2B, 0x65, 0x54, 0x32, 0x54, 0x5A, 0x77, 0x61, 0x6D, 0x6A, 0x50, 0x6A, 0x6C, 0x47, 0x6A, 0x68, 0x56, 0x74, 0x6E, 0x0A, 0x42, 0x4B, 0x41, 0x51, 0x4A, 0x47, 0x39, 0x64, 0x4B, 0x49, + 0x4C, 0x42, 0x6C, 0x31, 0x66, 0x59, 0x53, 0x43, 0x6B, 0x54, 0x74, 0x75, 0x47, 0x2B, 0x6B, 0x55, 0x33, 0x66, 0x68, 0x51, 0x78, 0x54, 0x47, 0x4A, 0x6F, 0x65, 0x4A, 0x4B, 0x4A, 0x50, 0x6A, 0x2F, 0x43, 0x69, 0x68, 0x51, 0x76, 0x4C, 0x39, 0x43, + 0x6C, 0x2F, 0x30, 0x71, 0x52, 0x59, 0x37, 0x69, 0x5A, 0x4E, 0x79, 0x61, 0x71, 0x6F, 0x65, 0x35, 0x72, 0x5A, 0x2B, 0x6A, 0x6A, 0x65, 0x52, 0x46, 0x63, 0x56, 0x0A, 0x35, 0x66, 0x69, 0x4D, 0x79, 0x4E, 0x6C, 0x49, 0x34, 0x67, 0x30, 0x57, 0x4A, + 0x78, 0x30, 0x65, 0x79, 0x49, 0x4F, 0x46, 0x4A, 0x62, 0x65, 0x36, 0x71, 0x6C, 0x56, 0x42, 0x7A, 0x41, 0x4D, 0x69, 0x53, 0x79, 0x32, 0x52, 0x6A, 0x59, 0x76, 0x6D, 0x69, 0x61, 0x39, 0x6D, 0x78, 0x2B, 0x6E, 0x2F, 0x4B, 0x2B, 0x6B, 0x38, 0x72, + 0x4E, 0x72, 0x53, 0x73, 0x38, 0x50, 0x68, 0x61, 0x4A, 0x79, 0x4A, 0x2B, 0x48, 0x6F, 0x41, 0x56, 0x74, 0x37, 0x30, 0x56, 0x5A, 0x56, 0x73, 0x0A, 0x2B, 0x37, 0x70, 0x6B, 0x33, 0x57, 0x4B, 0x4C, 0x33, 0x77, 0x74, 0x33, 0x4D, 0x75, 0x74, 0x69, + 0x7A, 0x43, 0x61, 0x61, 0x6D, 0x37, 0x75, 0x71, 0x59, 0x6F, 0x4E, 0x4D, 0x74, 0x41, 0x5A, 0x36, 0x4D, 0x4D, 0x67, 0x70, 0x76, 0x2B, 0x30, 0x47, 0x54, 0x5A, 0x65, 0x35, 0x48, 0x4D, 0x51, 0x78, 0x4B, 0x39, 0x56, 0x66, 0x76, 0x46, 0x4D, 0x53, + 0x46, 0x35, 0x79, 0x5A, 0x56, 0x79, 0x6C, 0x6D, 0x64, 0x32, 0x45, 0x68, 0x4D, 0x51, 0x63, 0x75, 0x4A, 0x55, 0x6D, 0x64, 0x0A, 0x47, 0x50, 0x4C, 0x75, 0x38, 0x79, 0x74, 0x78, 0x6A, 0x4C, 0x57, 0x36, 0x4F, 0x51, 0x64, 0x4A, 0x64, 0x2F, 0x7A, + 0x76, 0x4C, 0x70, 0x4B, 0x51, 0x42, 0x59, 0x30, 0x74, 0x4C, 0x33, 0x64, 0x37, 0x37, 0x30, 0x4F, 0x2F, 0x4E, 0x62, 0x75, 0x61, 0x32, 0x50, 0x6C, 0x7A, 0x70, 0x79, 0x7A, 0x79, 0x30, 0x46, 0x66, 0x75, 0x4B, 0x45, 0x34, 0x6D, 0x58, 0x34, 0x2B, + 0x51, 0x61, 0x41, 0x6B, 0x76, 0x75, 0x50, 0x6A, 0x63, 0x42, 0x75, 0x6B, 0x75, 0x6D, 0x6A, 0x35, 0x52, 0x0A, 0x70, 0x39, 0x45, 0x69, 0x78, 0x41, 0x71, 0x6E, 0x4F, 0x45, 0x68, 0x73, 0x73, 0x2F, 0x6E, 0x2F, 0x66, 0x61, 0x75, 0x47, 0x56, 0x2B, + 0x4F, 0x36, 0x31, 0x6F, 0x56, 0x34, 0x64, 0x37, 0x70, 0x44, 0x36, 0x6B, 0x68, 0x2F, 0x39, 0x74, 0x69, 0x2B, 0x49, 0x32, 0x30, 0x65, 0x76, 0x39, 0x45, 0x32, 0x62, 0x46, 0x68, 0x63, 0x38, 0x65, 0x36, 0x6B, 0x47, 0x56, 0x51, 0x61, 0x39, 0x51, + 0x50, 0x53, 0x64, 0x75, 0x62, 0x68, 0x6A, 0x4C, 0x30, 0x38, 0x73, 0x39, 0x4E, 0x49, 0x0A, 0x53, 0x2B, 0x4C, 0x49, 0x2B, 0x48, 0x2B, 0x53, 0x71, 0x48, 0x5A, 0x47, 0x6E, 0x45, 0x4A, 0x6C, 0x50, 0x71, 0x51, 0x65, 0x77, 0x51, 0x63, 0x44, 0x57, + 0x6B, 0x59, 0x74, 0x75, 0x4A, 0x66, 0x7A, 0x74, 0x39, 0x57, 0x79, 0x56, 0x53, 0x48, 0x76, 0x75, 0x74, 0x78, 0x4D, 0x41, 0x4A, 0x66, 0x37, 0x46, 0x4A, 0x55, 0x6E, 0x4D, 0x37, 0x2F, 0x6F, 0x51, 0x30, 0x64, 0x47, 0x30, 0x67, 0x69, 0x5A, 0x46, + 0x6D, 0x41, 0x37, 0x6D, 0x6E, 0x37, 0x53, 0x35, 0x75, 0x30, 0x34, 0x0A, 0x36, 0x75, 0x77, 0x42, 0x48, 0x6A, 0x78, 0x49, 0x56, 0x6B, 0x6B, 0x4A, 0x78, 0x30, 0x77, 0x33, 0x41, 0x4A, 0x36, 0x49, 0x44, 0x73, 0x42, 0x7A, 0x34, 0x57, 0x39, 0x6D, + 0x36, 0x58, 0x4A, 0x48, 0x4D, 0x44, 0x34, 0x51, 0x35, 0x51, 0x73, 0x44, 0x79, 0x5A, 0x70, 0x43, 0x41, 0x47, 0x7A, 0x46, 0x6C, 0x48, 0x35, 0x68, 0x78, 0x49, 0x72, 0x66, 0x66, 0x34, 0x49, 0x61, 0x43, 0x31, 0x6E, 0x45, 0x57, 0x54, 0x4A, 0x33, + 0x73, 0x37, 0x78, 0x67, 0x61, 0x56, 0x59, 0x35, 0x0A, 0x2F, 0x62, 0x51, 0x47, 0x65, 0x79, 0x7A, 0x57, 0x5A, 0x44, 0x62, 0x5A, 0x76, 0x55, 0x6A, 0x74, 0x68, 0x42, 0x39, 0x2B, 0x70, 0x53, 0x4B, 0x50, 0x4B, 0x72, 0x68, 0x43, 0x39, 0x49, 0x4B, + 0x33, 0x31, 0x46, 0x4F, 0x51, 0x65, 0x45, 0x34, 0x74, 0x47, 0x76, 0x32, 0x42, 0x62, 0x30, 0x54, 0x58, 0x4F, 0x77, 0x46, 0x30, 0x6C, 0x6B, 0x4C, 0x67, 0x41, 0x4F, 0x49, 0x75, 0x61, 0x2B, 0x72, 0x46, 0x37, 0x6E, 0x4B, 0x73, 0x75, 0x37, 0x2F, + 0x2B, 0x36, 0x71, 0x71, 0x6F, 0x0A, 0x2B, 0x4E, 0x7A, 0x32, 0x73, 0x6E, 0x6D, 0x4B, 0x74, 0x6D, 0x63, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, + 0x59, 0x45, 0x46, 0x4A, 0x33, 0x41, 0x5A, 0x36, 0x59, 0x4D, 0x49, 0x74, 0x6B, 0x6D, 0x39, 0x55, 0x57, 0x72, 0x70, 0x6D, 0x56, 0x53, 0x45, 0x53, 0x66, 0x59, 0x52, 0x61, 0x78, 0x6A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, + 0x45, 0x42, 0x0A, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x43, + 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x44, 0x41, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x43, 0x7A, 0x56, 0x30, 0x30, 0x51, 0x59, 0x6B, 0x34, 0x36, 0x35, 0x4B, 0x7A, 0x71, 0x75, 0x42, 0x79, 0x76, 0x0A, + 0x4D, 0x69, 0x50, 0x49, 0x73, 0x30, 0x6C, 0x61, 0x55, 0x5A, 0x78, 0x32, 0x4B, 0x49, 0x31, 0x35, 0x71, 0x6C, 0x64, 0x47, 0x46, 0x39, 0x58, 0x31, 0x55, 0x76, 0x61, 0x33, 0x52, 0x4F, 0x67, 0x49, 0x52, 0x4C, 0x38, 0x59, 0x68, 0x4E, 0x49, 0x4C, + 0x67, 0x4D, 0x33, 0x46, 0x45, 0x76, 0x30, 0x41, 0x56, 0x51, 0x56, 0x68, 0x68, 0x30, 0x48, 0x63, 0x74, 0x53, 0x53, 0x65, 0x50, 0x4D, 0x54, 0x59, 0x79, 0x50, 0x74, 0x77, 0x6E, 0x69, 0x39, 0x34, 0x6C, 0x6F, 0x4D, 0x67, 0x0A, 0x4E, 0x74, 0x35, + 0x38, 0x44, 0x32, 0x6B, 0x54, 0x69, 0x4B, 0x56, 0x31, 0x4E, 0x70, 0x67, 0x49, 0x70, 0x73, 0x62, 0x66, 0x72, 0x4D, 0x37, 0x6A, 0x57, 0x4E, 0x61, 0x33, 0x50, 0x74, 0x36, 0x36, 0x38, 0x2B, 0x73, 0x30, 0x51, 0x4E, 0x69, 0x69, 0x67, 0x66, 0x56, + 0x34, 0x50, 0x79, 0x2F, 0x56, 0x70, 0x66, 0x7A, 0x5A, 0x6F, 0x74, 0x52, 0x65, 0x42, 0x41, 0x34, 0x58, 0x72, 0x66, 0x35, 0x42, 0x38, 0x4F, 0x57, 0x79, 0x63, 0x76, 0x70, 0x45, 0x67, 0x6A, 0x4E, 0x43, 0x0A, 0x36, 0x43, 0x31, 0x59, 0x39, 0x31, + 0x61, 0x4D, 0x59, 0x6A, 0x2B, 0x36, 0x51, 0x72, 0x43, 0x63, 0x44, 0x46, 0x78, 0x2B, 0x4C, 0x6D, 0x55, 0x6D, 0x58, 0x46, 0x4E, 0x50, 0x41, 0x4C, 0x4A, 0x34, 0x66, 0x71, 0x45, 0x4E, 0x6D, 0x53, 0x32, 0x4E, 0x75, 0x42, 0x32, 0x4F, 0x6F, 0x73, + 0x53, 0x77, 0x2F, 0x57, 0x44, 0x51, 0x4D, 0x4B, 0x53, 0x4F, 0x79, 0x41, 0x52, 0x69, 0x71, 0x63, 0x54, 0x74, 0x4E, 0x64, 0x35, 0x36, 0x6C, 0x2B, 0x30, 0x4F, 0x4F, 0x46, 0x36, 0x53, 0x0A, 0x4C, 0x35, 0x4E, 0x77, 0x70, 0x61, 0x6D, 0x63, 0x62, + 0x36, 0x64, 0x39, 0x45, 0x78, 0x31, 0x2B, 0x78, 0x67, 0x68, 0x49, 0x73, 0x56, 0x35, 0x6E, 0x36, 0x31, 0x45, 0x49, 0x4A, 0x65, 0x6E, 0x6D, 0x4A, 0x57, 0x74, 0x53, 0x4B, 0x5A, 0x47, 0x63, 0x30, 0x6A, 0x6C, 0x7A, 0x43, 0x46, 0x66, 0x65, 0x6D, + 0x51, 0x61, 0x30, 0x57, 0x35, 0x30, 0x51, 0x42, 0x75, 0x48, 0x43, 0x41, 0x4B, 0x69, 0x34, 0x48, 0x45, 0x6F, 0x43, 0x43, 0x68, 0x54, 0x51, 0x77, 0x55, 0x48, 0x4B, 0x0A, 0x2B, 0x34, 0x77, 0x31, 0x49, 0x58, 0x32, 0x43, 0x4F, 0x50, 0x4B, 0x70, + 0x56, 0x4A, 0x45, 0x5A, 0x4E, 0x5A, 0x4F, 0x55, 0x62, 0x57, 0x6F, 0x36, 0x78, 0x62, 0x4C, 0x51, 0x75, 0x34, 0x6D, 0x47, 0x6B, 0x2B, 0x69, 0x62, 0x79, 0x51, 0x38, 0x36, 0x70, 0x33, 0x71, 0x34, 0x6F, 0x66, 0x42, 0x34, 0x52, 0x76, 0x72, 0x38, + 0x4E, 0x79, 0x2F, 0x6C, 0x69, 0x6F, 0x54, 0x7A, 0x33, 0x2F, 0x34, 0x45, 0x32, 0x61, 0x46, 0x6F, 0x6F, 0x43, 0x38, 0x6B, 0x34, 0x67, 0x6D, 0x56, 0x0A, 0x42, 0x74, 0x57, 0x56, 0x79, 0x75, 0x45, 0x6B, 0x6C, 0x75, 0x74, 0x38, 0x39, 0x70, 0x4D, + 0x46, 0x75, 0x2B, 0x31, 0x7A, 0x36, 0x53, 0x33, 0x52, 0x64, 0x54, 0x6E, 0x58, 0x35, 0x79, 0x54, 0x62, 0x32, 0x45, 0x35, 0x66, 0x51, 0x34, 0x2B, 0x65, 0x30, 0x42, 0x51, 0x35, 0x76, 0x31, 0x56, 0x77, 0x53, 0x4A, 0x6C, 0x58, 0x4D, 0x62, 0x53, + 0x63, 0x37, 0x6B, 0x71, 0x59, 0x41, 0x35, 0x59, 0x77, 0x48, 0x32, 0x41, 0x47, 0x37, 0x68, 0x73, 0x6A, 0x2F, 0x6F, 0x46, 0x67, 0x0A, 0x49, 0x78, 0x70, 0x48, 0x59, 0x6F, 0x57, 0x6C, 0x7A, 0x42, 0x6B, 0x30, 0x67, 0x47, 0x2B, 0x7A, 0x72, 0x42, + 0x72, 0x6A, 0x6E, 0x2F, 0x42, 0x37, 0x53, 0x4B, 0x33, 0x56, 0x41, 0x64, 0x6C, 0x6E, 0x74, 0x71, 0x6C, 0x79, 0x6B, 0x2B, 0x6F, 0x74, 0x5A, 0x72, 0x57, 0x79, 0x75, 0x4F, 0x51, 0x39, 0x50, 0x4C, 0x4C, 0x76, 0x54, 0x49, 0x7A, 0x71, 0x36, 0x77, + 0x65, 0x2F, 0x71, 0x7A, 0x57, 0x61, 0x56, 0x59, 0x61, 0x38, 0x47, 0x4B, 0x61, 0x31, 0x71, 0x46, 0x36, 0x30, 0x0A, 0x67, 0x32, 0x78, 0x72, 0x61, 0x55, 0x44, 0x54, 0x6E, 0x39, 0x7A, 0x78, 0x77, 0x32, 0x6C, 0x72, 0x75, 0x65, 0x46, 0x74, 0x43, + 0x66, 0x54, 0x78, 0x71, 0x6C, 0x42, 0x32, 0x43, 0x6E, 0x70, 0x39, 0x65, 0x68, 0x65, 0x68, 0x56, 0x5A, 0x5A, 0x43, 0x6D, 0x54, 0x45, 0x4A, 0x33, 0x57, 0x41, 0x52, 0x6A, 0x51, 0x55, 0x77, 0x66, 0x75, 0x61, 0x4F, 0x52, 0x74, 0x47, 0x64, 0x46, + 0x4E, 0x72, 0x48, 0x46, 0x2B, 0x51, 0x46, 0x6C, 0x6F, 0x7A, 0x45, 0x4A, 0x4C, 0x55, 0x62, 0x0A, 0x7A, 0x78, 0x51, 0x48, 0x73, 0x6B, 0x44, 0x34, 0x6F, 0x35, 0x35, 0x42, 0x68, 0x72, 0x77, 0x45, 0x30, 0x47, 0x75, 0x57, 0x79, 0x43, 0x71, 0x41, + 0x4E, 0x50, 0x32, 0x2F, 0x37, 0x77, 0x61, 0x6A, 0x33, 0x56, 0x6A, 0x46, 0x68, 0x54, 0x30, 0x2B, 0x6A, 0x2F, 0x36, 0x65, 0x4B, 0x65, 0x43, 0x32, 0x75, 0x41, 0x6C, 0x6F, 0x47, 0x52, 0x77, 0x59, 0x51, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x66, 0x66, 0x69, 0x72, 0x6D, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x50, 0x72, 0x65, 0x6D, + 0x69, 0x75, 0x6D, 0x20, 0x45, 0x43, 0x43, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, + 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x42, 0x2F, 0x6A, 0x43, 0x43, 0x41, 0x59, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, + 0x64, 0x4A, 0x63, 0x6C, 0x69, 0x73, 0x63, 0x2F, 0x65, 0x6C, 0x51, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x77, 0x52, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x30, 0x46, 0x6D, 0x5A, 0x6D, 0x6C, 0x79, 0x62, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x53, 0x41, + 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x64, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x51, 0x63, 0x6D, 0x56, 0x74, 0x61, 0x58, 0x56, 0x74, 0x49, 0x45, 0x56, + 0x44, 0x51, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x44, 0x41, 0x78, 0x0A, 0x4D, 0x6A, 0x6B, 0x78, 0x4E, 0x44, 0x49, 0x77, 0x4D, 0x6A, 0x52, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x44, 0x45, 0x79, 0x4D, 0x7A, 0x45, 0x78, 0x4E, 0x44, + 0x49, 0x77, 0x4D, 0x6A, 0x52, 0x61, 0x4D, 0x45, 0x55, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x51, 0x77, 0x45, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, + 0x74, 0x42, 0x5A, 0x6D, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x55, 0x0A, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x44, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x58, 0x51, 0x57, 0x5A, 0x6D, 0x61, 0x58, 0x4A, 0x74, 0x56, + 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x48, 0x4A, 0x6C, 0x62, 0x57, 0x6C, 0x31, 0x62, 0x53, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, + 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x0A, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x51, 0x4E, 0x4D, 0x46, 0x34, 0x62, 0x46, 0x5A, 0x30, 0x44, 0x30, 0x4B, 0x46, 0x35, 0x4E, 0x62, 0x63, 0x36, 0x50, 0x4A, 0x4A, 0x36, 0x79, 0x68, 0x55, 0x63, + 0x7A, 0x57, 0x4C, 0x7A, 0x6E, 0x43, 0x5A, 0x63, 0x42, 0x7A, 0x33, 0x6C, 0x56, 0x50, 0x71, 0x6A, 0x31, 0x73, 0x77, 0x53, 0x36, 0x76, 0x51, 0x55, 0x58, 0x2B, 0x69, 0x4F, 0x47, 0x61, 0x73, 0x76, 0x4C, 0x6B, 0x6A, 0x6D, 0x72, 0x42, 0x68, 0x44, + 0x65, 0x4B, 0x7A, 0x51, 0x0A, 0x4E, 0x38, 0x4F, 0x39, 0x73, 0x73, 0x30, 0x73, 0x35, 0x6B, 0x66, 0x69, 0x47, 0x75, 0x5A, 0x6A, 0x75, 0x44, 0x30, 0x75, 0x4C, 0x33, 0x6A, 0x45, 0x54, 0x39, 0x76, 0x30, 0x44, 0x36, 0x52, 0x6F, 0x54, 0x46, 0x56, + 0x79, 0x61, 0x35, 0x55, 0x64, 0x54, 0x68, 0x68, 0x43, 0x6C, 0x58, 0x6A, 0x4D, 0x4E, 0x7A, 0x79, 0x52, 0x34, 0x70, 0x74, 0x6C, 0x4B, 0x79, 0x6D, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, + 0x57, 0x0A, 0x42, 0x42, 0x53, 0x61, 0x72, 0x79, 0x6C, 0x36, 0x77, 0x42, 0x45, 0x31, 0x4E, 0x53, 0x5A, 0x52, 0x4D, 0x41, 0x44, 0x44, 0x61, 0x76, 0x35, 0x41, 0x31, 0x61, 0x37, 0x57, 0x50, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, + 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4B, 0x0A, 0x42, + 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6E, 0x41, 0x44, 0x42, 0x6B, 0x41, 0x6A, 0x41, 0x58, 0x43, 0x66, 0x4F, 0x48, 0x69, 0x46, 0x42, 0x61, 0x72, 0x38, 0x6A, 0x41, 0x51, 0x72, 0x39, 0x48, 0x58, + 0x2F, 0x56, 0x73, 0x61, 0x6F, 0x62, 0x67, 0x78, 0x43, 0x64, 0x30, 0x35, 0x44, 0x68, 0x54, 0x31, 0x77, 0x56, 0x2F, 0x47, 0x7A, 0x54, 0x6A, 0x78, 0x69, 0x2B, 0x7A, 0x79, 0x67, 0x6B, 0x38, 0x4E, 0x35, 0x33, 0x58, 0x0A, 0x35, 0x37, 0x68, 0x47, + 0x38, 0x66, 0x32, 0x68, 0x34, 0x6E, 0x45, 0x43, 0x4D, 0x45, 0x4A, 0x5A, 0x68, 0x30, 0x50, 0x55, 0x55, 0x64, 0x2B, 0x36, 0x30, 0x77, 0x6B, 0x79, 0x57, 0x73, 0x36, 0x49, 0x66, 0x6C, 0x63, 0x39, 0x6E, 0x46, 0x39, 0x43, 0x61, 0x2F, 0x55, 0x48, + 0x4C, 0x62, 0x58, 0x77, 0x67, 0x70, 0x50, 0x35, 0x57, 0x57, 0x2B, 0x75, 0x5A, 0x50, 0x70, 0x59, 0x35, 0x59, 0x73, 0x65, 0x34, 0x32, 0x4F, 0x2B, 0x74, 0x59, 0x48, 0x4E, 0x62, 0x77, 0x4B, 0x4D, 0x0A, 0x65, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x75, 0x6D, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x20, + 0x4E, 0x65, 0x74, 0x77, 0x6F, 0x72, 0x6B, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x75, 0x7A, 0x43, 0x43, 0x41, 0x71, 0x4F, 0x67, 0x41, 0x77, 0x49, + 0x42, 0x41, 0x67, 0x49, 0x44, 0x42, 0x45, 0x54, 0x41, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x42, 0x51, 0x55, 0x41, 0x4D, 0x48, 0x34, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x42, 0x4D, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x45, 0x78, 0x6C, 0x56, 0x62, 0x6D, 0x6C, 0x36, 0x5A, 0x58, 0x52, 0x76, 0x49, 0x46, 0x52, 0x6C, 0x59, 0x32, + 0x68, 0x75, 0x62, 0x32, 0x78, 0x76, 0x5A, 0x32, 0x6C, 0x6C, 0x63, 0x79, 0x42, 0x54, 0x4C, 0x6B, 0x45, 0x75, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x35, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, + 0x30, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x0A, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x49, 0x6A, 0x41, 0x67, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x47, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x31, 0x62, 0x53, 0x42, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x47, 0x56, 0x6B, 0x49, 0x45, 0x35, 0x6C, 0x64, 0x48, 0x64, 0x76, 0x63, 0x6D, 0x73, 0x67, 0x51, + 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x67, 0x78, 0x4D, 0x44, 0x49, 0x79, 0x0A, 0x4D, 0x54, 0x49, 0x77, 0x4E, 0x7A, 0x4D, 0x33, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x6B, 0x78, 0x4D, 0x6A, 0x4D, 0x78, 0x4D, 0x54, 0x49, 0x77, + 0x4E, 0x7A, 0x4D, 0x33, 0x57, 0x6A, 0x42, 0x2B, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x51, 0x54, 0x44, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, + 0x56, 0x57, 0x35, 0x70, 0x65, 0x6D, 0x56, 0x30, 0x62, 0x79, 0x42, 0x55, 0x0A, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x67, 0x55, 0x79, 0x35, 0x42, 0x4C, 0x6A, 0x45, 0x6E, 0x4D, 0x43, 0x55, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x65, 0x51, 0x32, 0x56, 0x79, 0x64, 0x48, 0x56, 0x74, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, + 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x0A, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, 0x30, 0x67, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, + 0x52, 0x6C, 0x5A, 0x43, 0x42, 0x4F, 0x5A, 0x58, 0x52, 0x33, 0x62, 0x33, 0x4A, 0x72, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, + 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x0A, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x34, 0x2F, 0x74, 0x39, 0x6F, 0x33, 0x4B, 0x36, 0x77, 0x76, 0x44, 0x4A, 0x46, 0x49, 0x66, 0x31, 0x61, + 0x77, 0x46, 0x4F, 0x34, 0x57, 0x35, 0x41, 0x42, 0x37, 0x70, 0x74, 0x4A, 0x31, 0x31, 0x2F, 0x39, 0x31, 0x73, 0x74, 0x73, 0x31, 0x72, 0x48, 0x55, 0x56, 0x2B, 0x72, 0x70, 0x44, 0x4B, 0x6D, 0x59, 0x59, 0x65, 0x32, 0x62, 0x67, 0x2B, 0x47, 0x30, + 0x6A, 0x41, 0x43, 0x0A, 0x6C, 0x2F, 0x6A, 0x58, 0x61, 0x56, 0x65, 0x68, 0x47, 0x44, 0x6C, 0x64, 0x61, 0x6D, 0x52, 0x35, 0x78, 0x67, 0x46, 0x5A, 0x72, 0x44, 0x77, 0x78, 0x53, 0x6A, 0x68, 0x38, 0x30, 0x67, 0x54, 0x53, 0x53, 0x79, 0x6A, 0x6F, + 0x49, 0x46, 0x38, 0x37, 0x42, 0x36, 0x4C, 0x4D, 0x54, 0x58, 0x50, 0x62, 0x38, 0x36, 0x35, 0x50, 0x78, 0x31, 0x62, 0x56, 0x57, 0x71, 0x65, 0x57, 0x69, 0x66, 0x72, 0x7A, 0x71, 0x32, 0x6A, 0x55, 0x49, 0x34, 0x5A, 0x5A, 0x4A, 0x38, 0x38, 0x4A, + 0x0A, 0x4A, 0x37, 0x79, 0x73, 0x62, 0x6E, 0x4B, 0x44, 0x48, 0x44, 0x42, 0x79, 0x33, 0x2B, 0x43, 0x69, 0x36, 0x64, 0x4C, 0x68, 0x64, 0x48, 0x55, 0x5A, 0x76, 0x53, 0x71, 0x65, 0x65, 0x78, 0x56, 0x55, 0x42, 0x42, 0x76, 0x58, 0x51, 0x7A, 0x6D, + 0x74, 0x56, 0x53, 0x6A, 0x46, 0x34, 0x68, 0x71, 0x37, 0x39, 0x4D, 0x44, 0x6B, 0x72, 0x6A, 0x68, 0x4A, 0x4D, 0x38, 0x78, 0x32, 0x68, 0x5A, 0x38, 0x35, 0x52, 0x64, 0x4B, 0x6B, 0x6E, 0x76, 0x49, 0x53, 0x6A, 0x46, 0x48, 0x34, 0x0A, 0x66, 0x4F, + 0x51, 0x74, 0x66, 0x2F, 0x57, 0x73, 0x58, 0x2B, 0x73, 0x57, 0x6E, 0x37, 0x45, 0x74, 0x30, 0x62, 0x72, 0x4D, 0x6B, 0x55, 0x4A, 0x33, 0x54, 0x43, 0x58, 0x4A, 0x6B, 0x44, 0x68, 0x76, 0x32, 0x2F, 0x44, 0x4D, 0x2B, 0x34, 0x34, 0x65, 0x6C, 0x31, + 0x6B, 0x2B, 0x31, 0x57, 0x42, 0x4F, 0x35, 0x67, 0x55, 0x6F, 0x37, 0x55, 0x6C, 0x35, 0x45, 0x30, 0x75, 0x36, 0x53, 0x4E, 0x73, 0x76, 0x2B, 0x58, 0x4C, 0x54, 0x4F, 0x63, 0x72, 0x2B, 0x48, 0x39, 0x67, 0x30, 0x0A, 0x63, 0x76, 0x57, 0x30, 0x51, + 0x4D, 0x38, 0x78, 0x41, 0x63, 0x50, 0x73, 0x33, 0x68, 0x45, 0x74, 0x46, 0x31, 0x30, 0x66, 0x75, 0x46, 0x44, 0x52, 0x58, 0x68, 0x6D, 0x6E, 0x61, 0x64, 0x34, 0x48, 0x4D, 0x79, 0x6A, 0x4B, 0x55, 0x4A, 0x58, 0x35, 0x70, 0x31, 0x54, 0x4C, 0x56, + 0x49, 0x5A, 0x51, 0x52, 0x61, 0x6E, 0x35, 0x53, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x0A, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, + 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x51, 0x49, 0x64, 0x73, 0x33, 0x4C, 0x42, 0x2F, 0x38, 0x6B, 0x39, 0x73, 0x58, 0x4E, 0x37, 0x62, 0x75, 0x51, 0x76, 0x4F, 0x4B, 0x45, + 0x4E, 0x30, 0x5A, 0x31, 0x39, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, + 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4B, 0x61, 0x6F, 0x72, 0x53, 0x4C, 0x4F, 0x41, 0x54, 0x32, 0x6D, 0x6F, 0x2F, 0x39, 0x69, 0x30, 0x45, 0x69, 0x64, 0x69, 0x31, 0x35, 0x79, 0x73, 0x48, 0x68, + 0x45, 0x34, 0x39, 0x77, 0x63, 0x72, 0x77, 0x6E, 0x39, 0x49, 0x30, 0x6A, 0x36, 0x76, 0x53, 0x72, 0x45, 0x75, 0x56, 0x55, 0x45, 0x74, 0x52, 0x43, 0x6A, 0x0A, 0x6A, 0x53, 0x66, 0x65, 0x43, 0x34, 0x4A, 0x6A, 0x30, 0x4F, 0x37, 0x65, 0x44, 0x44, + 0x64, 0x35, 0x51, 0x56, 0x73, 0x69, 0x73, 0x72, 0x43, 0x61, 0x51, 0x56, 0x79, 0x6D, 0x63, 0x4F, 0x44, 0x55, 0x30, 0x48, 0x66, 0x4C, 0x49, 0x39, 0x4D, 0x41, 0x34, 0x47, 0x78, 0x57, 0x4C, 0x2B, 0x46, 0x70, 0x44, 0x51, 0x33, 0x5A, 0x71, 0x72, + 0x38, 0x68, 0x67, 0x56, 0x44, 0x5A, 0x42, 0x71, 0x57, 0x6F, 0x2F, 0x35, 0x55, 0x33, 0x30, 0x4B, 0x72, 0x2B, 0x34, 0x72, 0x50, 0x31, 0x0A, 0x6D, 0x53, 0x31, 0x46, 0x68, 0x49, 0x72, 0x6C, 0x51, 0x67, 0x6E, 0x58, 0x64, 0x41, 0x49, 0x76, 0x39, + 0x34, 0x6E, 0x59, 0x6D, 0x65, 0x6D, 0x38, 0x4A, 0x39, 0x52, 0x48, 0x6A, 0x62, 0x6F, 0x4E, 0x52, 0x68, 0x78, 0x33, 0x7A, 0x78, 0x53, 0x6B, 0x48, 0x4C, 0x6D, 0x6B, 0x4D, 0x63, 0x53, 0x63, 0x4B, 0x48, 0x51, 0x44, 0x4E, 0x50, 0x38, 0x7A, 0x47, + 0x53, 0x61, 0x6C, 0x36, 0x51, 0x31, 0x30, 0x74, 0x7A, 0x36, 0x58, 0x78, 0x6E, 0x62, 0x6F, 0x4A, 0x35, 0x61, 0x6A, 0x0A, 0x5A, 0x74, 0x33, 0x68, 0x72, 0x76, 0x4A, 0x42, 0x57, 0x38, 0x71, 0x59, 0x56, 0x6F, 0x4E, 0x7A, 0x63, 0x4F, 0x53, 0x47, + 0x47, 0x74, 0x49, 0x78, 0x51, 0x62, 0x6F, 0x76, 0x76, 0x69, 0x30, 0x54, 0x57, 0x6E, 0x5A, 0x76, 0x54, 0x75, 0x68, 0x4F, 0x67, 0x51, 0x34, 0x2F, 0x57, 0x77, 0x4D, 0x69, 0x6F, 0x42, 0x4B, 0x2B, 0x5A, 0x6C, 0x67, 0x52, 0x53, 0x73, 0x73, 0x44, + 0x78, 0x4C, 0x51, 0x71, 0x4B, 0x69, 0x32, 0x57, 0x46, 0x2B, 0x41, 0x35, 0x56, 0x4C, 0x78, 0x49, 0x0A, 0x30, 0x33, 0x59, 0x6E, 0x6E, 0x5A, 0x6F, 0x74, 0x42, 0x71, 0x62, 0x4A, 0x37, 0x44, 0x6E, 0x53, 0x71, 0x39, 0x75, 0x66, 0x6D, 0x67, 0x73, + 0x6E, 0x41, 0x6A, 0x55, 0x70, 0x73, 0x55, 0x43, 0x56, 0x35, 0x2F, 0x6E, 0x6F, 0x6E, 0x46, 0x57, 0x49, 0x47, 0x55, 0x62, 0x57, 0x74, 0x7A, 0x54, 0x31, 0x66, 0x73, 0x34, 0x35, 0x6D, 0x74, 0x6B, 0x34, 0x38, 0x56, 0x48, 0x33, 0x54, 0x79, 0x77, + 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x57, 0x43, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x65, 0x7A, 0x43, 0x43, 0x41, 0x6D, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, + 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x44, 0x42, 0x66, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x55, 0x56, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, + 0x4A, 0x0A, 0x56, 0x45, 0x46, 0x4A, 0x56, 0x30, 0x46, 0x4F, 0x4C, 0x55, 0x4E, 0x42, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x44, 0x41, 0x64, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x53, + 0x6F, 0x77, 0x4B, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x46, 0x55, 0x56, 0x30, 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x0A, 0x64, + 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x67, 0x77, 0x4F, 0x44, 0x49, 0x34, 0x4D, 0x44, 0x63, 0x79, 0x4E, 0x44, 0x4D, 0x7A, 0x57, + 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x41, 0x78, 0x4D, 0x6A, 0x4D, 0x78, 0x4D, 0x54, 0x55, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x42, 0x66, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x55, + 0x56, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x4A, 0x56, 0x45, 0x46, 0x4A, 0x56, 0x30, 0x46, 0x4F, 0x4C, 0x55, 0x4E, 0x42, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, + 0x44, 0x41, 0x64, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x53, 0x6F, 0x77, 0x4B, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x46, 0x55, 0x56, 0x30, 0x4E, 0x42, 0x0A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, + 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, + 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x0A, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x77, 0x66, 0x6E, + 0x4B, 0x34, 0x70, 0x41, 0x4F, 0x55, 0x35, 0x71, 0x66, 0x65, 0x43, 0x54, 0x69, 0x52, 0x53, 0x68, 0x46, 0x41, 0x68, 0x36, 0x64, 0x38, 0x57, 0x57, 0x51, 0x55, 0x65, 0x37, 0x55, 0x52, 0x45, 0x4E, 0x33, 0x2B, 0x76, 0x39, 0x58, 0x41, 0x75, 0x31, + 0x62, 0x69, 0x68, 0x53, 0x58, 0x30, 0x4E, 0x58, 0x49, 0x50, 0x2B, 0x46, 0x50, 0x51, 0x51, 0x65, 0x46, 0x45, 0x41, 0x63, 0x4B, 0x30, 0x48, 0x4D, 0x4D, 0x78, 0x0A, 0x51, 0x68, 0x5A, 0x48, 0x68, 0x54, 0x4D, 0x69, 0x64, 0x72, 0x49, 0x4B, 0x62, + 0x77, 0x2F, 0x6C, 0x4A, 0x56, 0x42, 0x50, 0x68, 0x59, 0x61, 0x2B, 0x76, 0x35, 0x67, 0x75, 0x45, 0x47, 0x63, 0x65, 0x76, 0x68, 0x45, 0x46, 0x68, 0x67, 0x57, 0x51, 0x78, 0x46, 0x6E, 0x51, 0x66, 0x48, 0x67, 0x51, 0x73, 0x49, 0x42, 0x63, 0x74, + 0x2B, 0x48, 0x48, 0x4B, 0x33, 0x58, 0x4C, 0x66, 0x4A, 0x2B, 0x75, 0x74, 0x64, 0x47, 0x64, 0x49, 0x7A, 0x64, 0x6A, 0x70, 0x39, 0x78, 0x43, 0x0A, 0x6F, 0x69, 0x32, 0x53, 0x42, 0x42, 0x74, 0x51, 0x77, 0x58, 0x75, 0x34, 0x50, 0x68, 0x76, 0x4A, + 0x56, 0x67, 0x53, 0x4C, 0x4C, 0x31, 0x4B, 0x62, 0x72, 0x61, 0x6C, 0x57, 0x36, 0x63, 0x48, 0x2F, 0x72, 0x61, 0x6C, 0x59, 0x68, 0x7A, 0x43, 0x32, 0x67, 0x66, 0x65, 0x58, 0x52, 0x66, 0x77, 0x5A, 0x56, 0x7A, 0x73, 0x72, 0x62, 0x2B, 0x52, 0x48, + 0x39, 0x4A, 0x6C, 0x46, 0x2F, 0x68, 0x33, 0x78, 0x2B, 0x4A, 0x65, 0x6A, 0x69, 0x42, 0x30, 0x33, 0x48, 0x46, 0x79, 0x50, 0x0A, 0x34, 0x48, 0x59, 0x6C, 0x6D, 0x6C, 0x44, 0x34, 0x6F, 0x46, 0x54, 0x2F, 0x52, 0x4A, 0x42, 0x32, 0x49, 0x39, 0x49, + 0x79, 0x78, 0x73, 0x4F, 0x72, 0x42, 0x72, 0x2F, 0x38, 0x2B, 0x37, 0x2F, 0x7A, 0x72, 0x58, 0x32, 0x53, 0x59, 0x67, 0x4A, 0x62, 0x4B, 0x64, 0x4D, 0x31, 0x6F, 0x35, 0x4F, 0x61, 0x51, 0x32, 0x52, 0x67, 0x58, 0x62, 0x4C, 0x36, 0x4D, 0x76, 0x38, + 0x37, 0x42, 0x4B, 0x39, 0x4E, 0x51, 0x47, 0x72, 0x35, 0x78, 0x2B, 0x50, 0x76, 0x49, 0x2F, 0x31, 0x72, 0x0A, 0x79, 0x2B, 0x55, 0x50, 0x69, 0x7A, 0x67, 0x4E, 0x37, 0x67, 0x72, 0x38, 0x2F, 0x67, 0x2B, 0x59, 0x6E, 0x7A, 0x41, 0x78, 0x33, 0x57, + 0x78, 0x53, 0x5A, 0x66, 0x6D, 0x4C, 0x67, 0x62, 0x34, 0x69, 0x34, 0x52, 0x78, 0x59, 0x41, 0x37, 0x71, 0x52, 0x47, 0x34, 0x6B, 0x48, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, + 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x0A, 0x42, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, + 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x71, 0x4F, 0x46, 0x73, 0x6D, 0x6A, 0x64, 0x36, 0x4C, 0x57, 0x76, 0x4A, 0x50, 0x65, 0x6C, 0x53, 0x44, 0x47, 0x52, 0x6A, 0x6A, 0x43, 0x44, 0x57, 0x6D, 0x75, + 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x0A, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x45, 0x41, 0x50, 0x4E, 0x56, 0x33, 0x50, 0x64, 0x72, 0x66, 0x69, 0x62, 0x71, 0x48, + 0x44, 0x41, 0x68, 0x55, 0x61, 0x69, 0x42, 0x51, 0x6B, 0x72, 0x36, 0x77, 0x51, 0x54, 0x32, 0x35, 0x4A, 0x6D, 0x53, 0x44, 0x43, 0x69, 0x2F, 0x6F, 0x51, 0x4D, 0x43, 0x58, 0x4B, 0x43, 0x65, 0x43, 0x4D, 0x45, 0x72, 0x4A, 0x6B, 0x2F, 0x39, 0x71, + 0x35, 0x36, 0x59, 0x41, 0x66, 0x34, 0x6C, 0x43, 0x0A, 0x6D, 0x74, 0x59, 0x52, 0x35, 0x56, 0x50, 0x4F, 0x4C, 0x38, 0x7A, 0x79, 0x32, 0x67, 0x58, 0x45, 0x2F, 0x75, 0x4A, 0x51, 0x78, 0x44, 0x71, 0x47, 0x66, 0x63, 0x7A, 0x61, 0x66, 0x68, 0x41, + 0x4A, 0x4F, 0x35, 0x49, 0x31, 0x4B, 0x6C, 0x4F, 0x79, 0x2F, 0x75, 0x73, 0x72, 0x42, 0x64, 0x6C, 0x73, 0x58, 0x65, 0x62, 0x51, 0x37, 0x39, 0x4E, 0x71, 0x5A, 0x70, 0x34, 0x56, 0x4B, 0x49, 0x56, 0x36, 0x36, 0x49, 0x49, 0x41, 0x72, 0x42, 0x36, + 0x6E, 0x43, 0x57, 0x6C, 0x57, 0x0A, 0x51, 0x74, 0x4E, 0x6F, 0x55, 0x52, 0x69, 0x2B, 0x56, 0x4A, 0x71, 0x2F, 0x52, 0x45, 0x47, 0x36, 0x53, 0x62, 0x34, 0x67, 0x75, 0x6D, 0x6C, 0x63, 0x37, 0x72, 0x68, 0x33, 0x7A, 0x63, 0x35, 0x73, 0x48, 0x36, + 0x32, 0x44, 0x6C, 0x68, 0x68, 0x39, 0x44, 0x72, 0x55, 0x55, 0x4F, 0x59, 0x54, 0x78, 0x4B, 0x4F, 0x6B, 0x74, 0x6F, 0x35, 0x35, 0x37, 0x48, 0x6E, 0x70, 0x79, 0x57, 0x6F, 0x4F, 0x7A, 0x65, 0x57, 0x2F, 0x76, 0x74, 0x50, 0x7A, 0x51, 0x43, 0x71, + 0x56, 0x59, 0x0A, 0x54, 0x30, 0x62, 0x66, 0x2B, 0x32, 0x31, 0x35, 0x57, 0x66, 0x4B, 0x45, 0x49, 0x6C, 0x4B, 0x75, 0x44, 0x38, 0x7A, 0x37, 0x66, 0x44, 0x76, 0x6E, 0x61, 0x73, 0x70, 0x48, 0x59, 0x63, 0x4E, 0x36, 0x2B, 0x4E, 0x4F, 0x53, 0x42, + 0x42, 0x2B, 0x34, 0x49, 0x49, 0x54, 0x68, 0x4E, 0x6C, 0x51, 0x57, 0x78, 0x30, 0x44, 0x65, 0x4F, 0x34, 0x70, 0x7A, 0x33, 0x4E, 0x2F, 0x47, 0x43, 0x55, 0x7A, 0x66, 0x37, 0x4E, 0x72, 0x2F, 0x31, 0x46, 0x4E, 0x43, 0x6F, 0x63, 0x6E, 0x79, 0x0A, + 0x59, 0x68, 0x30, 0x69, 0x67, 0x7A, 0x79, 0x58, 0x78, 0x66, 0x6B, 0x5A, 0x59, 0x69, 0x65, 0x73, 0x5A, 0x53, 0x4C, 0x58, 0x30, 0x7A, 0x7A, 0x47, 0x35, 0x59, 0x36, 0x79, 0x55, 0x38, 0x78, 0x4A, 0x7A, 0x72, 0x77, 0x77, 0x2F, 0x6E, 0x73, 0x4F, + 0x4D, 0x35, 0x44, 0x37, 0x37, 0x64, 0x49, 0x55, 0x6B, 0x52, 0x38, 0x48, 0x72, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x20, 0x43, 0x6F, 0x6D, 0x6D, 0x75, 0x6E, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x43, 0x41, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x64, 0x7A, 0x43, 0x43, 0x41, 0x6C, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x44, 0x41, 0x4E, 0x42, 0x67, + 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x64, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x4B, 0x55, 0x44, 0x45, 0x6C, 0x4D, 0x43, + 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x63, 0x0A, 0x55, 0x30, 0x56, 0x44, 0x54, 0x30, 0x30, 0x67, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x44, 0x54, + 0x79, 0x34, 0x73, 0x54, 0x46, 0x52, 0x45, 0x4C, 0x6A, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x65, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x51, 0x32, 0x39, 0x74, 0x62, + 0x58, 0x56, 0x75, 0x61, 0x57, 0x4E, 0x68, 0x0A, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x51, 0x30, 0x45, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x41, 0x35, 0x4D, 0x44, 0x55, 0x79, 0x4F, 0x54, 0x41, 0x31, + 0x4D, 0x44, 0x41, 0x7A, 0x4F, 0x56, 0x6F, 0x58, 0x44, 0x54, 0x49, 0x35, 0x4D, 0x44, 0x55, 0x79, 0x4F, 0x54, 0x41, 0x31, 0x4D, 0x44, 0x41, 0x7A, 0x4F, 0x56, 0x6F, 0x77, 0x58, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x42, 0x68, 0x4D, 0x43, 0x0A, 0x53, 0x6C, 0x41, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x46, 0x4E, 0x46, 0x51, 0x30, 0x39, 0x4E, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, + 0x35, 0x63, 0x33, 0x52, 0x6C, 0x62, 0x58, 0x4D, 0x67, 0x51, 0x30, 0x38, 0x75, 0x4C, 0x45, 0x78, 0x55, 0x52, 0x43, 0x34, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x48, 0x6C, 0x4E, 0x6C, 0x59, 0x33, 0x56, + 0x79, 0x0A, 0x61, 0x58, 0x52, 0x35, 0x49, 0x45, 0x4E, 0x76, 0x62, 0x57, 0x31, 0x31, 0x62, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x45, 0x4E, 0x42, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x53, + 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x0A, 0x41, + 0x4E, 0x41, 0x56, 0x4F, 0x56, 0x4B, 0x78, 0x55, 0x72, 0x4F, 0x36, 0x78, 0x56, 0x6D, 0x43, 0x78, 0x46, 0x31, 0x53, 0x72, 0x6A, 0x70, 0x44, 0x5A, 0x59, 0x42, 0x4C, 0x78, 0x2F, 0x4B, 0x57, 0x76, 0x4E, 0x73, 0x32, 0x6C, 0x39, 0x61, 0x6D, 0x5A, + 0x49, 0x79, 0x6F, 0x58, 0x76, 0x44, 0x6A, 0x43, 0x68, 0x7A, 0x33, 0x33, 0x35, 0x63, 0x39, 0x53, 0x36, 0x37, 0x32, 0x58, 0x65, 0x77, 0x68, 0x74, 0x55, 0x47, 0x72, 0x7A, 0x62, 0x6C, 0x2B, 0x64, 0x70, 0x2B, 0x2B, 0x0A, 0x2B, 0x54, 0x34, 0x32, + 0x4E, 0x4B, 0x41, 0x37, 0x77, 0x66, 0x59, 0x78, 0x45, 0x55, 0x56, 0x30, 0x6B, 0x7A, 0x31, 0x58, 0x67, 0x4D, 0x58, 0x35, 0x69, 0x5A, 0x6E, 0x4B, 0x35, 0x61, 0x74, 0x71, 0x31, 0x4C, 0x58, 0x61, 0x51, 0x5A, 0x41, 0x51, 0x77, 0x64, 0x62, 0x57, + 0x51, 0x6F, 0x6E, 0x43, 0x76, 0x2F, 0x51, 0x34, 0x45, 0x70, 0x56, 0x4D, 0x56, 0x41, 0x58, 0x33, 0x4E, 0x75, 0x52, 0x46, 0x67, 0x33, 0x73, 0x55, 0x5A, 0x64, 0x62, 0x63, 0x44, 0x45, 0x33, 0x52, 0x0A, 0x33, 0x6E, 0x34, 0x4D, 0x71, 0x7A, 0x76, + 0x45, 0x46, 0x62, 0x34, 0x36, 0x56, 0x71, 0x5A, 0x61, 0x62, 0x33, 0x5A, 0x70, 0x55, 0x71, 0x6C, 0x36, 0x75, 0x63, 0x6A, 0x72, 0x61, 0x70, 0x70, 0x64, 0x55, 0x74, 0x41, 0x74, 0x43, 0x6D, 0x73, 0x31, 0x46, 0x67, 0x6B, 0x51, 0x68, 0x4E, 0x42, + 0x71, 0x79, 0x6A, 0x6F, 0x47, 0x41, 0x44, 0x64, 0x48, 0x35, 0x48, 0x35, 0x58, 0x54, 0x7A, 0x2B, 0x4C, 0x36, 0x32, 0x65, 0x34, 0x69, 0x4B, 0x72, 0x46, 0x76, 0x6C, 0x4E, 0x56, 0x0A, 0x73, 0x70, 0x48, 0x45, 0x66, 0x62, 0x6D, 0x77, 0x68, 0x52, + 0x6B, 0x47, 0x65, 0x43, 0x37, 0x62, 0x59, 0x52, 0x72, 0x36, 0x68, 0x66, 0x56, 0x4B, 0x6B, 0x61, 0x48, 0x6E, 0x46, 0x74, 0x57, 0x4F, 0x6F, 0x6A, 0x6E, 0x66, 0x6C, 0x4C, 0x68, 0x77, 0x48, 0x79, 0x67, 0x2F, 0x69, 0x2F, 0x78, 0x41, 0x58, 0x6D, + 0x4F, 0x44, 0x50, 0x49, 0x4D, 0x71, 0x47, 0x70, 0x6C, 0x72, 0x7A, 0x39, 0x35, 0x5A, 0x61, 0x6A, 0x76, 0x38, 0x62, 0x78, 0x62, 0x58, 0x48, 0x2F, 0x31, 0x4B, 0x0A, 0x45, 0x4F, 0x74, 0x4F, 0x67, 0x68, 0x59, 0x36, 0x72, 0x43, 0x63, 0x4D, 0x55, + 0x2F, 0x47, 0x74, 0x31, 0x53, 0x53, 0x77, 0x61, 0x77, 0x4E, 0x51, 0x77, 0x53, 0x30, 0x38, 0x46, 0x74, 0x31, 0x45, 0x4E, 0x43, 0x63, 0x61, 0x64, 0x66, 0x73, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x48, + 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x41, 0x71, 0x46, 0x71, 0x58, 0x64, 0x6C, 0x42, 0x5A, 0x68, 0x38, 0x0A, 0x51, 0x49, 0x48, 0x34, 0x44, 0x35, 0x63, 0x73, 0x4F, 0x50, 0x45, 0x4B, 0x37, 0x44, 0x7A, 0x50, + 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, + 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x0A, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x42, 0x4D, 0x4F, 0x71, 0x4E, 0x45, 0x72, 0x4C, 0x6C, + 0x46, 0x73, 0x63, 0x65, 0x54, 0x66, 0x73, 0x67, 0x4C, 0x43, 0x6B, 0x4C, 0x66, 0x5A, 0x4F, 0x6F, 0x63, 0x37, 0x6C, 0x6C, 0x73, 0x43, 0x4C, 0x71, 0x4A, 0x58, 0x32, 0x72, 0x4B, 0x53, 0x70, 0x57, 0x65, 0x65, 0x6F, 0x38, 0x48, 0x78, 0x64, 0x70, + 0x46, 0x63, 0x6F, 0x4A, 0x78, 0x44, 0x6A, 0x72, 0x53, 0x7A, 0x47, 0x2B, 0x6E, 0x74, 0x4B, 0x45, 0x6A, 0x0A, 0x75, 0x2F, 0x59, 0x6B, 0x6E, 0x38, 0x73, 0x58, 0x2F, 0x6F, 0x79, 0x6D, 0x7A, 0x73, 0x4C, 0x53, 0x32, 0x38, 0x79, 0x4E, 0x2F, 0x48, + 0x48, 0x38, 0x41, 0x79, 0x6E, 0x42, 0x62, 0x46, 0x30, 0x7A, 0x58, 0x32, 0x53, 0x32, 0x5A, 0x54, 0x75, 0x4A, 0x62, 0x78, 0x68, 0x32, 0x65, 0x50, 0x58, 0x63, 0x6F, 0x6B, 0x67, 0x66, 0x47, 0x54, 0x2B, 0x4F, 0x6B, 0x2B, 0x76, 0x78, 0x2B, 0x68, + 0x66, 0x75, 0x7A, 0x55, 0x37, 0x6A, 0x42, 0x42, 0x4A, 0x56, 0x31, 0x75, 0x58, 0x6B, 0x0A, 0x33, 0x66, 0x73, 0x2B, 0x42, 0x58, 0x7A, 0x69, 0x48, 0x56, 0x37, 0x47, 0x70, 0x37, 0x79, 0x58, 0x54, 0x32, 0x67, 0x36, 0x39, 0x65, 0x6B, 0x75, 0x43, + 0x6B, 0x4F, 0x32, 0x72, 0x31, 0x64, 0x63, 0x59, 0x6D, 0x68, 0x38, 0x74, 0x2F, 0x32, 0x6A, 0x69, 0x6F, 0x53, 0x67, 0x72, 0x47, 0x4B, 0x2B, 0x4B, 0x77, 0x6D, 0x48, 0x4E, 0x50, 0x42, 0x71, 0x41, 0x62, 0x75, 0x62, 0x4B, 0x56, 0x59, 0x38, 0x2F, + 0x67, 0x41, 0x33, 0x7A, 0x79, 0x4E, 0x73, 0x38, 0x55, 0x36, 0x71, 0x0A, 0x74, 0x6E, 0x52, 0x47, 0x45, 0x6D, 0x79, 0x52, 0x37, 0x6A, 0x54, 0x56, 0x37, 0x4A, 0x71, 0x52, 0x35, 0x30, 0x53, 0x2B, 0x6B, 0x44, 0x46, 0x79, 0x31, 0x55, 0x6B, 0x43, + 0x39, 0x67, 0x4C, 0x6C, 0x39, 0x42, 0x2F, 0x72, 0x66, 0x4E, 0x6D, 0x57, 0x56, 0x61, 0x6E, 0x2F, 0x37, 0x49, 0x72, 0x35, 0x6D, 0x55, 0x66, 0x2F, 0x4E, 0x56, 0x6F, 0x43, 0x71, 0x67, 0x54, 0x4C, 0x69, 0x6C, 0x75, 0x48, 0x63, 0x53, 0x6D, 0x52, + 0x76, 0x61, 0x53, 0x30, 0x65, 0x67, 0x32, 0x39, 0x0A, 0x6D, 0x76, 0x56, 0x58, 0x49, 0x77, 0x41, 0x48, 0x49, 0x52, 0x63, 0x2F, 0x53, 0x6A, 0x6E, 0x52, 0x42, 0x55, 0x6B, 0x4C, 0x70, 0x37, 0x59, 0x33, 0x67, 0x61, 0x56, 0x64, 0x6A, 0x4B, 0x6F, + 0x7A, 0x58, 0x6F, 0x45, 0x6F, 0x66, 0x4B, 0x64, 0x39, 0x4A, 0x2B, 0x73, 0x41, 0x72, 0x6F, 0x30, 0x33, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x63, 0x74, 0x61, 0x6C, 0x69, 0x73, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x75, 0x7A, 0x43, 0x43, 0x41, 0x36, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x56, 0x77, 0x6F, 0x52, 0x6C, + 0x30, 0x4C, 0x45, 0x34, 0x38, 0x77, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x61, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, + 0x68, 0x4D, 0x43, 0x53, 0x56, 0x51, 0x78, 0x44, 0x6A, 0x41, 0x4D, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x42, 0x55, 0x31, 0x70, 0x62, 0x47, 0x46, 0x75, 0x4D, 0x53, 0x4D, 0x77, 0x49, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, + 0x44, 0x42, 0x70, 0x42, 0x59, 0x33, 0x52, 0x68, 0x62, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4D, 0x75, 0x63, 0x43, 0x35, 0x42, 0x4C, 0x69, 0x38, 0x77, 0x4D, 0x7A, 0x4D, 0x31, 0x4F, 0x44, 0x55, 0x79, 0x4D, 0x44, 0x6B, 0x32, 0x4E, 0x7A, 0x45, 0x6E, + 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x41, 0x77, 0x77, 0x65, 0x51, 0x57, 0x4E, 0x30, 0x59, 0x57, 0x78, 0x70, 0x63, 0x79, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x5A, 0x57, 0x35, 0x30, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, + 0x76, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x6B, 0x79, 0x4D, 0x6A, 0x45, 0x78, 0x4D, 0x6A, 0x49, 0x77, 0x4D, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x4D, + 0x77, 0x4D, 0x44, 0x6B, 0x79, 0x0A, 0x4D, 0x6A, 0x45, 0x78, 0x4D, 0x6A, 0x49, 0x77, 0x4D, 0x6C, 0x6F, 0x77, 0x61, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x56, 0x51, 0x78, 0x44, 0x6A, + 0x41, 0x4D, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x42, 0x55, 0x31, 0x70, 0x62, 0x47, 0x46, 0x75, 0x4D, 0x53, 0x4D, 0x77, 0x49, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x70, 0x42, 0x59, 0x33, 0x52, 0x68, 0x62, 0x47, + 0x6C, 0x7A, 0x0A, 0x49, 0x46, 0x4D, 0x75, 0x63, 0x43, 0x35, 0x42, 0x4C, 0x69, 0x38, 0x77, 0x4D, 0x7A, 0x4D, 0x31, 0x4F, 0x44, 0x55, 0x79, 0x4D, 0x44, 0x6B, 0x32, 0x4E, 0x7A, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, + 0x77, 0x77, 0x65, 0x51, 0x57, 0x4E, 0x30, 0x59, 0x57, 0x78, 0x70, 0x63, 0x79, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x5A, 0x57, 0x35, 0x30, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x0A, + 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, + 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x70, 0x38, 0x62, 0x45, 0x70, 0x53, 0x6D, 0x6B, 0x4C, 0x4F, 0x2F, 0x6C, 0x47, 0x4D, 0x57, 0x77, 0x55, 0x4B, 0x4E, 0x76, 0x55, 0x54, 0x75, 0x66, 0x43, 0x6C, 0x72, 0x4A, 0x0A, 0x77, 0x6B, 0x67, + 0x34, 0x43, 0x73, 0x49, 0x63, 0x6F, 0x42, 0x68, 0x2F, 0x6B, 0x62, 0x57, 0x48, 0x75, 0x55, 0x41, 0x2F, 0x33, 0x52, 0x31, 0x6F, 0x48, 0x77, 0x69, 0x44, 0x31, 0x53, 0x30, 0x65, 0x69, 0x4B, 0x44, 0x34, 0x6A, 0x31, 0x61, 0x50, 0x62, 0x5A, 0x6B, + 0x43, 0x6B, 0x70, 0x41, 0x57, 0x31, 0x56, 0x38, 0x49, 0x62, 0x49, 0x6E, 0x58, 0x34, 0x61, 0x79, 0x38, 0x49, 0x4D, 0x4B, 0x78, 0x34, 0x49, 0x4E, 0x52, 0x69, 0x6D, 0x6C, 0x4E, 0x41, 0x4A, 0x5A, 0x61, 0x0A, 0x62, 0x79, 0x2F, 0x41, 0x52, 0x48, + 0x36, 0x6A, 0x44, 0x75, 0x53, 0x52, 0x7A, 0x56, 0x6A, 0x75, 0x33, 0x50, 0x76, 0x48, 0x48, 0x6B, 0x56, 0x48, 0x33, 0x53, 0x65, 0x35, 0x43, 0x41, 0x47, 0x66, 0x70, 0x69, 0x45, 0x64, 0x39, 0x55, 0x45, 0x74, 0x4C, 0x30, 0x7A, 0x39, 0x4B, 0x4B, + 0x33, 0x67, 0x69, 0x71, 0x30, 0x69, 0x74, 0x46, 0x5A, 0x6C, 0x6A, 0x6F, 0x5A, 0x55, 0x6A, 0x35, 0x4E, 0x44, 0x4B, 0x64, 0x34, 0x35, 0x52, 0x6E, 0x69, 0x6A, 0x4D, 0x43, 0x4F, 0x36, 0x0A, 0x7A, 0x66, 0x42, 0x39, 0x45, 0x31, 0x66, 0x41, 0x58, + 0x64, 0x4B, 0x44, 0x61, 0x30, 0x68, 0x4D, 0x78, 0x4B, 0x75, 0x66, 0x67, 0x46, 0x70, 0x62, 0x4F, 0x72, 0x33, 0x4A, 0x70, 0x79, 0x49, 0x2F, 0x67, 0x43, 0x63, 0x7A, 0x57, 0x77, 0x36, 0x33, 0x69, 0x67, 0x78, 0x64, 0x42, 0x7A, 0x63, 0x49, 0x79, + 0x32, 0x7A, 0x53, 0x65, 0x6B, 0x63, 0x69, 0x52, 0x44, 0x58, 0x46, 0x7A, 0x4D, 0x77, 0x75, 0x6A, 0x74, 0x30, 0x71, 0x37, 0x62, 0x64, 0x39, 0x5A, 0x67, 0x31, 0x66, 0x0A, 0x59, 0x56, 0x45, 0x69, 0x56, 0x52, 0x76, 0x6A, 0x52, 0x75, 0x50, 0x6A, + 0x50, 0x64, 0x41, 0x31, 0x59, 0x70, 0x72, 0x62, 0x72, 0x78, 0x54, 0x49, 0x57, 0x36, 0x48, 0x4D, 0x69, 0x52, 0x76, 0x68, 0x4D, 0x43, 0x62, 0x38, 0x6F, 0x4A, 0x73, 0x66, 0x67, 0x61, 0x64, 0x48, 0x48, 0x77, 0x54, 0x72, 0x6F, 0x7A, 0x6D, 0x53, + 0x42, 0x70, 0x2B, 0x5A, 0x30, 0x37, 0x2F, 0x54, 0x36, 0x6B, 0x39, 0x51, 0x6E, 0x42, 0x6E, 0x2B, 0x6C, 0x6F, 0x63, 0x65, 0x50, 0x47, 0x58, 0x32, 0x0A, 0x6F, 0x78, 0x67, 0x6B, 0x67, 0x34, 0x59, 0x51, 0x35, 0x31, 0x51, 0x2B, 0x71, 0x44, 0x70, + 0x32, 0x4A, 0x45, 0x2B, 0x42, 0x49, 0x63, 0x58, 0x6A, 0x44, 0x77, 0x4C, 0x34, 0x6B, 0x35, 0x52, 0x48, 0x49, 0x4C, 0x76, 0x2B, 0x31, 0x41, 0x37, 0x54, 0x61, 0x4C, 0x6E, 0x64, 0x78, 0x48, 0x71, 0x45, 0x67, 0x75, 0x4E, 0x54, 0x56, 0x48, 0x6E, + 0x64, 0x32, 0x35, 0x7A, 0x53, 0x38, 0x67, 0x65, 0x62, 0x4C, 0x72, 0x61, 0x38, 0x50, 0x75, 0x32, 0x46, 0x62, 0x65, 0x38, 0x6C, 0x0A, 0x45, 0x66, 0x4B, 0x58, 0x47, 0x6B, 0x4A, 0x68, 0x39, 0x30, 0x71, 0x58, 0x36, 0x49, 0x75, 0x78, 0x45, 0x41, + 0x66, 0x36, 0x5A, 0x59, 0x47, 0x79, 0x6F, 0x6A, 0x6E, 0x50, 0x39, 0x7A, 0x7A, 0x2F, 0x47, 0x50, 0x76, 0x47, 0x38, 0x56, 0x71, 0x4C, 0x57, 0x65, 0x49, 0x43, 0x72, 0x48, 0x75, 0x53, 0x30, 0x45, 0x34, 0x55, 0x54, 0x31, 0x6C, 0x46, 0x39, 0x67, + 0x78, 0x65, 0x4B, 0x46, 0x2B, 0x77, 0x36, 0x44, 0x39, 0x46, 0x7A, 0x38, 0x2B, 0x76, 0x6D, 0x32, 0x2F, 0x37, 0x0A, 0x68, 0x4E, 0x4E, 0x33, 0x57, 0x70, 0x56, 0x76, 0x72, 0x4A, 0x53, 0x45, 0x6E, 0x75, 0x36, 0x38, 0x77, 0x45, 0x71, 0x50, 0x53, + 0x70, 0x50, 0x34, 0x52, 0x43, 0x48, 0x69, 0x4D, 0x55, 0x56, 0x68, 0x55, 0x45, 0x34, 0x51, 0x32, 0x4F, 0x4D, 0x31, 0x66, 0x45, 0x77, 0x5A, 0x74, 0x4E, 0x34, 0x46, 0x76, 0x36, 0x4D, 0x47, 0x6E, 0x38, 0x69, 0x31, 0x7A, 0x65, 0x51, 0x66, 0x31, + 0x78, 0x63, 0x47, 0x44, 0x58, 0x71, 0x56, 0x64, 0x46, 0x55, 0x4E, 0x61, 0x42, 0x72, 0x38, 0x0A, 0x45, 0x42, 0x74, 0x69, 0x5A, 0x4A, 0x31, 0x74, 0x34, 0x4A, 0x57, 0x67, 0x77, 0x35, 0x51, 0x48, 0x56, 0x77, 0x30, 0x55, 0x35, 0x72, 0x30, 0x46, + 0x2B, 0x37, 0x69, 0x66, 0x35, 0x74, 0x2B, 0x4C, 0x34, 0x73, 0x62, 0x6E, 0x66, 0x70, 0x62, 0x32, 0x55, 0x38, 0x57, 0x41, 0x4E, 0x46, 0x41, 0x6F, 0x57, 0x50, 0x41, 0x53, 0x55, 0x48, 0x45, 0x58, 0x4D, 0x4C, 0x72, 0x6D, 0x65, 0x47, 0x4F, 0x38, + 0x39, 0x4C, 0x4B, 0x74, 0x6D, 0x79, 0x75, 0x79, 0x2F, 0x75, 0x45, 0x35, 0x0A, 0x6A, 0x46, 0x36, 0x36, 0x43, 0x79, 0x43, 0x55, 0x33, 0x6E, 0x75, 0x44, 0x75, 0x50, 0x2F, 0x6A, 0x56, 0x6F, 0x32, 0x33, 0x45, 0x65, 0x6B, 0x37, 0x6A, 0x50, 0x4B, + 0x78, 0x77, 0x56, 0x32, 0x64, 0x70, 0x41, 0x74, 0x4D, 0x4B, 0x39, 0x6D, 0x79, 0x47, 0x50, 0x57, 0x31, 0x6E, 0x30, 0x73, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x6A, 0x4D, 0x47, 0x45, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, + 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x46, 0x4C, 0x59, 0x0A, 0x69, 0x44, 0x72, 0x49, 0x6E, 0x33, 0x68, 0x6D, 0x37, 0x59, 0x6E, 0x7A, 0x65, 0x7A, 0x68, 0x77, 0x6C, 0x4D, 0x6B, 0x43, 0x41, 0x6A, 0x62, 0x51, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, + 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, 0x55, 0x55, 0x74, 0x69, 0x49, 0x4F, 0x73, + 0x69, 0x66, 0x65, 0x47, 0x62, 0x74, 0x0A, 0x69, 0x66, 0x4E, 0x37, 0x4F, 0x48, 0x43, 0x55, 0x79, 0x51, 0x49, 0x43, 0x4E, 0x74, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, + 0x67, 0x45, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x41, 0x4C, 0x65, 0x33, 0x4B, 0x48, 0x77, 0x47, 0x43, 0x6D, 0x53, + 0x55, 0x79, 0x49, 0x0A, 0x57, 0x4F, 0x59, 0x64, 0x69, 0x50, 0x63, 0x55, 0x5A, 0x45, 0x69, 0x6D, 0x32, 0x46, 0x67, 0x4B, 0x44, 0x6B, 0x38, 0x54, 0x4E, 0x64, 0x38, 0x31, 0x48, 0x64, 0x54, 0x74, 0x42, 0x6A, 0x48, 0x49, 0x67, 0x54, 0x35, 0x71, + 0x31, 0x64, 0x30, 0x37, 0x47, 0x6A, 0x4C, 0x75, 0x6B, 0x44, 0x30, 0x52, 0x30, 0x69, 0x37, 0x30, 0x6A, 0x73, 0x4E, 0x6A, 0x4C, 0x69, 0x4E, 0x6D, 0x73, 0x47, 0x65, 0x2B, 0x62, 0x37, 0x62, 0x41, 0x45, 0x7A, 0x6C, 0x67, 0x71, 0x71, 0x49, 0x30, + 0x0A, 0x4A, 0x5A, 0x4E, 0x31, 0x55, 0x74, 0x36, 0x6E, 0x6E, 0x61, 0x30, 0x4F, 0x68, 0x34, 0x6C, 0x53, 0x63, 0x57, 0x6F, 0x57, 0x50, 0x42, 0x6B, 0x64, 0x67, 0x2F, 0x69, 0x61, 0x4B, 0x57, 0x57, 0x2B, 0x39, 0x44, 0x2B, 0x61, 0x32, 0x66, 0x44, + 0x7A, 0x57, 0x6F, 0x63, 0x68, 0x63, 0x59, 0x42, 0x4E, 0x79, 0x2B, 0x41, 0x34, 0x6D, 0x7A, 0x2B, 0x37, 0x2B, 0x75, 0x41, 0x77, 0x54, 0x63, 0x2B, 0x47, 0x30, 0x32, 0x55, 0x51, 0x47, 0x52, 0x6A, 0x52, 0x6C, 0x77, 0x4B, 0x78, 0x0A, 0x4B, 0x33, + 0x4A, 0x43, 0x61, 0x4B, 0x79, 0x67, 0x76, 0x55, 0x35, 0x61, 0x32, 0x68, 0x69, 0x2F, 0x61, 0x35, 0x69, 0x42, 0x30, 0x50, 0x32, 0x61, 0x76, 0x6C, 0x34, 0x56, 0x53, 0x4D, 0x30, 0x52, 0x46, 0x62, 0x6E, 0x41, 0x4B, 0x56, 0x79, 0x30, 0x36, 0x49, + 0x6A, 0x33, 0x50, 0x6A, 0x61, 0x75, 0x74, 0x32, 0x4C, 0x39, 0x48, 0x6D, 0x4C, 0x65, 0x63, 0x48, 0x67, 0x51, 0x48, 0x45, 0x68, 0x62, 0x32, 0x72, 0x79, 0x6B, 0x4F, 0x4C, 0x70, 0x6E, 0x37, 0x56, 0x55, 0x2B, 0x0A, 0x58, 0x6C, 0x66, 0x66, 0x31, + 0x41, 0x4E, 0x41, 0x54, 0x49, 0x47, 0x6B, 0x30, 0x6B, 0x39, 0x6A, 0x70, 0x77, 0x6C, 0x43, 0x43, 0x52, 0x54, 0x38, 0x41, 0x4B, 0x6E, 0x43, 0x67, 0x48, 0x4E, 0x50, 0x4C, 0x73, 0x42, 0x41, 0x32, 0x52, 0x46, 0x37, 0x53, 0x4F, 0x70, 0x36, 0x41, + 0x73, 0x44, 0x54, 0x36, 0x79, 0x67, 0x42, 0x4A, 0x6C, 0x68, 0x30, 0x77, 0x63, 0x42, 0x7A, 0x49, 0x6D, 0x32, 0x54, 0x6C, 0x66, 0x30, 0x35, 0x66, 0x62, 0x73, 0x71, 0x34, 0x2F, 0x61, 0x43, 0x0A, 0x34, 0x79, 0x79, 0x58, 0x58, 0x30, 0x34, 0x66, + 0x6B, 0x5A, 0x54, 0x36, 0x2F, 0x69, 0x79, 0x6A, 0x32, 0x48, 0x59, 0x61, 0x75, 0x45, 0x32, 0x79, 0x4F, 0x45, 0x2B, 0x62, 0x2B, 0x68, 0x31, 0x49, 0x59, 0x48, 0x6B, 0x6D, 0x34, 0x76, 0x50, 0x39, 0x71, 0x64, 0x43, 0x61, 0x36, 0x48, 0x43, 0x50, + 0x53, 0x58, 0x72, 0x57, 0x35, 0x62, 0x30, 0x4B, 0x44, 0x74, 0x73, 0x74, 0x38, 0x34, 0x32, 0x2F, 0x36, 0x2B, 0x4F, 0x6B, 0x66, 0x63, 0x76, 0x48, 0x6C, 0x58, 0x48, 0x6F, 0x0A, 0x32, 0x71, 0x4E, 0x38, 0x78, 0x63, 0x4C, 0x34, 0x64, 0x4A, 0x49, + 0x45, 0x47, 0x34, 0x61, 0x73, 0x70, 0x43, 0x4A, 0x54, 0x51, 0x4C, 0x61, 0x73, 0x2F, 0x6B, 0x78, 0x32, 0x7A, 0x2F, 0x75, 0x55, 0x4D, 0x73, 0x41, 0x31, 0x6E, 0x33, 0x59, 0x2F, 0x62, 0x75, 0x57, 0x51, 0x62, 0x71, 0x43, 0x6D, 0x4A, 0x71, 0x4B, + 0x34, 0x4C, 0x4C, 0x37, 0x52, 0x4B, 0x34, 0x58, 0x39, 0x70, 0x32, 0x6A, 0x49, 0x75, 0x67, 0x45, 0x72, 0x73, 0x57, 0x78, 0x30, 0x48, 0x62, 0x68, 0x7A, 0x0A, 0x6C, 0x65, 0x66, 0x75, 0x74, 0x38, 0x63, 0x6C, 0x38, 0x41, 0x42, 0x4D, 0x41, 0x4C, + 0x4A, 0x2B, 0x74, 0x67, 0x75, 0x4C, 0x48, 0x50, 0x50, 0x41, 0x55, 0x4A, 0x34, 0x6C, 0x75, 0x65, 0x41, 0x49, 0x33, 0x6A, 0x5A, 0x6D, 0x2F, 0x7A, 0x65, 0x6C, 0x30, 0x62, 0x74, 0x55, 0x5A, 0x43, 0x7A, 0x4A, 0x4A, 0x37, 0x56, 0x4C, 0x6B, 0x6E, + 0x35, 0x6C, 0x2F, 0x39, 0x4D, 0x74, 0x34, 0x62, 0x6C, 0x4F, 0x76, 0x48, 0x2B, 0x6B, 0x51, 0x53, 0x47, 0x51, 0x51, 0x58, 0x65, 0x6D, 0x0A, 0x4F, 0x52, 0x2F, 0x71, 0x6E, 0x75, 0x4F, 0x66, 0x30, 0x47, 0x5A, 0x76, 0x42, 0x65, 0x79, 0x71, 0x64, + 0x6E, 0x36, 0x2F, 0x61, 0x78, 0x61, 0x67, 0x36, 0x37, 0x58, 0x48, 0x2F, 0x4A, 0x4A, 0x55, 0x4C, 0x79, 0x73, 0x52, 0x4A, 0x79, 0x55, 0x33, 0x65, 0x45, 0x78, 0x52, 0x61, 0x72, 0x44, 0x7A, 0x7A, 0x46, 0x68, 0x64, 0x46, 0x50, 0x46, 0x71, 0x53, + 0x42, 0x58, 0x2F, 0x77, 0x67, 0x65, 0x32, 0x73, 0x59, 0x30, 0x50, 0x6A, 0x6C, 0x78, 0x51, 0x52, 0x72, 0x4D, 0x39, 0x0A, 0x76, 0x77, 0x47, 0x59, 0x54, 0x37, 0x4A, 0x5A, 0x56, 0x45, 0x63, 0x2B, 0x4E, 0x48, 0x74, 0x34, 0x62, 0x56, 0x61, 0x54, + 0x4C, 0x6E, 0x50, 0x71, 0x5A, 0x69, 0x68, 0x34, 0x7A, 0x52, 0x30, 0x55, 0x76, 0x36, 0x43, 0x50, 0x4C, 0x79, 0x36, 0x34, 0x4C, 0x6F, 0x37, 0x79, 0x46, 0x49, 0x72, 0x4D, 0x36, 0x62, 0x56, 0x38, 0x2B, 0x32, 0x79, 0x64, 0x44, 0x4B, 0x58, 0x68, + 0x6C, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x42, 0x75, 0x79, 0x70, 0x61, 0x73, 0x73, 0x20, + 0x43, 0x6C, 0x61, 0x73, 0x73, 0x20, 0x32, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x57, 0x54, 0x43, 0x43, 0x41, 0x30, 0x47, 0x67, + 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x4F, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x4F, 0x54, 0x7A, 0x45, 0x64, 0x4D, 0x42, 0x73, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x55, 0x0A, 0x51, 0x6E, 0x56, 0x35, 0x63, 0x47, 0x46, 0x7A, 0x63, 0x79, 0x42, 0x42, 0x55, 0x79, 0x30, + 0x35, 0x4F, 0x44, 0x4D, 0x78, 0x4E, 0x6A, 0x4D, 0x7A, 0x4D, 0x6A, 0x63, 0x78, 0x49, 0x44, 0x41, 0x65, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x30, 0x4A, 0x31, 0x65, 0x58, 0x42, 0x68, 0x63, 0x33, 0x4D, 0x67, 0x51, 0x32, 0x78, + 0x68, 0x63, 0x33, 0x4D, 0x67, 0x4D, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x42, 0x34, 0x58, 0x0A, 0x44, 0x54, 0x45, 0x77, 0x4D, 0x54, 0x41, 0x79, 0x4E, 0x6A, 0x41, 0x34, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x31, + 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x54, 0x41, 0x79, 0x4E, 0x6A, 0x41, 0x34, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x31, 0x6F, 0x77, 0x54, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x54, 0x6B, + 0x38, 0x78, 0x48, 0x54, 0x41, 0x62, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x46, 0x45, 0x4A, 0x31, 0x0A, 0x65, 0x58, 0x42, 0x68, 0x63, 0x33, 0x4D, 0x67, 0x51, 0x56, 0x4D, 0x74, 0x4F, 0x54, 0x67, 0x7A, 0x4D, 0x54, 0x59, 0x7A, 0x4D, + 0x7A, 0x49, 0x33, 0x4D, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x64, 0x43, 0x64, 0x58, 0x6C, 0x77, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x45, 0x4E, 0x73, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x44, 0x49, 0x67, 0x55, + 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, + 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4E, 0x66, 0x48, 0x58, 0x76, 0x66, 0x42, 0x42, 0x39, 0x52, 0x33, 0x2B, 0x30, 0x4D, 0x68, 0x39, 0x50, 0x54, 0x31, 0x61, 0x65, 0x54, 0x75, 0x4D, 0x67, 0x48, 0x62, + 0x6F, 0x34, 0x59, 0x66, 0x35, 0x46, 0x6B, 0x4E, 0x75, 0x75, 0x64, 0x31, 0x0A, 0x67, 0x31, 0x4C, 0x72, 0x36, 0x68, 0x78, 0x68, 0x46, 0x55, 0x69, 0x37, 0x48, 0x51, 0x66, 0x4B, 0x6A, 0x4B, 0x36, 0x77, 0x33, 0x4A, 0x61, 0x64, 0x36, 0x73, 0x4E, + 0x67, 0x6B, 0x6F, 0x61, 0x43, 0x4B, 0x48, 0x4F, 0x63, 0x56, 0x67, 0x62, 0x2F, 0x53, 0x32, 0x54, 0x77, 0x44, 0x43, 0x6F, 0x33, 0x53, 0x62, 0x58, 0x6C, 0x7A, 0x77, 0x78, 0x38, 0x37, 0x76, 0x46, 0x4B, 0x75, 0x33, 0x4D, 0x77, 0x5A, 0x66, 0x50, + 0x56, 0x4C, 0x34, 0x4F, 0x32, 0x66, 0x75, 0x50, 0x6E, 0x0A, 0x39, 0x5A, 0x36, 0x72, 0x59, 0x50, 0x6E, 0x54, 0x38, 0x5A, 0x32, 0x53, 0x64, 0x49, 0x72, 0x6B, 0x48, 0x4A, 0x61, 0x73, 0x57, 0x34, 0x44, 0x70, 0x74, 0x66, 0x51, 0x78, 0x68, 0x36, + 0x4E, 0x52, 0x2F, 0x4D, 0x64, 0x2B, 0x6F, 0x57, 0x2B, 0x4F, 0x55, 0x33, 0x66, 0x55, 0x6C, 0x38, 0x46, 0x56, 0x4D, 0x35, 0x49, 0x2B, 0x47, 0x43, 0x39, 0x31, 0x31, 0x4B, 0x32, 0x47, 0x53, 0x63, 0x75, 0x56, 0x72, 0x31, 0x51, 0x47, 0x62, 0x4E, + 0x67, 0x47, 0x45, 0x34, 0x31, 0x62, 0x0A, 0x2F, 0x2B, 0x45, 0x6D, 0x47, 0x56, 0x6E, 0x41, 0x4A, 0x4C, 0x71, 0x42, 0x63, 0x58, 0x6D, 0x51, 0x52, 0x46, 0x42, 0x6F, 0x4A, 0x4A, 0x52, 0x66, 0x75, 0x4C, 0x4D, 0x52, 0x38, 0x53, 0x6C, 0x42, 0x59, + 0x61, 0x4E, 0x42, 0x79, 0x79, 0x4D, 0x32, 0x31, 0x63, 0x48, 0x78, 0x4D, 0x6C, 0x41, 0x51, 0x54, 0x6E, 0x2F, 0x30, 0x68, 0x70, 0x50, 0x73, 0x68, 0x4E, 0x4F, 0x4F, 0x76, 0x45, 0x75, 0x2F, 0x58, 0x41, 0x46, 0x4F, 0x42, 0x7A, 0x33, 0x63, 0x46, + 0x49, 0x71, 0x55, 0x0A, 0x43, 0x71, 0x54, 0x71, 0x63, 0x2F, 0x73, 0x4C, 0x55, 0x65, 0x67, 0x54, 0x42, 0x78, 0x6A, 0x36, 0x44, 0x76, 0x45, 0x72, 0x30, 0x56, 0x51, 0x56, 0x66, 0x54, 0x7A, 0x68, 0x39, 0x37, 0x51, 0x5A, 0x51, 0x6D, 0x64, 0x69, + 0x58, 0x6E, 0x66, 0x67, 0x6F, 0x6C, 0x58, 0x73, 0x74, 0x74, 0x6C, 0x70, 0x46, 0x39, 0x55, 0x36, 0x72, 0x30, 0x54, 0x74, 0x53, 0x73, 0x57, 0x65, 0x35, 0x48, 0x6F, 0x6E, 0x66, 0x4F, 0x56, 0x31, 0x31, 0x36, 0x72, 0x4C, 0x4A, 0x65, 0x66, 0x66, + 0x0A, 0x61, 0x77, 0x72, 0x62, 0x44, 0x30, 0x32, 0x54, 0x54, 0x71, 0x69, 0x67, 0x7A, 0x58, 0x73, 0x75, 0x38, 0x6C, 0x6B, 0x42, 0x61, 0x72, 0x63, 0x4E, 0x75, 0x41, 0x65, 0x42, 0x66, 0x6F, 0x73, 0x34, 0x47, 0x7A, 0x6A, 0x6D, 0x43, 0x6C, 0x65, + 0x5A, 0x50, 0x65, 0x34, 0x68, 0x36, 0x4B, 0x50, 0x31, 0x44, 0x42, 0x62, 0x64, 0x69, 0x2B, 0x77, 0x30, 0x6A, 0x70, 0x77, 0x71, 0x48, 0x41, 0x41, 0x56, 0x46, 0x34, 0x31, 0x6F, 0x67, 0x39, 0x4A, 0x77, 0x6E, 0x78, 0x67, 0x49, 0x0A, 0x7A, 0x52, + 0x46, 0x6F, 0x31, 0x63, 0x6C, 0x72, 0x55, 0x73, 0x33, 0x45, 0x52, 0x6F, 0x2F, 0x63, 0x74, 0x66, 0x50, 0x59, 0x56, 0x33, 0x4D, 0x65, 0x36, 0x5A, 0x51, 0x35, 0x42, 0x4C, 0x2F, 0x54, 0x33, 0x6A, 0x6A, 0x65, 0x74, 0x46, 0x50, 0x73, 0x61, 0x52, + 0x79, 0x69, 0x66, 0x73, 0x53, 0x50, 0x35, 0x42, 0x74, 0x77, 0x72, 0x66, 0x4B, 0x69, 0x2B, 0x66, 0x76, 0x33, 0x46, 0x6D, 0x52, 0x6D, 0x61, 0x5A, 0x39, 0x4A, 0x55, 0x61, 0x4C, 0x69, 0x46, 0x52, 0x68, 0x6E, 0x0A, 0x42, 0x6B, 0x70, 0x2F, 0x31, + 0x57, 0x79, 0x31, 0x54, 0x62, 0x4D, 0x7A, 0x34, 0x47, 0x48, 0x72, 0x58, 0x62, 0x37, 0x70, 0x6D, 0x41, 0x38, 0x79, 0x31, 0x78, 0x31, 0x4C, 0x50, 0x43, 0x35, 0x61, 0x41, 0x56, 0x4B, 0x52, 0x43, 0x66, 0x4C, 0x66, 0x36, 0x6F, 0x33, 0x59, 0x42, + 0x6B, 0x42, 0x6A, 0x71, 0x68, 0x48, 0x6B, 0x2F, 0x73, 0x4D, 0x33, 0x6E, 0x68, 0x52, 0x53, 0x50, 0x2F, 0x54, 0x69, 0x7A, 0x50, 0x4A, 0x68, 0x6B, 0x39, 0x48, 0x39, 0x5A, 0x32, 0x76, 0x58, 0x0A, 0x55, 0x71, 0x36, 0x2F, 0x61, 0x4B, 0x74, 0x41, + 0x51, 0x36, 0x42, 0x58, 0x4E, 0x56, 0x4E, 0x34, 0x38, 0x46, 0x50, 0x34, 0x59, 0x55, 0x49, 0x48, 0x5A, 0x4D, 0x62, 0x58, 0x62, 0x35, 0x74, 0x4D, 0x4F, 0x41, 0x31, 0x6A, 0x72, 0x47, 0x4B, 0x76, 0x4E, 0x6F, 0x75, 0x69, 0x63, 0x77, 0x6F, 0x4E, + 0x39, 0x53, 0x47, 0x39, 0x64, 0x4B, 0x70, 0x4E, 0x36, 0x6E, 0x49, 0x44, 0x53, 0x64, 0x76, 0x48, 0x58, 0x78, 0x31, 0x69, 0x59, 0x38, 0x66, 0x39, 0x33, 0x5A, 0x48, 0x73, 0x0A, 0x4D, 0x2B, 0x37, 0x31, 0x62, 0x62, 0x52, 0x75, 0x4D, 0x47, 0x6A, + 0x65, 0x79, 0x4E, 0x59, 0x6D, 0x73, 0x48, 0x56, 0x65, 0x65, 0x37, 0x51, 0x48, 0x49, 0x4A, 0x69, 0x68, 0x64, 0x6A, 0x4B, 0x34, 0x54, 0x57, 0x78, 0x50, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, + 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4D, 0x6D, 0x41, 0x64, 0x2B, + 0x42, 0x69, 0x6B, 0x6F, 0x4C, 0x31, 0x52, 0x70, 0x7A, 0x7A, 0x75, 0x76, 0x64, 0x4D, 0x77, 0x39, 0x36, 0x34, 0x6F, 0x36, 0x30, 0x35, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, + 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x0A, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x55, 0x31, 0x38, 0x68, 0x39, 0x62, 0x71, 0x77, 0x4F, + 0x6C, 0x49, 0x35, 0x4C, 0x4A, 0x4B, 0x77, 0x62, 0x41, 0x44, 0x4A, 0x37, 0x38, 0x34, 0x67, 0x37, 0x77, 0x62, 0x79, 0x6C, 0x70, 0x37, 0x70, 0x70, 0x48, 0x52, 0x2F, 0x65, 0x68, 0x62, 0x38, 0x74, 0x2F, 0x57, 0x32, 0x2B, 0x78, 0x55, 0x62, 0x50, + 0x36, 0x75, 0x6D, 0x77, 0x48, 0x4A, 0x64, 0x45, 0x4C, 0x46, 0x78, 0x37, 0x72, 0x78, 0x50, 0x34, 0x36, 0x32, 0x73, 0x0A, 0x41, 0x32, 0x30, 0x75, 0x63, 0x53, 0x36, 0x76, 0x78, 0x4F, 0x4F, 0x74, 0x6F, 0x37, 0x30, 0x4D, 0x45, 0x61, 0x65, 0x30, + 0x2F, 0x30, 0x71, 0x79, 0x65, 0x78, 0x41, 0x51, 0x48, 0x36, 0x64, 0x58, 0x51, 0x62, 0x4C, 0x41, 0x72, 0x76, 0x51, 0x73, 0x57, 0x64, 0x5A, 0x48, 0x45, 0x49, 0x6A, 0x7A, 0x49, 0x56, 0x45, 0x70, 0x4D, 0x4D, 0x70, 0x67, 0x68, 0x71, 0x39, 0x47, + 0x71, 0x78, 0x33, 0x74, 0x4F, 0x6C, 0x75, 0x77, 0x6C, 0x4E, 0x35, 0x45, 0x34, 0x30, 0x45, 0x49, 0x0A, 0x6F, 0x73, 0x48, 0x73, 0x48, 0x64, 0x62, 0x39, 0x54, 0x37, 0x62, 0x57, 0x52, 0x39, 0x41, 0x55, 0x43, 0x38, 0x72, 0x6D, 0x79, 0x72, 0x56, + 0x37, 0x64, 0x33, 0x35, 0x42, 0x48, 0x31, 0x36, 0x44, 0x78, 0x37, 0x61, 0x4D, 0x4F, 0x5A, 0x61, 0x77, 0x50, 0x35, 0x61, 0x42, 0x51, 0x57, 0x39, 0x67, 0x6B, 0x4F, 0x4C, 0x6F, 0x2B, 0x66, 0x73, 0x69, 0x63, 0x64, 0x6C, 0x39, 0x73, 0x7A, 0x31, + 0x47, 0x76, 0x37, 0x53, 0x45, 0x72, 0x35, 0x41, 0x63, 0x44, 0x34, 0x38, 0x53, 0x0A, 0x61, 0x71, 0x2F, 0x76, 0x37, 0x68, 0x35, 0x36, 0x72, 0x67, 0x4A, 0x4B, 0x69, 0x68, 0x63, 0x72, 0x64, 0x76, 0x36, 0x73, 0x56, 0x49, 0x6B, 0x6B, 0x4C, 0x45, + 0x38, 0x2F, 0x74, 0x72, 0x4B, 0x6E, 0x54, 0x6F, 0x79, 0x6F, 0x6B, 0x5A, 0x66, 0x37, 0x4B, 0x63, 0x5A, 0x37, 0x58, 0x43, 0x32, 0x35, 0x79, 0x32, 0x61, 0x32, 0x74, 0x36, 0x68, 0x62, 0x45, 0x6C, 0x47, 0x46, 0x74, 0x51, 0x6C, 0x2B, 0x59, 0x6E, + 0x68, 0x77, 0x2F, 0x71, 0x6C, 0x71, 0x59, 0x4C, 0x59, 0x64, 0x0A, 0x44, 0x6E, 0x6B, 0x4D, 0x2F, 0x63, 0x72, 0x71, 0x4A, 0x49, 0x42, 0x79, 0x77, 0x35, 0x63, 0x2F, 0x38, 0x6E, 0x65, 0x72, 0x51, 0x79, 0x49, 0x4B, 0x78, 0x2B, 0x75, 0x32, 0x44, + 0x49, 0x53, 0x43, 0x4C, 0x49, 0x42, 0x72, 0x51, 0x59, 0x6F, 0x49, 0x77, 0x4F, 0x75, 0x6C, 0x61, 0x39, 0x2B, 0x5A, 0x45, 0x73, 0x75, 0x4B, 0x31, 0x56, 0x36, 0x41, 0x44, 0x4A, 0x48, 0x67, 0x4A, 0x67, 0x67, 0x32, 0x53, 0x4D, 0x58, 0x36, 0x4F, + 0x42, 0x45, 0x31, 0x2F, 0x79, 0x57, 0x44, 0x0A, 0x4C, 0x66, 0x4A, 0x36, 0x76, 0x39, 0x72, 0x39, 0x6A, 0x76, 0x36, 0x6C, 0x79, 0x30, 0x55, 0x73, 0x48, 0x38, 0x53, 0x49, 0x55, 0x36, 0x35, 0x33, 0x44, 0x74, 0x6D, 0x61, 0x64, 0x73, 0x57, 0x4F, + 0x4C, 0x42, 0x32, 0x6A, 0x75, 0x74, 0x58, 0x73, 0x4D, 0x71, 0x37, 0x41, 0x71, 0x71, 0x7A, 0x33, 0x30, 0x58, 0x70, 0x4E, 0x36, 0x39, 0x51, 0x48, 0x34, 0x6B, 0x6A, 0x33, 0x49, 0x6F, 0x36, 0x77, 0x70, 0x4A, 0x39, 0x71, 0x7A, 0x6F, 0x36, 0x79, + 0x73, 0x6D, 0x44, 0x30, 0x0A, 0x6F, 0x79, 0x4C, 0x51, 0x49, 0x2B, 0x75, 0x55, 0x57, 0x6E, 0x70, 0x70, 0x33, 0x51, 0x2B, 0x2F, 0x51, 0x46, 0x65, 0x73, 0x61, 0x31, 0x6C, 0x51, 0x32, 0x61, 0x4F, 0x5A, 0x34, 0x57, 0x37, 0x2B, 0x6A, 0x51, 0x46, + 0x35, 0x4A, 0x79, 0x4D, 0x56, 0x33, 0x70, 0x4B, 0x64, 0x65, 0x77, 0x6C, 0x4E, 0x57, 0x75, 0x64, 0x4C, 0x53, 0x44, 0x42, 0x61, 0x47, 0x4F, 0x59, 0x4B, 0x62, 0x65, 0x61, 0x50, 0x34, 0x4E, 0x4B, 0x37, 0x35, 0x74, 0x39, 0x38, 0x62, 0x69, 0x47, + 0x43, 0x0A, 0x77, 0x57, 0x67, 0x35, 0x54, 0x62, 0x53, 0x59, 0x57, 0x47, 0x5A, 0x69, 0x7A, 0x45, 0x71, 0x51, 0x58, 0x73, 0x50, 0x36, 0x4A, 0x77, 0x53, 0x78, 0x65, 0x52, 0x56, 0x30, 0x6D, 0x63, 0x79, 0x2B, 0x72, 0x53, 0x44, 0x65, 0x4A, 0x6D, + 0x41, 0x63, 0x36, 0x31, 0x5A, 0x52, 0x70, 0x71, 0x50, 0x71, 0x35, 0x4B, 0x4D, 0x2F, 0x70, 0x2F, 0x39, 0x68, 0x33, 0x50, 0x46, 0x61, 0x54, 0x57, 0x77, 0x79, 0x49, 0x30, 0x50, 0x75, 0x72, 0x4B, 0x6A, 0x75, 0x37, 0x6B, 0x6F, 0x53, 0x0A, 0x43, + 0x54, 0x78, 0x64, 0x63, 0x63, 0x4B, 0x2B, 0x65, 0x66, 0x72, 0x43, 0x68, 0x32, 0x67, 0x64, 0x43, 0x2F, 0x31, 0x63, 0x61, 0x63, 0x77, 0x47, 0x30, 0x4A, 0x70, 0x39, 0x56, 0x4A, 0x6B, 0x71, 0x79, 0x54, 0x6B, 0x61, 0x47, 0x61, 0x39, 0x4C, 0x4B, + 0x6B, 0x50, 0x7A, 0x59, 0x31, 0x31, 0x61, 0x57, 0x4F, 0x49, 0x76, 0x34, 0x78, 0x33, 0x6B, 0x71, 0x64, 0x62, 0x51, 0x43, 0x74, 0x43, 0x65, 0x76, 0x39, 0x65, 0x42, 0x43, 0x66, 0x48, 0x4A, 0x78, 0x79, 0x59, 0x4E, 0x0A, 0x72, 0x4A, 0x67, 0x57, + 0x56, 0x71, 0x41, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x42, 0x75, 0x79, 0x70, 0x61, 0x73, 0x73, 0x20, + 0x43, 0x6C, 0x61, 0x73, 0x73, 0x20, 0x33, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x57, 0x54, 0x43, 0x43, 0x41, 0x30, 0x47, 0x67, + 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x4F, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x4F, 0x54, 0x7A, 0x45, 0x64, 0x4D, 0x42, 0x73, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x55, 0x0A, 0x51, 0x6E, 0x56, 0x35, 0x63, 0x47, 0x46, 0x7A, 0x63, 0x79, 0x42, 0x42, 0x55, 0x79, 0x30, + 0x35, 0x4F, 0x44, 0x4D, 0x78, 0x4E, 0x6A, 0x4D, 0x7A, 0x4D, 0x6A, 0x63, 0x78, 0x49, 0x44, 0x41, 0x65, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x30, 0x4A, 0x31, 0x65, 0x58, 0x42, 0x68, 0x63, 0x33, 0x4D, 0x67, 0x51, 0x32, 0x78, + 0x68, 0x63, 0x33, 0x4D, 0x67, 0x4D, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x42, 0x34, 0x58, 0x0A, 0x44, 0x54, 0x45, 0x77, 0x4D, 0x54, 0x41, 0x79, 0x4E, 0x6A, 0x41, 0x34, 0x4D, 0x6A, 0x67, 0x31, 0x4F, 0x46, + 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x54, 0x41, 0x79, 0x4E, 0x6A, 0x41, 0x34, 0x4D, 0x6A, 0x67, 0x31, 0x4F, 0x46, 0x6F, 0x77, 0x54, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x54, 0x6B, + 0x38, 0x78, 0x48, 0x54, 0x41, 0x62, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x46, 0x45, 0x4A, 0x31, 0x0A, 0x65, 0x58, 0x42, 0x68, 0x63, 0x33, 0x4D, 0x67, 0x51, 0x56, 0x4D, 0x74, 0x4F, 0x54, 0x67, 0x7A, 0x4D, 0x54, 0x59, 0x7A, 0x4D, + 0x7A, 0x49, 0x33, 0x4D, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x64, 0x43, 0x64, 0x58, 0x6C, 0x77, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x45, 0x4E, 0x73, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x44, 0x4D, 0x67, 0x55, + 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, + 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4B, 0x58, 0x61, 0x43, 0x70, 0x55, 0x57, 0x55, 0x4F, 0x4F, 0x56, 0x38, 0x6C, 0x36, 0x64, 0x64, 0x6A, 0x45, 0x47, 0x4D, 0x6E, 0x71, 0x62, 0x38, 0x52, 0x42, 0x32, + 0x75, 0x41, 0x43, 0x61, 0x74, 0x56, 0x49, 0x32, 0x7A, 0x53, 0x52, 0x48, 0x0A, 0x73, 0x4A, 0x38, 0x59, 0x5A, 0x4C, 0x79, 0x61, 0x39, 0x76, 0x72, 0x56, 0x65, 0x64, 0x69, 0x51, 0x59, 0x6B, 0x77, 0x69, 0x4C, 0x39, 0x34, 0x34, 0x50, 0x64, 0x62, + 0x67, 0x71, 0x4F, 0x6B, 0x63, 0x4C, 0x4E, 0x74, 0x34, 0x45, 0x65, 0x6D, 0x4F, 0x61, 0x46, 0x45, 0x56, 0x63, 0x73, 0x66, 0x7A, 0x4D, 0x34, 0x66, 0x6B, 0x6F, 0x46, 0x30, 0x4C, 0x58, 0x4F, 0x42, 0x58, 0x42, 0x79, 0x6F, 0x77, 0x39, 0x63, 0x33, + 0x45, 0x4E, 0x33, 0x63, 0x6F, 0x54, 0x52, 0x69, 0x52, 0x0A, 0x35, 0x72, 0x2F, 0x56, 0x55, 0x76, 0x31, 0x78, 0x4C, 0x58, 0x41, 0x2B, 0x35, 0x38, 0x62, 0x45, 0x69, 0x75, 0x50, 0x77, 0x4B, 0x41, 0x76, 0x30, 0x64, 0x70, 0x69, 0x68, 0x69, 0x34, + 0x64, 0x56, 0x73, 0x6A, 0x6F, 0x54, 0x2F, 0x4C, 0x63, 0x2B, 0x4A, 0x7A, 0x65, 0x4F, 0x49, 0x75, 0x4F, 0x6F, 0x54, 0x79, 0x72, 0x76, 0x59, 0x4C, 0x73, 0x39, 0x74, 0x7A, 0x6E, 0x44, 0x44, 0x67, 0x46, 0x48, 0x6D, 0x56, 0x30, 0x53, 0x54, 0x39, + 0x74, 0x44, 0x2B, 0x6C, 0x65, 0x68, 0x0A, 0x37, 0x66, 0x6D, 0x64, 0x76, 0x68, 0x46, 0x48, 0x4A, 0x6C, 0x73, 0x54, 0x6D, 0x4B, 0x74, 0x64, 0x46, 0x6F, 0x71, 0x77, 0x4E, 0x78, 0x78, 0x58, 0x6E, 0x55, 0x58, 0x2F, 0x69, 0x4A, 0x59, 0x32, 0x76, + 0x37, 0x76, 0x4B, 0x42, 0x33, 0x74, 0x76, 0x68, 0x32, 0x50, 0x58, 0x30, 0x44, 0x4A, 0x71, 0x31, 0x6C, 0x31, 0x73, 0x44, 0x50, 0x47, 0x7A, 0x62, 0x6A, 0x6E, 0x69, 0x61, 0x7A, 0x45, 0x75, 0x4F, 0x51, 0x41, 0x6E, 0x46, 0x4E, 0x34, 0x34, 0x77, + 0x4F, 0x77, 0x5A, 0x0A, 0x5A, 0x6F, 0x59, 0x53, 0x36, 0x4A, 0x31, 0x79, 0x46, 0x68, 0x4E, 0x6B, 0x55, 0x73, 0x65, 0x70, 0x4E, 0x78, 0x7A, 0x39, 0x67, 0x6A, 0x44, 0x74, 0x68, 0x42, 0x67, 0x64, 0x39, 0x4B, 0x35, 0x63, 0x2F, 0x33, 0x41, 0x54, + 0x41, 0x4F, 0x75, 0x78, 0x39, 0x54, 0x4E, 0x36, 0x53, 0x39, 0x5A, 0x56, 0x2B, 0x41, 0x57, 0x4E, 0x53, 0x32, 0x6D, 0x77, 0x39, 0x62, 0x4D, 0x6F, 0x4E, 0x6C, 0x77, 0x55, 0x78, 0x46, 0x46, 0x7A, 0x54, 0x57, 0x73, 0x4C, 0x38, 0x54, 0x51, 0x48, + 0x0A, 0x32, 0x78, 0x63, 0x35, 0x31, 0x39, 0x77, 0x6F, 0x65, 0x32, 0x76, 0x31, 0x6E, 0x2F, 0x4D, 0x75, 0x77, 0x55, 0x38, 0x58, 0x4B, 0x68, 0x44, 0x7A, 0x7A, 0x4D, 0x72, 0x6F, 0x36, 0x2F, 0x31, 0x72, 0x71, 0x79, 0x36, 0x61, 0x6E, 0x79, 0x32, + 0x43, 0x62, 0x67, 0x54, 0x55, 0x55, 0x67, 0x47, 0x54, 0x4C, 0x54, 0x32, 0x47, 0x2F, 0x48, 0x37, 0x38, 0x33, 0x2B, 0x39, 0x43, 0x48, 0x61, 0x5A, 0x72, 0x37, 0x37, 0x6B, 0x67, 0x78, 0x76, 0x65, 0x39, 0x6F, 0x4B, 0x65, 0x56, 0x0A, 0x2F, 0x61, + 0x66, 0x6D, 0x69, 0x53, 0x54, 0x59, 0x7A, 0x49, 0x77, 0x30, 0x62, 0x4F, 0x49, 0x6A, 0x4C, 0x39, 0x6B, 0x53, 0x47, 0x69, 0x47, 0x35, 0x56, 0x5A, 0x46, 0x76, 0x43, 0x35, 0x46, 0x35, 0x47, 0x51, 0x79, 0x74, 0x51, 0x49, 0x67, 0x4C, 0x63, 0x4F, + 0x4A, 0x36, 0x30, 0x67, 0x37, 0x59, 0x61, 0x45, 0x69, 0x37, 0x67, 0x68, 0x4D, 0x35, 0x45, 0x46, 0x6A, 0x70, 0x32, 0x43, 0x6F, 0x48, 0x78, 0x68, 0x4C, 0x62, 0x57, 0x4E, 0x76, 0x53, 0x4F, 0x31, 0x55, 0x51, 0x0A, 0x52, 0x77, 0x55, 0x56, 0x5A, + 0x32, 0x4A, 0x2B, 0x47, 0x47, 0x4F, 0x6D, 0x52, 0x6A, 0x38, 0x4A, 0x44, 0x6C, 0x51, 0x79, 0x58, 0x72, 0x38, 0x4E, 0x59, 0x6E, 0x6F, 0x6E, 0x37, 0x34, 0x44, 0x6F, 0x32, 0x39, 0x6C, 0x4C, 0x42, 0x6C, 0x6F, 0x33, 0x57, 0x69, 0x58, 0x51, 0x43, + 0x42, 0x4A, 0x33, 0x31, 0x47, 0x38, 0x4A, 0x55, 0x4A, 0x63, 0x39, 0x79, 0x42, 0x33, 0x44, 0x33, 0x34, 0x78, 0x46, 0x4D, 0x46, 0x62, 0x47, 0x30, 0x32, 0x53, 0x72, 0x5A, 0x76, 0x50, 0x41, 0x0A, 0x58, 0x70, 0x61, 0x63, 0x77, 0x38, 0x54, 0x76, + 0x77, 0x33, 0x78, 0x72, 0x69, 0x7A, 0x70, 0x35, 0x66, 0x37, 0x4E, 0x4A, 0x7A, 0x7A, 0x33, 0x69, 0x69, 0x5A, 0x2B, 0x67, 0x4D, 0x45, 0x75, 0x46, 0x75, 0x5A, 0x79, 0x55, 0x4A, 0x48, 0x6D, 0x50, 0x66, 0x57, 0x75, 0x70, 0x52, 0x57, 0x67, 0x50, + 0x4B, 0x39, 0x44, 0x78, 0x32, 0x68, 0x7A, 0x4C, 0x61, 0x62, 0x6A, 0x4B, 0x53, 0x57, 0x4A, 0x74, 0x79, 0x4E, 0x42, 0x6A, 0x59, 0x74, 0x31, 0x67, 0x44, 0x31, 0x69, 0x71, 0x0A, 0x6A, 0x36, 0x47, 0x38, 0x42, 0x61, 0x56, 0x6D, 0x6F, 0x73, 0x38, + 0x62, 0x64, 0x72, 0x4B, 0x45, 0x5A, 0x4C, 0x46, 0x4D, 0x4F, 0x56, 0x4C, 0x41, 0x4D, 0x4C, 0x72, 0x77, 0x6A, 0x45, 0x73, 0x43, 0x73, 0x4C, 0x61, 0x33, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, + 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x45, 0x65, 0x34, 0x7A, 0x66, + 0x2F, 0x6C, 0x62, 0x2B, 0x37, 0x34, 0x73, 0x75, 0x77, 0x76, 0x54, 0x67, 0x37, 0x35, 0x4A, 0x62, 0x43, 0x4F, 0x50, 0x47, 0x76, 0x44, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, + 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x0A, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x41, 0x43, 0x41, 0x6A, 0x51, 0x54, 0x55, 0x45, 0x6B, + 0x4D, 0x4A, 0x41, 0x59, 0x6D, 0x44, 0x76, 0x34, 0x6A, 0x56, 0x4D, 0x31, 0x7A, 0x2B, 0x73, 0x34, 0x6A, 0x53, 0x51, 0x75, 0x4B, 0x46, 0x76, 0x64, 0x76, 0x6F, 0x57, 0x46, 0x71, 0x52, 0x49, 0x4E, 0x79, 0x7A, 0x70, 0x6B, 0x4D, 0x4C, 0x79, 0x50, + 0x50, 0x67, 0x4B, 0x6E, 0x39, 0x69, 0x42, 0x35, 0x62, 0x74, 0x62, 0x32, 0x69, 0x55, 0x73, 0x70, 0x4B, 0x64, 0x56, 0x0A, 0x63, 0x53, 0x51, 0x79, 0x39, 0x73, 0x67, 0x4C, 0x38, 0x72, 0x78, 0x71, 0x2B, 0x4A, 0x4F, 0x73, 0x73, 0x67, 0x66, 0x43, + 0x58, 0x35, 0x2F, 0x62, 0x7A, 0x4D, 0x69, 0x4B, 0x71, 0x72, 0x35, 0x71, 0x62, 0x2B, 0x46, 0x4A, 0x45, 0x4D, 0x77, 0x78, 0x31, 0x34, 0x43, 0x37, 0x75, 0x38, 0x6A, 0x59, 0x6F, 0x67, 0x35, 0x6B, 0x56, 0x2B, 0x71, 0x69, 0x39, 0x63, 0x4B, 0x70, + 0x4D, 0x52, 0x58, 0x53, 0x49, 0x47, 0x72, 0x73, 0x2F, 0x43, 0x49, 0x42, 0x4B, 0x4D, 0x2B, 0x47, 0x0A, 0x75, 0x49, 0x41, 0x65, 0x71, 0x63, 0x77, 0x52, 0x70, 0x54, 0x7A, 0x79, 0x46, 0x72, 0x4E, 0x48, 0x6E, 0x66, 0x7A, 0x53, 0x67, 0x43, 0x48, + 0x45, 0x79, 0x39, 0x42, 0x48, 0x63, 0x45, 0x47, 0x68, 0x79, 0x6F, 0x4D, 0x5A, 0x43, 0x43, 0x78, 0x74, 0x38, 0x6C, 0x31, 0x33, 0x6E, 0x49, 0x6F, 0x55, 0x45, 0x39, 0x51, 0x32, 0x48, 0x4A, 0x4C, 0x77, 0x35, 0x51, 0x59, 0x33, 0x33, 0x4B, 0x62, + 0x6D, 0x6B, 0x4A, 0x73, 0x34, 0x6A, 0x31, 0x78, 0x72, 0x47, 0x30, 0x61, 0x47, 0x0A, 0x51, 0x30, 0x4A, 0x66, 0x50, 0x67, 0x45, 0x48, 0x55, 0x31, 0x52, 0x64, 0x5A, 0x58, 0x33, 0x33, 0x69, 0x6E, 0x4F, 0x68, 0x6D, 0x6C, 0x52, 0x61, 0x48, 0x79, + 0x6C, 0x44, 0x46, 0x43, 0x66, 0x43, 0x68, 0x51, 0x2B, 0x31, 0x69, 0x48, 0x73, 0x61, 0x4F, 0x35, 0x53, 0x33, 0x48, 0x57, 0x43, 0x6E, 0x74, 0x5A, 0x7A, 0x6E, 0x4B, 0x57, 0x6C, 0x58, 0x57, 0x70, 0x75, 0x54, 0x65, 0x6B, 0x4D, 0x77, 0x47, 0x77, + 0x50, 0x58, 0x59, 0x73, 0x68, 0x41, 0x70, 0x71, 0x72, 0x38, 0x0A, 0x5A, 0x4F, 0x52, 0x4B, 0x31, 0x35, 0x46, 0x54, 0x41, 0x61, 0x67, 0x67, 0x69, 0x47, 0x36, 0x63, 0x58, 0x30, 0x53, 0x35, 0x79, 0x32, 0x43, 0x42, 0x4E, 0x4F, 0x78, 0x76, 0x30, + 0x33, 0x33, 0x61, 0x53, 0x46, 0x2F, 0x72, 0x74, 0x4A, 0x43, 0x38, 0x4C, 0x61, 0x6B, 0x63, 0x43, 0x36, 0x77, 0x63, 0x31, 0x61, 0x4A, 0x6F, 0x49, 0x49, 0x41, 0x45, 0x31, 0x76, 0x79, 0x78, 0x6A, 0x79, 0x2B, 0x37, 0x53, 0x6A, 0x45, 0x4E, 0x53, + 0x6F, 0x59, 0x63, 0x36, 0x2B, 0x49, 0x32, 0x0A, 0x4B, 0x53, 0x62, 0x31, 0x32, 0x74, 0x6A, 0x45, 0x38, 0x6E, 0x56, 0x68, 0x7A, 0x33, 0x36, 0x75, 0x64, 0x6D, 0x4E, 0x4B, 0x65, 0x6B, 0x42, 0x6C, 0x6B, 0x34, 0x66, 0x34, 0x48, 0x6F, 0x43, 0x4D, + 0x68, 0x75, 0x57, 0x47, 0x31, 0x6F, 0x38, 0x4F, 0x2F, 0x46, 0x4D, 0x73, 0x59, 0x4F, 0x67, 0x57, 0x59, 0x52, 0x71, 0x69, 0x50, 0x6B, 0x4E, 0x37, 0x7A, 0x54, 0x6C, 0x67, 0x56, 0x47, 0x72, 0x31, 0x38, 0x6F, 0x6B, 0x6D, 0x41, 0x57, 0x69, 0x44, + 0x53, 0x4B, 0x49, 0x7A, 0x0A, 0x36, 0x4D, 0x6B, 0x45, 0x6B, 0x62, 0x49, 0x52, 0x4E, 0x42, 0x45, 0x2B, 0x36, 0x74, 0x42, 0x44, 0x47, 0x52, 0x38, 0x44, 0x6B, 0x35, 0x41, 0x4D, 0x2F, 0x31, 0x45, 0x39, 0x56, 0x2F, 0x52, 0x42, 0x62, 0x75, 0x48, + 0x4C, 0x6F, 0x4C, 0x37, 0x72, 0x79, 0x57, 0x50, 0x4E, 0x62, 0x63, 0x7A, 0x6B, 0x2B, 0x44, 0x61, 0x71, 0x61, 0x4A, 0x33, 0x74, 0x76, 0x56, 0x32, 0x58, 0x63, 0x45, 0x51, 0x4E, 0x74, 0x67, 0x34, 0x31, 0x33, 0x4F, 0x45, 0x4D, 0x58, 0x62, 0x75, + 0x67, 0x0A, 0x55, 0x5A, 0x54, 0x4C, 0x66, 0x68, 0x62, 0x72, 0x45, 0x53, 0x2B, 0x6A, 0x6B, 0x6B, 0x58, 0x49, 0x54, 0x48, 0x48, 0x5A, 0x76, 0x4D, 0x6D, 0x5A, 0x55, 0x6C, 0x64, 0x47, 0x4C, 0x31, 0x44, 0x50, 0x76, 0x54, 0x56, 0x70, 0x39, 0x44, + 0x30, 0x56, 0x7A, 0x67, 0x61, 0x6C, 0x4C, 0x41, 0x38, 0x2B, 0x39, 0x6F, 0x47, 0x36, 0x6C, 0x4C, 0x76, 0x44, 0x75, 0x37, 0x39, 0x6C, 0x65, 0x4E, 0x4B, 0x47, 0x65, 0x66, 0x39, 0x4A, 0x4F, 0x78, 0x71, 0x44, 0x44, 0x50, 0x44, 0x65, 0x0A, 0x65, + 0x4F, 0x7A, 0x49, 0x38, 0x6B, 0x31, 0x4D, 0x47, 0x74, 0x36, 0x43, 0x4B, 0x66, 0x6A, 0x42, 0x57, 0x74, 0x72, 0x74, 0x37, 0x75, 0x59, 0x6E, 0x58, 0x75, 0x68, 0x46, 0x30, 0x4A, 0x30, 0x63, 0x55, 0x61, 0x68, 0x6F, 0x71, 0x30, 0x54, 0x6A, 0x30, + 0x49, 0x74, 0x71, 0x34, 0x2F, 0x67, 0x37, 0x75, 0x39, 0x78, 0x4E, 0x31, 0x32, 0x54, 0x79, 0x55, 0x62, 0x37, 0x6D, 0x71, 0x71, 0x74, 0x61, 0x36, 0x54, 0x48, 0x75, 0x42, 0x72, 0x78, 0x7A, 0x76, 0x78, 0x4E, 0x69, 0x0A, 0x43, 0x70, 0x2F, 0x48, + 0x75, 0x5A, 0x63, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x2D, 0x54, 0x65, 0x6C, 0x65, 0x53, 0x65, + 0x63, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x6C, 0x61, 0x73, 0x73, 0x20, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, + 0x49, 0x44, 0x77, 0x7A, 0x43, 0x43, 0x41, 0x71, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, + 0x43, 0x42, 0x67, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x0A, 0x49, 0x6C, 0x51, 0x74, 0x55, + 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x46, 0x62, 0x6E, 0x52, 0x6C, 0x63, 0x6E, 0x42, 0x79, 0x61, 0x58, 0x4E, 0x6C, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x64, 0x74, 0x59, + 0x6B, 0x67, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x46, 0x6C, 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x55, 0x0A, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x44, + 0x5A, 0x57, 0x35, 0x30, 0x5A, 0x58, 0x49, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x46, 0x51, 0x74, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x56, 0x4E, 0x6C, 0x59, 0x79, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, + 0x59, 0x57, 0x78, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x73, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x44, 0x4D, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x67, 0x78, 0x0A, 0x4D, 0x44, 0x41, 0x78, 0x4D, 0x54, 0x41, 0x79, 0x4F, 0x54, 0x55, + 0x32, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x4D, 0x78, 0x4D, 0x44, 0x41, 0x78, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x43, 0x42, 0x67, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, + 0x43, 0x52, 0x45, 0x55, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x49, 0x6C, 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x0A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x46, 0x62, 0x6E, 0x52, 0x6C, 0x63, 0x6E, + 0x42, 0x79, 0x61, 0x58, 0x4E, 0x6C, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x46, 0x6C, + 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x44, 0x0A, 0x5A, 0x57, 0x35, 0x30, 0x5A, 0x58, 0x49, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x4D, 0x4D, 0x48, 0x46, 0x51, 0x74, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x56, 0x4E, 0x6C, 0x59, 0x79, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x73, 0x59, 0x58, 0x4E, 0x7A, 0x49, + 0x44, 0x4D, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0A, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, + 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x39, 0x64, 0x5A, 0x50, 0x77, 0x59, 0x69, 0x4A, 0x76, 0x4A, 0x4B, 0x37, 0x67, 0x65, 0x6E, 0x61, 0x73, 0x66, 0x62, 0x33, 0x5A, 0x4A, 0x4E, 0x57, 0x34, 0x74, 0x2F, 0x7A, 0x4E, 0x38, 0x45, 0x4C, 0x67, + 0x36, 0x33, 0x69, 0x49, 0x56, 0x6C, 0x36, 0x62, 0x6D, 0x6C, 0x51, 0x64, 0x54, 0x51, 0x79, 0x4B, 0x0A, 0x39, 0x74, 0x50, 0x50, 0x63, 0x50, 0x52, 0x53, 0x74, 0x64, 0x69, 0x54, 0x42, 0x4F, 0x4E, 0x47, 0x68, 0x6E, 0x46, 0x42, 0x53, 0x69, 0x76, + 0x77, 0x4B, 0x69, 0x78, 0x56, 0x41, 0x39, 0x5A, 0x49, 0x77, 0x2B, 0x41, 0x35, 0x4F, 0x4F, 0x33, 0x79, 0x58, 0x44, 0x77, 0x2F, 0x52, 0x4C, 0x79, 0x54, 0x50, 0x57, 0x47, 0x72, 0x54, 0x73, 0x30, 0x4E, 0x76, 0x76, 0x41, 0x67, 0x4A, 0x31, 0x67, + 0x4F, 0x52, 0x48, 0x38, 0x45, 0x47, 0x6F, 0x65, 0x6C, 0x31, 0x35, 0x59, 0x55, 0x0A, 0x4E, 0x70, 0x44, 0x51, 0x53, 0x58, 0x75, 0x68, 0x64, 0x66, 0x73, 0x61, 0x61, 0x33, 0x4F, 0x78, 0x2B, 0x4D, 0x36, 0x70, 0x43, 0x53, 0x7A, 0x79, 0x55, 0x39, + 0x58, 0x44, 0x46, 0x45, 0x53, 0x34, 0x68, 0x71, 0x58, 0x32, 0x69, 0x79, 0x73, 0x35, 0x32, 0x71, 0x4D, 0x7A, 0x56, 0x4E, 0x6E, 0x36, 0x63, 0x68, 0x72, 0x33, 0x49, 0x68, 0x55, 0x63, 0x69, 0x4A, 0x46, 0x72, 0x66, 0x32, 0x62, 0x6C, 0x77, 0x32, + 0x71, 0x41, 0x73, 0x43, 0x54, 0x7A, 0x33, 0x34, 0x5A, 0x46, 0x0A, 0x69, 0x50, 0x30, 0x5A, 0x66, 0x33, 0x57, 0x48, 0x48, 0x78, 0x2B, 0x78, 0x47, 0x77, 0x70, 0x7A, 0x4A, 0x46, 0x75, 0x35, 0x5A, 0x65, 0x41, 0x73, 0x56, 0x4D, 0x68, 0x67, 0x30, + 0x32, 0x59, 0x58, 0x50, 0x2B, 0x48, 0x4D, 0x56, 0x44, 0x4E, 0x7A, 0x6B, 0x51, 0x49, 0x36, 0x70, 0x6E, 0x39, 0x37, 0x64, 0x6A, 0x6D, 0x69, 0x48, 0x35, 0x61, 0x32, 0x4F, 0x4B, 0x36, 0x31, 0x79, 0x4A, 0x4E, 0x30, 0x48, 0x5A, 0x36, 0x35, 0x74, + 0x4F, 0x56, 0x67, 0x6E, 0x53, 0x39, 0x57, 0x0A, 0x30, 0x65, 0x44, 0x72, 0x58, 0x6C, 0x74, 0x4D, 0x45, 0x6E, 0x41, 0x4D, 0x62, 0x45, 0x51, 0x67, 0x71, 0x78, 0x48, 0x59, 0x39, 0x42, 0x6E, 0x32, 0x30, 0x70, 0x78, 0x53, 0x4E, 0x2B, 0x66, 0x36, + 0x74, 0x73, 0x49, 0x78, 0x4F, 0x30, 0x72, 0x55, 0x46, 0x4A, 0x6D, 0x74, 0x78, 0x78, 0x72, 0x31, 0x58, 0x56, 0x2F, 0x36, 0x42, 0x37, 0x68, 0x38, 0x44, 0x52, 0x2F, 0x57, 0x67, 0x78, 0x36, 0x7A, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, + 0x51, 0x6A, 0x42, 0x41, 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, + 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x31, 0x41, 0x2F, 0x64, 0x32, 0x4F, 0x32, 0x47, 0x43, 0x61, 0x68, 0x4B, 0x71, 0x47, 0x46, 0x50, + 0x72, 0x0A, 0x41, 0x79, 0x47, 0x55, 0x76, 0x2F, 0x37, 0x4F, 0x79, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x45, 0x41, 0x56, 0x6A, + 0x33, 0x76, 0x6C, 0x4E, 0x57, 0x39, 0x32, 0x6E, 0x4F, 0x79, 0x57, 0x4C, 0x36, 0x75, 0x6B, 0x4B, 0x32, 0x59, 0x4A, 0x35, 0x66, 0x2B, 0x41, 0x62, 0x47, 0x77, 0x55, 0x67, 0x43, 0x34, 0x54, 0x65, 0x51, 0x62, 0x49, 0x58, 0x51, 0x62, 0x0A, 0x66, + 0x73, 0x44, 0x75, 0x58, 0x6D, 0x6B, 0x71, 0x4A, 0x61, 0x39, 0x63, 0x31, 0x68, 0x33, 0x61, 0x30, 0x6E, 0x6E, 0x4A, 0x38, 0x35, 0x63, 0x70, 0x34, 0x49, 0x61, 0x48, 0x33, 0x67, 0x52, 0x5A, 0x44, 0x2F, 0x46, 0x5A, 0x31, 0x47, 0x53, 0x46, 0x53, + 0x35, 0x6D, 0x76, 0x4A, 0x51, 0x51, 0x65, 0x79, 0x55, 0x61, 0x70, 0x6C, 0x39, 0x36, 0x43, 0x73, 0x68, 0x74, 0x77, 0x6E, 0x35, 0x7A, 0x32, 0x72, 0x33, 0x45, 0x78, 0x33, 0x58, 0x73, 0x46, 0x70, 0x53, 0x7A, 0x54, 0x0A, 0x75, 0x63, 0x70, 0x48, + 0x39, 0x73, 0x72, 0x79, 0x39, 0x75, 0x65, 0x74, 0x75, 0x55, 0x67, 0x2F, 0x76, 0x42, 0x61, 0x33, 0x77, 0x57, 0x33, 0x30, 0x36, 0x67, 0x6D, 0x76, 0x37, 0x50, 0x4F, 0x31, 0x35, 0x77, 0x57, 0x65, 0x70, 0x68, 0x36, 0x4B, 0x55, 0x31, 0x48, 0x57, + 0x6B, 0x34, 0x48, 0x4D, 0x64, 0x4A, 0x50, 0x32, 0x75, 0x64, 0x71, 0x6D, 0x4A, 0x51, 0x56, 0x30, 0x65, 0x56, 0x70, 0x2B, 0x51, 0x44, 0x36, 0x43, 0x53, 0x79, 0x59, 0x52, 0x4D, 0x47, 0x37, 0x68, 0x0A, 0x50, 0x30, 0x48, 0x48, 0x52, 0x77, 0x41, + 0x31, 0x31, 0x66, 0x58, 0x54, 0x39, 0x31, 0x51, 0x2B, 0x67, 0x54, 0x33, 0x61, 0x53, 0x57, 0x71, 0x61, 0x73, 0x2B, 0x38, 0x51, 0x50, 0x65, 0x62, 0x72, 0x62, 0x39, 0x48, 0x49, 0x49, 0x6B, 0x66, 0x4C, 0x7A, 0x4D, 0x38, 0x42, 0x4D, 0x5A, 0x4C, + 0x5A, 0x47, 0x4F, 0x4D, 0x69, 0x76, 0x67, 0x6B, 0x65, 0x47, 0x6A, 0x35, 0x61, 0x73, 0x75, 0x52, 0x72, 0x44, 0x46, 0x52, 0x36, 0x66, 0x55, 0x4E, 0x4F, 0x75, 0x49, 0x6D, 0x6C, 0x0A, 0x65, 0x39, 0x65, 0x69, 0x50, 0x5A, 0x61, 0x47, 0x7A, 0x50, + 0x49, 0x6D, 0x4E, 0x43, 0x31, 0x71, 0x6B, 0x70, 0x32, 0x61, 0x47, 0x74, 0x41, 0x77, 0x34, 0x6C, 0x31, 0x4F, 0x42, 0x4C, 0x42, 0x66, 0x69, 0x79, 0x42, 0x2B, 0x64, 0x38, 0x45, 0x39, 0x6C, 0x59, 0x4C, 0x52, 0x52, 0x70, 0x6F, 0x37, 0x50, 0x48, + 0x69, 0x34, 0x62, 0x36, 0x48, 0x51, 0x44, 0x57, 0x53, 0x69, 0x65, 0x42, 0x34, 0x70, 0x54, 0x70, 0x50, 0x44, 0x70, 0x46, 0x51, 0x55, 0x57, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x2D, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x6C, 0x61, 0x73, 0x73, 0x20, 0x33, 0x20, 0x43, 0x41, 0x20, 0x32, 0x20, + 0x32, 0x30, 0x30, 0x39, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x4D, 0x7A, 0x43, 0x43, 0x41, 0x78, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, + 0x41, 0x67, 0x49, 0x44, 0x43, 0x59, 0x50, 0x7A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x45, 0x30, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x52, 0x46, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x44, 0x41, 0x78, 0x45, 0x4C, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, + 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x6B, 0x51, 0x74, 0x56, 0x46, 0x4A, 0x56, 0x55, 0x31, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x62, 0x47, 0x46, 0x7A, 0x63, 0x79, 0x41, + 0x7A, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x4D, 0x6A, 0x41, 0x77, 0x4F, 0x54, 0x41, 0x65, 0x0A, 0x46, 0x77, 0x30, 0x77, 0x4F, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x44, 0x4D, 0x31, 0x4E, 0x54, 0x68, 0x61, 0x46, 0x77, + 0x30, 0x79, 0x4F, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x44, 0x4D, 0x31, 0x4E, 0x54, 0x68, 0x61, 0x4D, 0x45, 0x30, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x52, 0x46, 0x4D, 0x52, + 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x78, 0x45, 0x0A, 0x4C, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x4D, 0x4D, 0x48, 0x6B, 0x51, 0x74, 0x56, 0x46, 0x4A, 0x56, 0x55, 0x31, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x62, 0x47, 0x46, 0x7A, 0x63, 0x79, 0x41, 0x7A, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x4D, + 0x6A, 0x41, 0x77, 0x4F, 0x54, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, + 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4E, 0x4F, 0x79, 0x53, 0x73, 0x39, 0x36, 0x52, 0x2B, 0x39, 0x31, 0x6D, 0x79, 0x50, 0x36, 0x4F, 0x69, 0x2F, 0x57, 0x55, 0x45, 0x57, 0x4A, 0x4E, 0x54, 0x72, 0x47, 0x61, 0x39, 0x76, 0x2B, + 0x32, 0x77, 0x42, 0x6F, 0x71, 0x4F, 0x41, 0x44, 0x0A, 0x45, 0x52, 0x30, 0x33, 0x55, 0x41, 0x69, 0x66, 0x54, 0x55, 0x70, 0x6F, 0x6C, 0x44, 0x57, 0x7A, 0x55, 0x39, 0x47, 0x55, 0x59, 0x36, 0x63, 0x67, 0x56, 0x71, 0x2F, 0x65, 0x55, 0x58, 0x6A, + 0x73, 0x4B, 0x6A, 0x33, 0x7A, 0x53, 0x45, 0x68, 0x51, 0x50, 0x67, 0x72, 0x66, 0x52, 0x6C, 0x57, 0x4C, 0x4A, 0x32, 0x33, 0x44, 0x45, 0x45, 0x30, 0x4E, 0x6B, 0x56, 0x4A, 0x44, 0x32, 0x49, 0x66, 0x67, 0x58, 0x55, 0x34, 0x32, 0x74, 0x53, 0x48, + 0x4B, 0x58, 0x7A, 0x6C, 0x41, 0x0A, 0x42, 0x46, 0x39, 0x62, 0x66, 0x73, 0x79, 0x6A, 0x78, 0x69, 0x75, 0x70, 0x51, 0x42, 0x37, 0x5A, 0x4E, 0x6F, 0x54, 0x57, 0x53, 0x50, 0x4F, 0x53, 0x48, 0x6A, 0x52, 0x47, 0x49, 0x43, 0x54, 0x42, 0x70, 0x46, + 0x47, 0x4F, 0x53, 0x68, 0x72, 0x76, 0x55, 0x44, 0x39, 0x70, 0x58, 0x52, 0x6C, 0x2F, 0x52, 0x63, 0x50, 0x48, 0x41, 0x59, 0x39, 0x52, 0x79, 0x53, 0x50, 0x6F, 0x63, 0x71, 0x36, 0x30, 0x76, 0x46, 0x59, 0x4A, 0x66, 0x78, 0x4C, 0x4C, 0x48, 0x4C, + 0x47, 0x76, 0x0A, 0x4B, 0x5A, 0x41, 0x4B, 0x79, 0x56, 0x58, 0x4D, 0x44, 0x39, 0x4F, 0x30, 0x47, 0x75, 0x31, 0x48, 0x4E, 0x56, 0x70, 0x4B, 0x37, 0x5A, 0x78, 0x7A, 0x42, 0x43, 0x48, 0x51, 0x71, 0x72, 0x30, 0x4D, 0x45, 0x37, 0x55, 0x41, 0x79, + 0x69, 0x5A, 0x73, 0x78, 0x47, 0x73, 0x4D, 0x6C, 0x46, 0x71, 0x56, 0x6C, 0x4E, 0x70, 0x51, 0x6D, 0x76, 0x48, 0x2F, 0x70, 0x53, 0x74, 0x6D, 0x4D, 0x61, 0x54, 0x4A, 0x4F, 0x4B, 0x44, 0x66, 0x48, 0x52, 0x2B, 0x34, 0x43, 0x53, 0x37, 0x7A, 0x0A, + 0x70, 0x2B, 0x68, 0x6E, 0x55, 0x71, 0x75, 0x56, 0x48, 0x2B, 0x42, 0x47, 0x50, 0x74, 0x69, 0x6B, 0x77, 0x38, 0x70, 0x61, 0x78, 0x54, 0x47, 0x41, 0x36, 0x45, 0x69, 0x61, 0x6E, 0x35, 0x52, 0x70, 0x2F, 0x68, 0x6E, 0x64, 0x32, 0x48, 0x4E, 0x38, + 0x67, 0x63, 0x71, 0x57, 0x33, 0x6F, 0x37, 0x74, 0x73, 0x7A, 0x49, 0x46, 0x5A, 0x59, 0x51, 0x30, 0x35, 0x75, 0x62, 0x39, 0x56, 0x78, 0x43, 0x31, 0x58, 0x33, 0x61, 0x2F, 0x4C, 0x37, 0x41, 0x51, 0x44, 0x63, 0x55, 0x43, 0x0A, 0x41, 0x77, 0x45, + 0x41, 0x41, 0x61, 0x4F, 0x43, 0x41, 0x52, 0x6F, 0x77, 0x67, 0x67, 0x45, 0x57, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, + 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x50, 0x33, 0x61, 0x46, 0x4D, 0x53, 0x66, 0x4D, 0x4E, 0x34, 0x68, 0x76, 0x52, 0x35, 0x43, 0x4F, 0x66, 0x79, 0x72, 0x59, 0x79, 0x4E, 0x4A, 0x0A, 0x34, 0x50, 0x47, 0x45, 0x4D, 0x41, + 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x43, 0x42, 0x30, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x66, 0x42, 0x49, 0x48, 0x4C, 0x4D, 0x49, 0x48, 0x49, 0x4D, 0x49, + 0x47, 0x41, 0x6F, 0x48, 0x36, 0x67, 0x66, 0x49, 0x5A, 0x36, 0x62, 0x47, 0x52, 0x68, 0x63, 0x44, 0x6F, 0x76, 0x4C, 0x32, 0x52, 0x70, 0x63, 0x6D, 0x56, 0x6A, 0x64, 0x47, 0x39, 0x79, 0x0A, 0x65, 0x53, 0x35, 0x6B, 0x4C, 0x58, 0x52, 0x79, 0x64, + 0x58, 0x4E, 0x30, 0x4C, 0x6D, 0x35, 0x6C, 0x64, 0x43, 0x39, 0x44, 0x54, 0x6A, 0x31, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x4A, 0x54, 0x49, 0x77, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x55, 0x79, 0x4D, 0x45, 0x4E, 0x73, 0x59, + 0x58, 0x4E, 0x7A, 0x4A, 0x54, 0x49, 0x77, 0x4D, 0x79, 0x55, 0x79, 0x4D, 0x45, 0x4E, 0x42, 0x4A, 0x54, 0x49, 0x77, 0x4D, 0x69, 0x55, 0x79, 0x4D, 0x44, 0x49, 0x77, 0x0A, 0x4D, 0x44, 0x6B, 0x73, 0x54, 0x7A, 0x31, 0x45, 0x4C, 0x56, 0x52, 0x79, + 0x64, 0x58, 0x4E, 0x30, 0x4A, 0x54, 0x49, 0x77, 0x52, 0x32, 0x31, 0x69, 0x53, 0x43, 0x78, 0x44, 0x50, 0x55, 0x52, 0x46, 0x50, 0x32, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x6C, 0x63, 0x6D, 0x56, 0x32, + 0x62, 0x32, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6D, 0x78, 0x70, 0x63, 0x33, 0x51, 0x77, 0x51, 0x36, 0x42, 0x42, 0x6F, 0x44, 0x2B, 0x47, 0x0A, 0x50, 0x57, 0x68, 0x30, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x33, 0x64, 0x33, 0x63, + 0x75, 0x5A, 0x43, 0x31, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x35, 0x75, 0x5A, 0x58, 0x51, 0x76, 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x32, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x66, 0x63, 0x6D, 0x39, 0x76, 0x64, 0x46, 0x39, + 0x6A, 0x62, 0x47, 0x46, 0x7A, 0x63, 0x31, 0x38, 0x7A, 0x58, 0x32, 0x4E, 0x68, 0x58, 0x7A, 0x4A, 0x66, 0x4D, 0x6A, 0x41, 0x77, 0x0A, 0x4F, 0x53, 0x35, 0x6A, 0x63, 0x6D, 0x77, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, + 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x48, 0x2B, 0x58, 0x32, 0x7A, 0x44, 0x49, 0x33, 0x36, 0x53, 0x63, 0x66, 0x53, 0x46, 0x36, 0x67, 0x48, 0x44, 0x4F, 0x46, 0x42, 0x4A, 0x70, 0x69, 0x42, + 0x53, 0x56, 0x59, 0x45, 0x51, 0x42, 0x72, 0x4C, 0x4C, 0x70, 0x4D, 0x45, 0x2B, 0x62, 0x55, 0x4D, 0x4A, 0x6D, 0x0A, 0x32, 0x48, 0x36, 0x4E, 0x4D, 0x4C, 0x56, 0x77, 0x4D, 0x65, 0x6E, 0x69, 0x61, 0x63, 0x66, 0x7A, 0x63, 0x4E, 0x73, 0x67, 0x46, + 0x59, 0x62, 0x51, 0x44, 0x66, 0x43, 0x2B, 0x72, 0x41, 0x46, 0x31, 0x68, 0x4D, 0x35, 0x2B, 0x6E, 0x30, 0x32, 0x2F, 0x74, 0x32, 0x41, 0x37, 0x6E, 0x50, 0x50, 0x4B, 0x48, 0x65, 0x4A, 0x65, 0x61, 0x4E, 0x69, 0x6A, 0x6E, 0x5A, 0x66, 0x6C, 0x51, + 0x47, 0x44, 0x53, 0x4E, 0x69, 0x48, 0x2B, 0x30, 0x4C, 0x53, 0x34, 0x46, 0x39, 0x70, 0x30, 0x0A, 0x6F, 0x33, 0x2F, 0x55, 0x33, 0x37, 0x43, 0x59, 0x41, 0x71, 0x78, 0x76, 0x61, 0x32, 0x73, 0x73, 0x4A, 0x53, 0x52, 0x79, 0x6F, 0x57, 0x58, 0x75, + 0x4A, 0x56, 0x72, 0x6C, 0x35, 0x6A, 0x4C, 0x6E, 0x38, 0x74, 0x2B, 0x72, 0x53, 0x66, 0x72, 0x7A, 0x6B, 0x47, 0x6B, 0x6A, 0x32, 0x77, 0x54, 0x5A, 0x35, 0x31, 0x78, 0x59, 0x2F, 0x47, 0x58, 0x55, 0x6C, 0x37, 0x37, 0x4D, 0x2F, 0x43, 0x34, 0x4B, + 0x7A, 0x43, 0x55, 0x71, 0x4E, 0x51, 0x54, 0x34, 0x59, 0x4A, 0x45, 0x56, 0x0A, 0x64, 0x54, 0x31, 0x42, 0x2F, 0x79, 0x4D, 0x66, 0x47, 0x63, 0x68, 0x73, 0x36, 0x34, 0x4A, 0x54, 0x42, 0x4B, 0x62, 0x6B, 0x54, 0x43, 0x4A, 0x4E, 0x6A, 0x59, 0x79, + 0x36, 0x7A, 0x6C, 0x74, 0x7A, 0x37, 0x47, 0x52, 0x55, 0x55, 0x47, 0x33, 0x52, 0x6E, 0x46, 0x58, 0x37, 0x61, 0x63, 0x4D, 0x32, 0x77, 0x34, 0x79, 0x38, 0x50, 0x49, 0x57, 0x6D, 0x61, 0x77, 0x6F, 0x6D, 0x44, 0x65, 0x43, 0x54, 0x6D, 0x47, 0x43, + 0x75, 0x66, 0x73, 0x59, 0x6B, 0x6C, 0x34, 0x70, 0x68, 0x0A, 0x58, 0x35, 0x47, 0x4F, 0x5A, 0x70, 0x49, 0x4A, 0x68, 0x7A, 0x62, 0x4E, 0x69, 0x35, 0x73, 0x74, 0x50, 0x76, 0x5A, 0x52, 0x31, 0x46, 0x44, 0x55, 0x57, 0x53, 0x69, 0x39, 0x67, 0x2F, + 0x4C, 0x4D, 0x4B, 0x48, 0x74, 0x54, 0x68, 0x6D, 0x33, 0x59, 0x4A, 0x6F, 0x68, 0x77, 0x31, 0x2B, 0x71, 0x52, 0x7A, 0x54, 0x36, 0x35, 0x79, 0x73, 0x43, 0x51, 0x62, 0x6C, 0x72, 0x47, 0x58, 0x6E, 0x52, 0x6C, 0x31, 0x31, 0x7A, 0x2B, 0x6F, 0x2B, + 0x49, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x2D, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x6C, 0x61, 0x73, 0x73, 0x20, 0x33, 0x20, 0x43, 0x41, 0x20, 0x32, 0x20, 0x45, 0x56, 0x20, 0x32, 0x30, 0x30, 0x39, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x51, 0x7A, 0x43, 0x43, 0x41, 0x79, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x44, 0x43, 0x59, 0x50, 0x30, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, + 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x46, 0x41, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x52, 0x46, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x4B, 0x0A, 0x44, 0x41, 0x78, 0x45, 0x4C, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x78, 0x4B, 0x6A, 0x41, 0x6F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x49, 0x55, 0x51, 0x74, 0x56, + 0x46, 0x4A, 0x56, 0x55, 0x31, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x62, 0x47, 0x46, 0x7A, 0x63, 0x79, 0x41, 0x7A, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x52, 0x56, 0x59, 0x67, 0x4D, 0x6A, 0x41, 0x77, 0x0A, + 0x4F, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x77, 0x4F, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x44, 0x55, 0x77, 0x4E, 0x44, 0x5A, 0x61, 0x46, 0x77, 0x30, 0x79, 0x4F, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x44, 0x55, 0x77, + 0x4E, 0x44, 0x5A, 0x61, 0x4D, 0x46, 0x41, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x52, 0x46, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x44, 0x41, 0x78, + 0x45, 0x4C, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x78, 0x4B, 0x6A, 0x41, 0x6F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x49, 0x55, 0x51, 0x74, 0x56, 0x46, 0x4A, 0x56, 0x55, 0x31, 0x51, + 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x62, 0x47, 0x46, 0x7A, 0x63, 0x79, 0x41, 0x7A, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x52, 0x56, 0x59, 0x67, 0x4D, 0x6A, 0x41, 0x77, 0x0A, 0x4F, 0x54, 0x43, 0x43, 0x41, 0x53, + 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6F, 0x43, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4A, + 0x6E, 0x78, 0x68, 0x44, 0x52, 0x77, 0x75, 0x69, 0x2B, 0x33, 0x4D, 0x4B, 0x43, 0x4F, 0x76, 0x58, 0x77, 0x45, 0x7A, 0x37, 0x35, 0x69, 0x76, 0x4A, 0x6E, 0x39, 0x67, 0x70, 0x66, 0x53, 0x0A, 0x65, 0x67, 0x70, 0x6E, 0x6C, 0x6A, 0x67, 0x4A, 0x39, + 0x68, 0x42, 0x4F, 0x6C, 0x53, 0x4A, 0x7A, 0x6D, 0x59, 0x33, 0x61, 0x46, 0x53, 0x33, 0x6E, 0x42, 0x66, 0x77, 0x5A, 0x63, 0x79, 0x4B, 0x33, 0x6A, 0x70, 0x67, 0x41, 0x76, 0x44, 0x77, 0x39, 0x72, 0x4B, 0x46, 0x73, 0x2B, 0x39, 0x5A, 0x35, 0x4A, + 0x55, 0x75, 0x74, 0x38, 0x4D, 0x78, 0x6B, 0x32, 0x6F, 0x67, 0x2B, 0x4B, 0x62, 0x67, 0x50, 0x43, 0x64, 0x4D, 0x30, 0x33, 0x54, 0x50, 0x31, 0x59, 0x74, 0x48, 0x68, 0x0A, 0x7A, 0x52, 0x6E, 0x70, 0x37, 0x68, 0x68, 0x50, 0x54, 0x46, 0x69, 0x75, + 0x34, 0x68, 0x37, 0x57, 0x44, 0x46, 0x73, 0x56, 0x57, 0x74, 0x67, 0x36, 0x75, 0x4D, 0x51, 0x59, 0x5A, 0x42, 0x37, 0x6A, 0x4D, 0x37, 0x4B, 0x31, 0x69, 0x58, 0x64, 0x4F, 0x44, 0x4C, 0x2F, 0x5A, 0x6C, 0x47, 0x73, 0x54, 0x6C, 0x32, 0x38, 0x53, + 0x6F, 0x2F, 0x36, 0x5A, 0x71, 0x51, 0x54, 0x4D, 0x46, 0x65, 0x78, 0x67, 0x61, 0x44, 0x62, 0x74, 0x43, 0x48, 0x75, 0x33, 0x39, 0x62, 0x2B, 0x54, 0x0A, 0x37, 0x57, 0x59, 0x78, 0x67, 0x34, 0x7A, 0x47, 0x63, 0x54, 0x53, 0x48, 0x54, 0x68, 0x66, + 0x71, 0x72, 0x34, 0x75, 0x52, 0x6A, 0x52, 0x78, 0x57, 0x51, 0x61, 0x34, 0x69, 0x4E, 0x31, 0x34, 0x33, 0x38, 0x68, 0x33, 0x5A, 0x30, 0x53, 0x30, 0x4E, 0x4C, 0x32, 0x6C, 0x52, 0x70, 0x37, 0x35, 0x6D, 0x70, 0x6F, 0x6F, 0x36, 0x4B, 0x72, 0x33, + 0x48, 0x47, 0x72, 0x48, 0x68, 0x46, 0x50, 0x43, 0x2B, 0x4F, 0x68, 0x32, 0x35, 0x7A, 0x31, 0x75, 0x78, 0x61, 0x76, 0x36, 0x30, 0x0A, 0x73, 0x55, 0x59, 0x67, 0x6F, 0x76, 0x73, 0x65, 0x4F, 0x33, 0x44, 0x76, 0x6B, 0x35, 0x68, 0x39, 0x6A, 0x48, + 0x4F, 0x57, 0x38, 0x73, 0x58, 0x76, 0x68, 0x58, 0x43, 0x74, 0x4B, 0x53, 0x62, 0x38, 0x48, 0x67, 0x51, 0x2B, 0x48, 0x4B, 0x44, 0x59, 0x44, 0x38, 0x74, 0x53, 0x67, 0x32, 0x4A, 0x38, 0x37, 0x6F, 0x74, 0x54, 0x6C, 0x5A, 0x43, 0x70, 0x56, 0x36, + 0x4C, 0x71, 0x59, 0x51, 0x58, 0x59, 0x2B, 0x55, 0x33, 0x45, 0x4A, 0x2F, 0x70, 0x75, 0x72, 0x65, 0x33, 0x35, 0x0A, 0x31, 0x31, 0x48, 0x33, 0x61, 0x36, 0x55, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4F, 0x43, 0x41, 0x53, 0x51, 0x77, 0x67, + 0x67, 0x45, 0x67, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, + 0x4E, 0x4F, 0x55, 0x69, 0x6B, 0x78, 0x69, 0x45, 0x79, 0x6F, 0x5A, 0x4C, 0x73, 0x79, 0x76, 0x0A, 0x63, 0x6F, 0x70, 0x39, 0x4E, 0x74, 0x65, 0x61, 0x48, 0x4E, 0x78, 0x6E, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, + 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x43, 0x42, 0x33, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x66, 0x42, 0x49, 0x48, 0x56, 0x4D, 0x49, 0x48, 0x53, 0x4D, 0x49, 0x47, 0x48, 0x6F, 0x49, 0x47, 0x45, 0x6F, 0x49, 0x47, 0x42, + 0x68, 0x6E, 0x39, 0x73, 0x5A, 0x47, 0x46, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x0A, 0x5A, 0x47, 0x6C, 0x79, 0x5A, 0x57, 0x4E, 0x30, 0x62, 0x33, 0x4A, 0x35, 0x4C, 0x6D, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x75, 0x62, 0x6D, 0x56, + 0x30, 0x4C, 0x30, 0x4E, 0x4F, 0x50, 0x55, 0x51, 0x74, 0x56, 0x46, 0x4A, 0x56, 0x55, 0x31, 0x51, 0x6C, 0x4D, 0x6A, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4A, 0x54, 0x49, 0x77, 0x51, 0x32, 0x78, 0x68, 0x63, 0x33, 0x4D, 0x6C, 0x4D, 0x6A, 0x41, + 0x7A, 0x4A, 0x54, 0x49, 0x77, 0x51, 0x30, 0x45, 0x6C, 0x0A, 0x4D, 0x6A, 0x41, 0x79, 0x4A, 0x54, 0x49, 0x77, 0x52, 0x56, 0x59, 0x6C, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x44, 0x41, 0x35, 0x4C, 0x45, 0x38, 0x39, 0x52, 0x43, 0x31, 0x55, 0x63, 0x6E, + 0x56, 0x7A, 0x64, 0x43, 0x55, 0x79, 0x4D, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x73, 0x51, 0x7A, 0x31, 0x45, 0x52, 0x54, 0x39, 0x6A, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x5A, 0x58, 0x4A, 0x6C, 0x64, 0x6D, + 0x39, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x0A, 0x62, 0x32, 0x35, 0x73, 0x61, 0x58, 0x4E, 0x30, 0x4D, 0x45, 0x61, 0x67, 0x52, 0x4B, 0x42, 0x43, 0x68, 0x6B, 0x42, 0x6F, 0x64, 0x48, 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x64, 0x33, 0x64, 0x33, 0x4C, + 0x6D, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x75, 0x62, 0x6D, 0x56, 0x30, 0x4C, 0x32, 0x4E, 0x79, 0x62, 0x43, 0x39, 0x6B, 0x4C, 0x58, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x58, 0x33, 0x4A, 0x76, 0x62, 0x33, 0x52, 0x66, 0x59, + 0x32, 0x78, 0x68, 0x0A, 0x63, 0x33, 0x4E, 0x66, 0x4D, 0x31, 0x39, 0x6A, 0x59, 0x56, 0x38, 0x79, 0x58, 0x32, 0x56, 0x32, 0x58, 0x7A, 0x49, 0x77, 0x4D, 0x44, 0x6B, 0x75, 0x59, 0x33, 0x4A, 0x73, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, + 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x41, 0x30, 0x37, 0x58, 0x74, 0x61, 0x50, 0x4B, 0x53, 0x55, 0x69, 0x4F, 0x38, 0x61, 0x45, 0x58, 0x55, 0x48, 0x4C, 0x37, 0x50, 0x2B, + 0x0A, 0x50, 0x50, 0x6F, 0x65, 0x55, 0x53, 0x62, 0x72, 0x68, 0x2F, 0x59, 0x70, 0x33, 0x75, 0x44, 0x78, 0x31, 0x4D, 0x59, 0x6B, 0x43, 0x65, 0x6E, 0x42, 0x7A, 0x31, 0x55, 0x62, 0x74, 0x44, 0x44, 0x5A, 0x7A, 0x68, 0x72, 0x2B, 0x42, 0x6C, 0x47, + 0x6D, 0x46, 0x61, 0x51, 0x74, 0x37, 0x37, 0x4A, 0x4C, 0x76, 0x79, 0x41, 0x6F, 0x4A, 0x55, 0x6E, 0x52, 0x70, 0x6A, 0x5A, 0x33, 0x4E, 0x4F, 0x68, 0x6B, 0x33, 0x31, 0x4B, 0x78, 0x45, 0x63, 0x64, 0x7A, 0x65, 0x73, 0x30, 0x35, 0x0A, 0x6E, 0x73, + 0x4B, 0x74, 0x6A, 0x48, 0x45, 0x68, 0x38, 0x6C, 0x70, 0x72, 0x72, 0x39, 0x38, 0x38, 0x54, 0x6C, 0x57, 0x76, 0x73, 0x6F, 0x52, 0x6C, 0x46, 0x49, 0x6D, 0x35, 0x64, 0x38, 0x73, 0x71, 0x4D, 0x62, 0x37, 0x50, 0x6F, 0x32, 0x33, 0x50, 0x62, 0x30, + 0x69, 0x55, 0x4D, 0x6B, 0x5A, 0x76, 0x35, 0x33, 0x47, 0x4D, 0x6F, 0x4B, 0x61, 0x45, 0x47, 0x54, 0x63, 0x48, 0x38, 0x67, 0x4E, 0x46, 0x43, 0x53, 0x75, 0x47, 0x64, 0x58, 0x7A, 0x66, 0x58, 0x32, 0x6C, 0x58, 0x0A, 0x41, 0x4E, 0x74, 0x75, 0x32, + 0x4B, 0x5A, 0x79, 0x49, 0x6B, 0x74, 0x51, 0x31, 0x48, 0x57, 0x59, 0x56, 0x74, 0x2B, 0x33, 0x47, 0x50, 0x39, 0x44, 0x51, 0x31, 0x43, 0x75, 0x65, 0x6B, 0x52, 0x37, 0x38, 0x48, 0x6C, 0x52, 0x31, 0x30, 0x4D, 0x39, 0x70, 0x39, 0x4F, 0x42, 0x30, + 0x2F, 0x44, 0x4A, 0x54, 0x37, 0x6E, 0x61, 0x78, 0x70, 0x65, 0x47, 0x30, 0x49, 0x4C, 0x44, 0x35, 0x45, 0x4A, 0x74, 0x2F, 0x72, 0x44, 0x69, 0x5A, 0x45, 0x34, 0x4F, 0x4A, 0x75, 0x64, 0x41, 0x0A, 0x4E, 0x43, 0x61, 0x31, 0x43, 0x49, 0x6E, 0x58, + 0x43, 0x47, 0x4E, 0x6A, 0x4F, 0x43, 0x64, 0x31, 0x48, 0x6A, 0x50, 0x71, 0x62, 0x71, 0x6A, 0x64, 0x6E, 0x35, 0x6C, 0x50, 0x64, 0x45, 0x32, 0x42, 0x69, 0x59, 0x42, 0x4C, 0x33, 0x5A, 0x71, 0x58, 0x4B, 0x56, 0x77, 0x76, 0x76, 0x6F, 0x46, 0x42, + 0x75, 0x59, 0x7A, 0x2F, 0x36, 0x6E, 0x31, 0x67, 0x42, 0x70, 0x37, 0x4E, 0x31, 0x7A, 0x33, 0x54, 0x4C, 0x71, 0x4D, 0x56, 0x76, 0x4B, 0x6A, 0x6D, 0x4A, 0x75, 0x56, 0x76, 0x0A, 0x77, 0x39, 0x79, 0x34, 0x41, 0x79, 0x48, 0x71, 0x6E, 0x78, 0x62, + 0x78, 0x4C, 0x46, 0x53, 0x31, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x41, 0x20, 0x44, 0x69, 0x73, 0x69, + 0x67, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x61, 0x54, 0x43, 0x43, 0x41, 0x31, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4A, 0x41, 0x4A, 0x4B, 0x34, 0x69, + 0x4E, 0x75, 0x77, 0x69, 0x73, 0x46, 0x6A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x46, 0x49, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x59, 0x54, 0x41, 0x6C, 0x4E, 0x4C, 0x4D, 0x52, 0x4D, 0x77, 0x0A, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x45, 0x77, 0x70, 0x43, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x58, 0x4E, 0x73, 0x59, 0x58, 0x5A, 0x68, 0x4D, 0x52, 0x4D, 0x77, + 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x70, 0x45, 0x61, 0x58, 0x4E, 0x70, 0x5A, 0x79, 0x42, 0x68, 0x4C, 0x6E, 0x4D, 0x75, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, 0x44, + 0x51, 0x53, 0x42, 0x45, 0x61, 0x58, 0x4E, 0x70, 0x0A, 0x5A, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x46, 0x49, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x79, 0x4D, 0x44, 0x63, 0x78, 0x4F, 0x54, 0x41, 0x35, 0x4D, 0x54, 0x55, + 0x7A, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x79, 0x4D, 0x44, 0x63, 0x78, 0x4F, 0x54, 0x41, 0x35, 0x4D, 0x54, 0x55, 0x7A, 0x4D, 0x46, 0x6F, 0x77, 0x55, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, + 0x43, 0x55, 0x30, 0x73, 0x78, 0x0A, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x6B, 0x4A, 0x79, 0x59, 0x58, 0x52, 0x70, 0x63, 0x32, 0x78, 0x68, 0x64, 0x6D, 0x45, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, 0x6B, 0x52, 0x70, 0x63, 0x32, 0x6C, 0x6E, 0x49, 0x47, 0x45, 0x75, 0x63, 0x79, 0x34, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x45, 0x4E, 0x42, 0x49, 0x45, + 0x52, 0x70, 0x0A, 0x63, 0x32, 0x6C, 0x6E, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x49, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, + 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x69, 0x6F, 0x38, 0x51, 0x41, 0x43, 0x64, 0x61, 0x46, 0x58, 0x53, 0x31, 0x74, 0x46, 0x50, 0x62, 0x43, 0x0A, + 0x77, 0x33, 0x4F, 0x65, 0x4E, 0x63, 0x4A, 0x78, 0x56, 0x58, 0x36, 0x42, 0x2B, 0x36, 0x74, 0x47, 0x55, 0x4F, 0x44, 0x42, 0x66, 0x45, 0x6C, 0x34, 0x35, 0x71, 0x74, 0x35, 0x57, 0x44, 0x7A, 0x61, 0x2F, 0x33, 0x77, 0x63, 0x6E, 0x39, 0x69, 0x58, + 0x41, 0x6E, 0x67, 0x2B, 0x61, 0x30, 0x45, 0x45, 0x36, 0x55, 0x47, 0x39, 0x76, 0x67, 0x4D, 0x73, 0x52, 0x66, 0x59, 0x76, 0x5A, 0x4E, 0x53, 0x72, 0x58, 0x61, 0x4E, 0x48, 0x50, 0x57, 0x53, 0x62, 0x36, 0x57, 0x69, 0x61, 0x0A, 0x78, 0x73, 0x77, + 0x62, 0x50, 0x37, 0x71, 0x2B, 0x73, 0x6F, 0x73, 0x30, 0x41, 0x69, 0x36, 0x59, 0x56, 0x52, 0x6E, 0x38, 0x6A, 0x47, 0x2B, 0x71, 0x58, 0x39, 0x70, 0x4D, 0x7A, 0x6B, 0x30, 0x44, 0x49, 0x61, 0x50, 0x59, 0x30, 0x6A, 0x53, 0x54, 0x56, 0x70, 0x62, + 0x4C, 0x54, 0x41, 0x77, 0x41, 0x46, 0x6A, 0x78, 0x66, 0x47, 0x73, 0x33, 0x49, 0x78, 0x32, 0x79, 0x6D, 0x72, 0x64, 0x4D, 0x78, 0x70, 0x37, 0x7A, 0x6F, 0x35, 0x65, 0x46, 0x6D, 0x31, 0x74, 0x4C, 0x37, 0x0A, 0x41, 0x37, 0x52, 0x42, 0x5A, 0x63, + 0x6B, 0x51, 0x72, 0x67, 0x34, 0x46, 0x59, 0x38, 0x61, 0x41, 0x61, 0x6D, 0x6B, 0x77, 0x2F, 0x64, 0x4C, 0x75, 0x6B, 0x4F, 0x38, 0x4E, 0x4A, 0x39, 0x2B, 0x66, 0x6C, 0x58, 0x50, 0x30, 0x34, 0x53, 0x58, 0x61, 0x62, 0x42, 0x62, 0x65, 0x51, 0x54, + 0x67, 0x30, 0x36, 0x6F, 0x76, 0x38, 0x30, 0x65, 0x67, 0x45, 0x46, 0x47, 0x45, 0x74, 0x51, 0x58, 0x36, 0x73, 0x78, 0x33, 0x64, 0x4F, 0x79, 0x31, 0x46, 0x55, 0x2B, 0x31, 0x36, 0x53, 0x0A, 0x47, 0x42, 0x73, 0x45, 0x57, 0x6D, 0x6A, 0x47, 0x79, + 0x63, 0x54, 0x36, 0x74, 0x78, 0x4F, 0x67, 0x6D, 0x4C, 0x63, 0x52, 0x4B, 0x37, 0x66, 0x57, 0x56, 0x38, 0x78, 0x38, 0x6E, 0x68, 0x66, 0x52, 0x79, 0x79, 0x58, 0x2B, 0x68, 0x6B, 0x34, 0x6B, 0x4C, 0x6C, 0x59, 0x4D, 0x65, 0x45, 0x32, 0x65, 0x41, + 0x52, 0x4B, 0x6D, 0x4B, 0x36, 0x63, 0x42, 0x5A, 0x57, 0x35, 0x38, 0x59, 0x68, 0x32, 0x45, 0x68, 0x4E, 0x2F, 0x71, 0x77, 0x47, 0x75, 0x31, 0x70, 0x53, 0x71, 0x56, 0x0A, 0x67, 0x38, 0x4E, 0x54, 0x45, 0x51, 0x78, 0x7A, 0x48, 0x51, 0x75, 0x79, + 0x52, 0x70, 0x44, 0x52, 0x51, 0x6A, 0x72, 0x4F, 0x51, 0x47, 0x36, 0x56, 0x72, 0x66, 0x2F, 0x47, 0x6C, 0x4B, 0x31, 0x75, 0x6C, 0x34, 0x53, 0x4F, 0x66, 0x57, 0x2B, 0x65, 0x69, 0x6F, 0x41, 0x4E, 0x53, 0x57, 0x31, 0x7A, 0x34, 0x6E, 0x75, 0x53, + 0x48, 0x73, 0x50, 0x7A, 0x77, 0x66, 0x50, 0x72, 0x4C, 0x67, 0x56, 0x76, 0x32, 0x52, 0x76, 0x50, 0x4E, 0x33, 0x59, 0x45, 0x79, 0x4C, 0x52, 0x61, 0x0A, 0x35, 0x42, 0x65, 0x6E, 0x79, 0x39, 0x31, 0x32, 0x48, 0x39, 0x41, 0x5A, 0x64, 0x75, 0x67, + 0x73, 0x42, 0x62, 0x50, 0x57, 0x6E, 0x44, 0x54, 0x59, 0x6C, 0x74, 0x78, 0x68, 0x68, 0x35, 0x45, 0x46, 0x35, 0x45, 0x51, 0x49, 0x4D, 0x38, 0x48, 0x61, 0x75, 0x51, 0x68, 0x6C, 0x31, 0x4B, 0x36, 0x79, 0x4E, 0x67, 0x33, 0x72, 0x75, 0x6A, 0x69, + 0x36, 0x44, 0x4F, 0x57, 0x62, 0x6E, 0x75, 0x75, 0x4E, 0x5A, 0x74, 0x32, 0x5A, 0x7A, 0x39, 0x61, 0x4A, 0x51, 0x66, 0x59, 0x45, 0x0A, 0x6B, 0x6F, 0x6F, 0x70, 0x4B, 0x57, 0x31, 0x72, 0x4F, 0x68, 0x7A, 0x6E, 0x64, 0x58, 0x30, 0x43, 0x63, 0x51, + 0x37, 0x7A, 0x77, 0x4F, 0x65, 0x39, 0x79, 0x78, 0x6E, 0x64, 0x6E, 0x57, 0x43, 0x79, 0x77, 0x6D, 0x5A, 0x67, 0x74, 0x72, 0x45, 0x45, 0x37, 0x73, 0x6E, 0x6D, 0x68, 0x72, 0x6D, 0x61, 0x5A, 0x6B, 0x43, 0x6F, 0x35, 0x78, 0x48, 0x74, 0x67, 0x55, + 0x55, 0x44, 0x69, 0x2F, 0x5A, 0x6E, 0x57, 0x65, 0x6A, 0x42, 0x42, 0x68, 0x47, 0x39, 0x33, 0x63, 0x2B, 0x41, 0x0A, 0x41, 0x6B, 0x39, 0x6C, 0x51, 0x48, 0x68, 0x63, 0x52, 0x31, 0x44, 0x49, 0x6D, 0x2B, 0x59, 0x66, 0x67, 0x58, 0x76, 0x6B, 0x52, + 0x4B, 0x68, 0x62, 0x68, 0x5A, 0x72, 0x69, 0x33, 0x6C, 0x72, 0x56, 0x78, 0x2F, 0x6B, 0x36, 0x52, 0x47, 0x5A, 0x4C, 0x35, 0x44, 0x4A, 0x55, 0x66, 0x4F, 0x52, 0x73, 0x6E, 0x4C, 0x4D, 0x4F, 0x50, 0x52, 0x65, 0x69, 0x73, 0x6A, 0x51, 0x53, 0x31, + 0x6E, 0x36, 0x79, 0x71, 0x45, 0x6D, 0x37, 0x30, 0x58, 0x6F, 0x6F, 0x51, 0x4C, 0x36, 0x69, 0x0A, 0x46, 0x68, 0x2F, 0x66, 0x35, 0x44, 0x63, 0x66, 0x45, 0x58, 0x50, 0x37, 0x6B, 0x41, 0x70, 0x6C, 0x51, 0x36, 0x49, 0x4E, 0x66, 0x50, 0x67, 0x47, + 0x41, 0x56, 0x55, 0x7A, 0x66, 0x62, 0x41, 0x4E, 0x75, 0x50, 0x54, 0x31, 0x72, 0x71, 0x56, 0x43, 0x56, 0x33, 0x77, 0x32, 0x45, 0x59, 0x78, 0x37, 0x58, 0x73, 0x51, 0x44, 0x6E, 0x59, 0x78, 0x35, 0x6E, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, + 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, + 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x74, 0x5A, 0x6E, 0x34, 0x72, 0x37, 0x43, 0x55, 0x39, 0x65, 0x4D, 0x67, 0x31, 0x67, 0x71, + 0x74, 0x7A, 0x6B, 0x35, 0x57, 0x70, 0x43, 0x35, 0x75, 0x0A, 0x51, 0x75, 0x30, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x43, + 0x59, 0x47, 0x58, 0x6E, 0x44, 0x6E, 0x5A, 0x54, 0x50, 0x49, 0x67, 0x6D, 0x37, 0x5A, 0x6E, 0x42, 0x63, 0x36, 0x47, 0x33, 0x70, 0x6D, 0x73, 0x67, 0x48, 0x32, 0x65, 0x44, 0x74, 0x70, 0x58, 0x69, 0x2F, 0x71, 0x2F, 0x30, 0x37, 0x35, 0x4B, 0x4D, + 0x4F, 0x59, 0x4B, 0x6D, 0x46, 0x4D, 0x0A, 0x74, 0x43, 0x51, 0x53, 0x69, 0x6E, 0x31, 0x74, 0x45, 0x52, 0x54, 0x33, 0x6E, 0x4C, 0x58, 0x4B, 0x35, 0x72, 0x79, 0x65, 0x4A, 0x34, 0x35, 0x4D, 0x47, 0x63, 0x69, 0x70, 0x76, 0x58, 0x72, 0x41, 0x31, + 0x7A, 0x59, 0x4F, 0x62, 0x59, 0x56, 0x79, 0x62, 0x71, 0x6A, 0x47, 0x6F, 0x6D, 0x33, 0x32, 0x2B, 0x6E, 0x4E, 0x6A, 0x66, 0x37, 0x78, 0x75, 0x65, 0x51, 0x67, 0x63, 0x6E, 0x59, 0x71, 0x66, 0x47, 0x6F, 0x70, 0x54, 0x70, 0x74, 0x69, 0x37, 0x32, + 0x54, 0x56, 0x56, 0x0A, 0x73, 0x52, 0x48, 0x46, 0x71, 0x51, 0x4F, 0x7A, 0x56, 0x6A, 0x75, 0x35, 0x68, 0x4A, 0x4D, 0x69, 0x58, 0x6E, 0x37, 0x42, 0x39, 0x68, 0x4A, 0x53, 0x69, 0x2B, 0x6F, 0x73, 0x5A, 0x37, 0x7A, 0x2B, 0x4E, 0x6B, 0x7A, 0x31, + 0x75, 0x4D, 0x2F, 0x52, 0x73, 0x30, 0x6D, 0x53, 0x4F, 0x39, 0x4D, 0x70, 0x44, 0x70, 0x6B, 0x62, 0x6C, 0x76, 0x64, 0x68, 0x75, 0x44, 0x76, 0x45, 0x4B, 0x37, 0x5A, 0x34, 0x62, 0x4C, 0x51, 0x6A, 0x62, 0x2F, 0x44, 0x39, 0x30, 0x37, 0x4A, 0x65, + 0x0A, 0x64, 0x52, 0x2B, 0x5A, 0x6C, 0x61, 0x69, 0x73, 0x39, 0x74, 0x72, 0x68, 0x78, 0x54, 0x46, 0x37, 0x2B, 0x39, 0x46, 0x47, 0x73, 0x39, 0x4B, 0x38, 0x5A, 0x37, 0x52, 0x69, 0x56, 0x4C, 0x6F, 0x4A, 0x39, 0x32, 0x4F, 0x77, 0x6B, 0x36, 0x4B, + 0x61, 0x2B, 0x65, 0x6C, 0x53, 0x4C, 0x6F, 0x74, 0x67, 0x45, 0x71, 0x76, 0x38, 0x39, 0x57, 0x42, 0x57, 0x37, 0x78, 0x42, 0x63, 0x69, 0x38, 0x51, 0x61, 0x51, 0x74, 0x79, 0x44, 0x57, 0x32, 0x51, 0x4F, 0x79, 0x37, 0x57, 0x38, 0x0A, 0x31, 0x6B, + 0x2F, 0x42, 0x66, 0x44, 0x78, 0x75, 0x6A, 0x52, 0x4E, 0x74, 0x2B, 0x33, 0x76, 0x72, 0x4D, 0x4E, 0x44, 0x63, 0x54, 0x61, 0x2F, 0x46, 0x31, 0x62, 0x61, 0x6C, 0x54, 0x46, 0x74, 0x78, 0x79, 0x65, 0x67, 0x78, 0x76, 0x75, 0x67, 0x34, 0x42, 0x6B, + 0x69, 0x68, 0x47, 0x75, 0x4C, 0x71, 0x30, 0x74, 0x34, 0x53, 0x4F, 0x56, 0x67, 0x61, 0x2F, 0x34, 0x41, 0x4F, 0x67, 0x6E, 0x58, 0x6D, 0x74, 0x38, 0x6B, 0x48, 0x62, 0x41, 0x37, 0x76, 0x2F, 0x7A, 0x6A, 0x78, 0x0A, 0x6D, 0x48, 0x48, 0x45, 0x74, + 0x33, 0x38, 0x4F, 0x46, 0x64, 0x41, 0x6C, 0x61, 0x62, 0x30, 0x69, 0x6E, 0x53, 0x76, 0x74, 0x42, 0x66, 0x5A, 0x47, 0x52, 0x36, 0x7A, 0x74, 0x77, 0x50, 0x44, 0x55, 0x4F, 0x2B, 0x4C, 0x73, 0x37, 0x70, 0x5A, 0x62, 0x6B, 0x42, 0x4E, 0x4F, 0x48, + 0x6C, 0x59, 0x36, 0x36, 0x37, 0x44, 0x76, 0x6C, 0x72, 0x75, 0x57, 0x49, 0x78, 0x47, 0x36, 0x38, 0x6B, 0x4F, 0x47, 0x64, 0x47, 0x53, 0x56, 0x79, 0x43, 0x68, 0x31, 0x33, 0x78, 0x30, 0x31, 0x0A, 0x75, 0x74, 0x49, 0x33, 0x67, 0x7A, 0x68, 0x54, + 0x4F, 0x44, 0x59, 0x37, 0x7A, 0x32, 0x7A, 0x70, 0x2B, 0x57, 0x73, 0x4F, 0x30, 0x50, 0x73, 0x45, 0x36, 0x45, 0x39, 0x33, 0x31, 0x32, 0x55, 0x42, 0x65, 0x49, 0x59, 0x4D, 0x65, 0x6A, 0x34, 0x68, 0x59, 0x76, 0x46, 0x2F, 0x59, 0x33, 0x45, 0x4D, + 0x79, 0x5A, 0x39, 0x45, 0x32, 0x36, 0x67, 0x6E, 0x6F, 0x6E, 0x57, 0x2B, 0x62, 0x6F, 0x45, 0x2B, 0x31, 0x38, 0x44, 0x72, 0x47, 0x35, 0x67, 0x50, 0x63, 0x46, 0x77, 0x30, 0x0A, 0x73, 0x6F, 0x72, 0x4D, 0x77, 0x49, 0x55, 0x59, 0x36, 0x32, 0x35, + 0x36, 0x73, 0x2F, 0x64, 0x61, 0x6F, 0x51, 0x65, 0x2F, 0x71, 0x55, 0x4B, 0x53, 0x38, 0x32, 0x41, 0x69, 0x6C, 0x2B, 0x51, 0x55, 0x6F, 0x51, 0x65, 0x62, 0x54, 0x6E, 0x62, 0x41, 0x6A, 0x6E, 0x33, 0x39, 0x70, 0x43, 0x58, 0x48, 0x52, 0x2B, 0x33, + 0x2F, 0x48, 0x33, 0x4F, 0x73, 0x7A, 0x4D, 0x4F, 0x6C, 0x36, 0x57, 0x38, 0x4B, 0x6A, 0x70, 0x74, 0x6C, 0x77, 0x6C, 0x43, 0x46, 0x74, 0x61, 0x4F, 0x67, 0x0A, 0x55, 0x78, 0x4C, 0x4D, 0x56, 0x59, 0x64, 0x68, 0x38, 0x34, 0x47, 0x75, 0x45, 0x45, + 0x5A, 0x68, 0x76, 0x55, 0x51, 0x68, 0x75, 0x4D, 0x49, 0x39, 0x64, 0x4D, 0x39, 0x2B, 0x4A, 0x44, 0x58, 0x36, 0x48, 0x41, 0x63, 0x4F, 0x6D, 0x7A, 0x30, 0x69, 0x79, 0x75, 0x38, 0x78, 0x4C, 0x34, 0x79, 0x73, 0x45, 0x72, 0x33, 0x76, 0x51, 0x43, + 0x6A, 0x38, 0x4B, 0x57, 0x65, 0x66, 0x73, 0x68, 0x4E, 0x50, 0x5A, 0x69, 0x54, 0x45, 0x55, 0x78, 0x6E, 0x70, 0x48, 0x69, 0x6B, 0x56, 0x0A, 0x37, 0x2B, 0x5A, 0x74, 0x73, 0x48, 0x38, 0x74, 0x5A, 0x2F, 0x33, 0x7A, 0x62, 0x42, 0x74, 0x31, 0x52, + 0x71, 0x50, 0x6C, 0x53, 0x68, 0x66, 0x70, 0x70, 0x4E, 0x63, 0x4C, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, + 0x43, 0x43, 0x56, 0x52, 0x41, 0x49, 0x5A, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x48, 0x30, 0x7A, 0x43, 0x43, 0x42, 0x62, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x58, 0x73, 0x4F, 0x33, 0x70, 0x6B, 0x4E, 0x2F, 0x70, 0x4F, 0x41, 0x77, 0x44, + 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x77, 0x51, 0x6A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x4A, 0x51, 0x55, 0x4E, 0x44, 0x56, + 0x6C, 0x4A, 0x42, 0x0A, 0x53, 0x56, 0x6F, 0x78, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x44, 0x41, 0x64, 0x51, 0x53, 0x30, 0x6C, 0x42, 0x51, 0x30, 0x4E, 0x57, 0x4D, 0x51, 0x30, 0x77, 0x43, 0x77, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x52, 0x42, 0x51, 0x30, 0x4E, 0x57, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x46, 0x55, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x54, 0x41, 0x31, + 0x0A, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x54, 0x4D, 0x33, 0x4D, 0x7A, 0x64, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x44, 0x45, 0x79, 0x4D, 0x7A, 0x45, 0x77, 0x4F, 0x54, 0x4D, 0x33, 0x4D, 0x7A, 0x64, 0x61, 0x4D, 0x45, 0x49, 0x78, 0x45, 0x6A, 0x41, + 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x43, 0x55, 0x46, 0x44, 0x51, 0x31, 0x5A, 0x53, 0x51, 0x55, 0x6C, 0x61, 0x4D, 0x54, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x77, 0x77, 0x48, 0x0A, 0x55, 0x45, + 0x74, 0x4A, 0x51, 0x55, 0x4E, 0x44, 0x56, 0x6A, 0x45, 0x4E, 0x4D, 0x41, 0x73, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x45, 0x51, 0x55, 0x4E, 0x44, 0x56, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, + 0x4D, 0x43, 0x52, 0x56, 0x4D, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x0A, 0x44, 0x77, 0x41, 0x77, 0x67, + 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x62, 0x71, 0x61, 0x75, 0x2F, 0x59, 0x55, 0x71, 0x58, 0x72, 0x79, 0x2B, 0x58, 0x5A, 0x70, 0x70, 0x30, 0x58, 0x39, 0x44, 0x5A, 0x6C, 0x76, 0x33, 0x50, 0x34, 0x75, 0x52, 0x6D, 0x37, + 0x78, 0x38, 0x66, 0x52, 0x7A, 0x50, 0x43, 0x52, 0x4B, 0x50, 0x66, 0x6D, 0x74, 0x34, 0x66, 0x74, 0x56, 0x54, 0x64, 0x46, 0x58, 0x78, 0x70, 0x4E, 0x52, 0x46, 0x76, 0x75, 0x38, 0x67, 0x4D, 0x0A, 0x6A, 0x6D, 0x6F, 0x59, 0x48, 0x74, 0x69, 0x50, + 0x32, 0x52, 0x61, 0x38, 0x45, 0x45, 0x67, 0x32, 0x58, 0x50, 0x42, 0x6A, 0x73, 0x35, 0x42, 0x61, 0x58, 0x43, 0x51, 0x33, 0x31, 0x36, 0x50, 0x57, 0x79, 0x77, 0x6C, 0x78, 0x75, 0x66, 0x45, 0x42, 0x63, 0x6F, 0x53, 0x77, 0x66, 0x64, 0x74, 0x4E, + 0x67, 0x4D, 0x33, 0x38, 0x30, 0x32, 0x2F, 0x4A, 0x2B, 0x4E, 0x71, 0x32, 0x44, 0x6F, 0x4C, 0x53, 0x52, 0x59, 0x57, 0x6F, 0x47, 0x32, 0x69, 0x6F, 0x50, 0x65, 0x6A, 0x30, 0x0A, 0x52, 0x47, 0x79, 0x39, 0x6F, 0x63, 0x4C, 0x4C, 0x41, 0x37, 0x36, + 0x4D, 0x50, 0x68, 0x4D, 0x41, 0x68, 0x4E, 0x39, 0x4B, 0x53, 0x4D, 0x44, 0x6A, 0x49, 0x67, 0x72, 0x6F, 0x36, 0x54, 0x65, 0x6E, 0x47, 0x45, 0x79, 0x78, 0x43, 0x51, 0x30, 0x6A, 0x56, 0x6E, 0x38, 0x45, 0x54, 0x64, 0x6B, 0x58, 0x68, 0x42, 0x69, + 0x6C, 0x79, 0x4E, 0x70, 0x41, 0x6C, 0x48, 0x50, 0x72, 0x7A, 0x67, 0x35, 0x58, 0x50, 0x41, 0x4F, 0x42, 0x4F, 0x70, 0x30, 0x4B, 0x6F, 0x56, 0x64, 0x44, 0x0A, 0x61, 0x61, 0x78, 0x58, 0x62, 0x58, 0x6D, 0x51, 0x65, 0x4F, 0x57, 0x31, 0x74, 0x44, + 0x76, 0x59, 0x76, 0x45, 0x79, 0x4E, 0x4B, 0x4B, 0x47, 0x6E, 0x6F, 0x36, 0x65, 0x36, 0x41, 0x6B, 0x34, 0x6C, 0x30, 0x53, 0x71, 0x75, 0x37, 0x61, 0x34, 0x44, 0x49, 0x72, 0x68, 0x72, 0x49, 0x41, 0x38, 0x77, 0x4B, 0x46, 0x53, 0x56, 0x66, 0x2B, + 0x44, 0x75, 0x7A, 0x67, 0x70, 0x6D, 0x6E, 0x64, 0x46, 0x41, 0x4C, 0x57, 0x34, 0x69, 0x72, 0x35, 0x30, 0x61, 0x77, 0x51, 0x55, 0x5A, 0x0A, 0x30, 0x6D, 0x2F, 0x41, 0x38, 0x70, 0x2F, 0x34, 0x65, 0x37, 0x4D, 0x43, 0x51, 0x76, 0x74, 0x51, 0x71, + 0x52, 0x30, 0x74, 0x6B, 0x77, 0x38, 0x6A, 0x71, 0x38, 0x62, 0x42, 0x44, 0x35, 0x4C, 0x2F, 0x30, 0x4B, 0x49, 0x56, 0x39, 0x56, 0x4D, 0x4A, 0x63, 0x52, 0x7A, 0x2F, 0x52, 0x52, 0x4F, 0x45, 0x35, 0x69, 0x5A, 0x65, 0x2B, 0x4F, 0x43, 0x49, 0x48, + 0x41, 0x72, 0x38, 0x46, 0x72, 0x61, 0x6F, 0x63, 0x77, 0x61, 0x34, 0x38, 0x47, 0x4F, 0x45, 0x41, 0x71, 0x44, 0x47, 0x0A, 0x57, 0x75, 0x7A, 0x6E, 0x64, 0x4E, 0x39, 0x77, 0x72, 0x71, 0x4F, 0x44, 0x4A, 0x65, 0x72, 0x57, 0x78, 0x35, 0x65, 0x48, + 0x6B, 0x36, 0x66, 0x47, 0x69, 0x6F, 0x6F, 0x7A, 0x6C, 0x32, 0x41, 0x33, 0x45, 0x44, 0x36, 0x58, 0x50, 0x6D, 0x34, 0x70, 0x46, 0x64, 0x61, 0x68, 0x44, 0x39, 0x47, 0x49, 0x4C, 0x42, 0x4B, 0x66, 0x62, 0x36, 0x71, 0x6B, 0x78, 0x6B, 0x4C, 0x72, + 0x51, 0x61, 0x4C, 0x6A, 0x6C, 0x55, 0x50, 0x54, 0x41, 0x59, 0x56, 0x74, 0x6A, 0x72, 0x73, 0x37, 0x0A, 0x38, 0x79, 0x4D, 0x32, 0x78, 0x2F, 0x34, 0x37, 0x34, 0x4B, 0x45, 0x6C, 0x42, 0x30, 0x69, 0x72, 0x79, 0x59, 0x6C, 0x30, 0x2F, 0x77, 0x69, + 0x50, 0x67, 0x4C, 0x2F, 0x41, 0x6C, 0x6D, 0x58, 0x7A, 0x37, 0x75, 0x78, 0x4C, 0x61, 0x4C, 0x32, 0x64, 0x69, 0x4D, 0x4D, 0x78, 0x73, 0x30, 0x44, 0x78, 0x36, 0x4D, 0x2F, 0x32, 0x4F, 0x4C, 0x75, 0x63, 0x35, 0x4E, 0x46, 0x2F, 0x31, 0x4F, 0x56, + 0x59, 0x6D, 0x33, 0x7A, 0x36, 0x31, 0x50, 0x4D, 0x4F, 0x6D, 0x33, 0x57, 0x52, 0x0A, 0x35, 0x4C, 0x70, 0x53, 0x4C, 0x68, 0x6C, 0x2B, 0x30, 0x66, 0x58, 0x4E, 0x57, 0x68, 0x6E, 0x38, 0x75, 0x67, 0x62, 0x32, 0x2B, 0x31, 0x4B, 0x6F, 0x53, 0x35, + 0x6B, 0x45, 0x33, 0x66, 0x6A, 0x35, 0x74, 0x49, 0x74, 0x51, 0x6F, 0x30, 0x35, 0x69, 0x69, 0x66, 0x43, 0x48, 0x4A, 0x50, 0x71, 0x44, 0x51, 0x73, 0x47, 0x48, 0x2B, 0x74, 0x55, 0x74, 0x4B, 0x53, 0x70, 0x61, 0x63, 0x58, 0x70, 0x6B, 0x61, 0x74, + 0x63, 0x6E, 0x59, 0x47, 0x4D, 0x4E, 0x32, 0x38, 0x35, 0x4A, 0x0A, 0x39, 0x59, 0x30, 0x66, 0x6B, 0x49, 0x6B, 0x79, 0x46, 0x2F, 0x68, 0x7A, 0x51, 0x37, 0x6A, 0x53, 0x57, 0x70, 0x4F, 0x47, 0x59, 0x64, 0x62, 0x68, 0x64, 0x51, 0x72, 0x71, 0x65, + 0x57, 0x5A, 0x32, 0x69, 0x45, 0x39, 0x78, 0x36, 0x77, 0x51, 0x6C, 0x31, 0x67, 0x70, 0x61, 0x65, 0x70, 0x50, 0x6C, 0x75, 0x55, 0x73, 0x58, 0x51, 0x41, 0x2B, 0x78, 0x74, 0x72, 0x6E, 0x31, 0x33, 0x6B, 0x2F, 0x63, 0x34, 0x4C, 0x4F, 0x73, 0x4F, + 0x78, 0x46, 0x77, 0x59, 0x49, 0x52, 0x4B, 0x0A, 0x51, 0x32, 0x36, 0x5A, 0x49, 0x4D, 0x41, 0x70, 0x63, 0x51, 0x72, 0x41, 0x5A, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x34, 0x49, 0x43, 0x79, 0x7A, 0x43, 0x43, 0x41, 0x73, 0x63, 0x77, + 0x66, 0x51, 0x59, 0x49, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x51, 0x55, 0x48, 0x41, 0x51, 0x45, 0x45, 0x63, 0x54, 0x42, 0x76, 0x4D, 0x45, 0x77, 0x47, 0x43, 0x43, 0x73, 0x47, 0x41, 0x51, 0x55, 0x46, 0x42, 0x7A, 0x41, 0x43, 0x68, 0x6B, 0x42, 0x6F, + 0x64, 0x48, 0x52, 0x77, 0x0A, 0x4F, 0x69, 0x38, 0x76, 0x64, 0x33, 0x64, 0x33, 0x4C, 0x6D, 0x46, 0x6A, 0x59, 0x33, 0x59, 0x75, 0x5A, 0x58, 0x4D, 0x76, 0x5A, 0x6D, 0x6C, 0x73, 0x5A, 0x57, 0x46, 0x6B, 0x62, 0x57, 0x6C, 0x75, 0x4C, 0x30, 0x46, + 0x79, 0x59, 0x32, 0x68, 0x70, 0x64, 0x6D, 0x39, 0x7A, 0x4C, 0x32, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x57, 0x52, 0x76, 0x63, 0x79, 0x39, 0x79, 0x59, 0x57, 0x6C, 0x36, 0x59, 0x57, 0x4E, 0x6A, 0x64, 0x6A, 0x45, + 0x75, 0x0A, 0x59, 0x33, 0x4A, 0x30, 0x4D, 0x42, 0x38, 0x47, 0x43, 0x43, 0x73, 0x47, 0x41, 0x51, 0x55, 0x46, 0x42, 0x7A, 0x41, 0x42, 0x68, 0x68, 0x4E, 0x6F, 0x64, 0x48, 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x62, 0x32, 0x4E, 0x7A, 0x63, 0x43, + 0x35, 0x68, 0x59, 0x32, 0x4E, 0x32, 0x4C, 0x6D, 0x56, 0x7A, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x53, 0x68, 0x37, 0x54, 0x6A, 0x33, 0x7A, 0x63, 0x6E, 0x6B, 0x31, 0x58, 0x32, 0x0A, 0x56, + 0x75, 0x71, 0x42, 0x35, 0x54, 0x62, 0x4D, 0x6A, 0x42, 0x34, 0x2F, 0x76, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x38, 0x47, 0x41, + 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x4E, 0x4B, 0x48, 0x74, 0x4F, 0x50, 0x66, 0x4E, 0x79, 0x65, 0x54, 0x56, 0x66, 0x5A, 0x57, 0x36, 0x6F, 0x48, 0x6C, 0x4E, 0x73, 0x79, 0x4D, 0x0A, 0x48, 0x6A, 0x2B, 0x39, + 0x4D, 0x49, 0x49, 0x42, 0x63, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x67, 0x42, 0x49, 0x49, 0x42, 0x61, 0x6A, 0x43, 0x43, 0x41, 0x57, 0x59, 0x77, 0x67, 0x67, 0x46, 0x69, 0x42, 0x67, 0x52, 0x56, 0x48, 0x53, 0x41, 0x41, 0x4D, 0x49, 0x49, 0x42, + 0x57, 0x44, 0x43, 0x43, 0x41, 0x53, 0x49, 0x47, 0x43, 0x43, 0x73, 0x47, 0x41, 0x51, 0x55, 0x46, 0x42, 0x77, 0x49, 0x43, 0x4D, 0x49, 0x49, 0x42, 0x46, 0x42, 0x36, 0x43, 0x41, 0x52, 0x41, 0x41, 0x0A, 0x51, 0x51, 0x42, 0x31, 0x41, 0x48, 0x51, + 0x41, 0x62, 0x77, 0x42, 0x79, 0x41, 0x47, 0x6B, 0x41, 0x5A, 0x41, 0x42, 0x68, 0x41, 0x47, 0x51, 0x41, 0x49, 0x41, 0x42, 0x6B, 0x41, 0x47, 0x55, 0x41, 0x49, 0x41, 0x42, 0x44, 0x41, 0x47, 0x55, 0x41, 0x63, 0x67, 0x42, 0x30, 0x41, 0x47, 0x6B, + 0x41, 0x5A, 0x67, 0x42, 0x70, 0x41, 0x47, 0x4D, 0x41, 0x59, 0x51, 0x42, 0x6A, 0x41, 0x47, 0x6B, 0x41, 0x38, 0x77, 0x42, 0x75, 0x41, 0x43, 0x41, 0x41, 0x55, 0x67, 0x42, 0x68, 0x0A, 0x41, 0x4F, 0x30, 0x41, 0x65, 0x67, 0x41, 0x67, 0x41, 0x47, + 0x51, 0x41, 0x5A, 0x51, 0x41, 0x67, 0x41, 0x47, 0x77, 0x41, 0x59, 0x51, 0x41, 0x67, 0x41, 0x45, 0x45, 0x41, 0x51, 0x77, 0x42, 0x44, 0x41, 0x46, 0x59, 0x41, 0x49, 0x41, 0x41, 0x6F, 0x41, 0x45, 0x45, 0x41, 0x5A, 0x77, 0x42, 0x6C, 0x41, 0x47, + 0x34, 0x41, 0x59, 0x77, 0x42, 0x70, 0x41, 0x47, 0x45, 0x41, 0x49, 0x41, 0x42, 0x6B, 0x41, 0x47, 0x55, 0x41, 0x49, 0x41, 0x42, 0x55, 0x41, 0x47, 0x55, 0x41, 0x0A, 0x59, 0x77, 0x42, 0x75, 0x41, 0x47, 0x38, 0x41, 0x62, 0x41, 0x42, 0x76, 0x41, + 0x47, 0x63, 0x41, 0x37, 0x51, 0x42, 0x68, 0x41, 0x43, 0x41, 0x41, 0x65, 0x51, 0x41, 0x67, 0x41, 0x45, 0x4D, 0x41, 0x5A, 0x51, 0x42, 0x79, 0x41, 0x48, 0x51, 0x41, 0x61, 0x51, 0x42, 0x6D, 0x41, 0x47, 0x6B, 0x41, 0x59, 0x77, 0x42, 0x68, 0x41, + 0x47, 0x4D, 0x41, 0x61, 0x51, 0x44, 0x7A, 0x41, 0x47, 0x34, 0x41, 0x49, 0x41, 0x42, 0x46, 0x41, 0x47, 0x77, 0x41, 0x5A, 0x51, 0x42, 0x6A, 0x0A, 0x41, 0x48, 0x51, 0x41, 0x63, 0x67, 0x44, 0x7A, 0x41, 0x47, 0x34, 0x41, 0x61, 0x51, 0x42, 0x6A, + 0x41, 0x47, 0x45, 0x41, 0x4C, 0x41, 0x41, 0x67, 0x41, 0x45, 0x4D, 0x41, 0x53, 0x51, 0x42, 0x47, 0x41, 0x43, 0x41, 0x41, 0x55, 0x51, 0x41, 0x30, 0x41, 0x44, 0x59, 0x41, 0x4D, 0x41, 0x41, 0x78, 0x41, 0x44, 0x45, 0x41, 0x4E, 0x51, 0x41, 0x32, + 0x41, 0x45, 0x55, 0x41, 0x4B, 0x51, 0x41, 0x75, 0x41, 0x43, 0x41, 0x41, 0x51, 0x77, 0x42, 0x51, 0x41, 0x46, 0x4D, 0x41, 0x0A, 0x49, 0x41, 0x42, 0x6C, 0x41, 0x47, 0x34, 0x41, 0x49, 0x41, 0x42, 0x6F, 0x41, 0x48, 0x51, 0x41, 0x64, 0x41, 0x42, + 0x77, 0x41, 0x44, 0x6F, 0x41, 0x4C, 0x77, 0x41, 0x76, 0x41, 0x48, 0x63, 0x41, 0x64, 0x77, 0x42, 0x33, 0x41, 0x43, 0x34, 0x41, 0x59, 0x51, 0x42, 0x6A, 0x41, 0x47, 0x4D, 0x41, 0x64, 0x67, 0x41, 0x75, 0x41, 0x47, 0x55, 0x41, 0x63, 0x7A, 0x41, + 0x77, 0x42, 0x67, 0x67, 0x72, 0x42, 0x67, 0x45, 0x46, 0x42, 0x51, 0x63, 0x43, 0x41, 0x52, 0x59, 0x6B, 0x0A, 0x61, 0x48, 0x52, 0x30, 0x63, 0x44, 0x6F, 0x76, 0x4C, 0x33, 0x64, 0x33, 0x64, 0x79, 0x35, 0x68, 0x59, 0x32, 0x4E, 0x32, 0x4C, 0x6D, + 0x56, 0x7A, 0x4C, 0x32, 0x78, 0x6C, 0x5A, 0x32, 0x6C, 0x7A, 0x62, 0x47, 0x46, 0x6A, 0x61, 0x57, 0x39, 0x75, 0x58, 0x32, 0x4D, 0x75, 0x61, 0x48, 0x52, 0x74, 0x4D, 0x46, 0x55, 0x47, 0x41, 0x31, 0x55, 0x64, 0x48, 0x77, 0x52, 0x4F, 0x4D, 0x45, + 0x77, 0x77, 0x53, 0x71, 0x42, 0x49, 0x6F, 0x45, 0x61, 0x47, 0x52, 0x47, 0x68, 0x30, 0x0A, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x33, 0x64, 0x33, 0x63, 0x75, 0x59, 0x57, 0x4E, 0x6A, 0x64, 0x69, 0x35, 0x6C, 0x63, 0x79, 0x39, 0x6D, 0x61, + 0x57, 0x78, 0x6C, 0x59, 0x57, 0x52, 0x74, 0x61, 0x57, 0x34, 0x76, 0x51, 0x58, 0x4A, 0x6A, 0x61, 0x47, 0x6C, 0x32, 0x62, 0x33, 0x4D, 0x76, 0x59, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x5A, 0x47, 0x39, 0x7A, 0x4C, + 0x33, 0x4A, 0x68, 0x61, 0x58, 0x70, 0x68, 0x59, 0x32, 0x4E, 0x32, 0x0A, 0x4D, 0x56, 0x39, 0x6B, 0x5A, 0x58, 0x49, 0x75, 0x59, 0x33, 0x4A, 0x73, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, + 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x45, 0x45, 0x45, 0x44, 0x41, 0x4F, 0x67, 0x51, 0x78, 0x68, 0x59, 0x32, 0x4E, 0x32, 0x51, 0x47, 0x46, 0x6A, 0x59, 0x33, 0x59, 0x75, 0x5A, 0x58, 0x4D, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4A, 0x63, 0x78, 0x41, 0x70, 0x2F, 0x6E, 0x2F, 0x55, 0x4E, 0x6E, 0x53, 0x45, 0x51, + 0x55, 0x35, 0x43, 0x6D, 0x48, 0x37, 0x55, 0x77, 0x6F, 0x5A, 0x74, 0x43, 0x50, 0x4E, 0x64, 0x70, 0x4E, 0x59, 0x62, 0x64, 0x4B, 0x6C, 0x30, 0x32, 0x31, 0x32, 0x35, 0x44, 0x67, 0x42, 0x53, 0x34, 0x4F, 0x78, 0x6E, 0x6E, 0x51, 0x38, 0x70, 0x64, + 0x70, 0x44, 0x37, 0x30, 0x45, 0x0A, 0x52, 0x39, 0x6D, 0x2B, 0x32, 0x37, 0x55, 0x70, 0x32, 0x70, 0x76, 0x5A, 0x72, 0x71, 0x6D, 0x5A, 0x31, 0x64, 0x4D, 0x38, 0x4D, 0x4A, 0x50, 0x31, 0x6A, 0x61, 0x47, 0x6F, 0x2F, 0x41, 0x61, 0x4E, 0x52, 0x50, + 0x54, 0x4B, 0x46, 0x70, 0x56, 0x38, 0x4D, 0x39, 0x78, 0x69, 0x69, 0x36, 0x67, 0x33, 0x2B, 0x43, 0x66, 0x59, 0x43, 0x53, 0x30, 0x62, 0x37, 0x38, 0x67, 0x55, 0x4A, 0x79, 0x43, 0x70, 0x5A, 0x45, 0x54, 0x2F, 0x4C, 0x74, 0x5A, 0x31, 0x71, 0x6D, + 0x78, 0x4E, 0x0A, 0x59, 0x45, 0x41, 0x5A, 0x53, 0x55, 0x4E, 0x55, 0x59, 0x39, 0x72, 0x69, 0x7A, 0x4C, 0x70, 0x6D, 0x35, 0x55, 0x39, 0x45, 0x65, 0x6C, 0x76, 0x5A, 0x61, 0x6F, 0x45, 0x72, 0x51, 0x4E, 0x56, 0x2F, 0x2B, 0x51, 0x45, 0x6E, 0x57, + 0x43, 0x7A, 0x49, 0x37, 0x55, 0x69, 0x52, 0x66, 0x44, 0x2B, 0x6D, 0x41, 0x4D, 0x2F, 0x45, 0x4B, 0x58, 0x4D, 0x52, 0x4E, 0x74, 0x36, 0x47, 0x47, 0x54, 0x36, 0x64, 0x37, 0x68, 0x6D, 0x4B, 0x47, 0x39, 0x57, 0x77, 0x37, 0x59, 0x34, 0x39, 0x0A, + 0x6E, 0x43, 0x72, 0x41, 0x44, 0x64, 0x67, 0x39, 0x5A, 0x75, 0x4D, 0x38, 0x44, 0x62, 0x33, 0x56, 0x6C, 0x46, 0x7A, 0x69, 0x34, 0x71, 0x63, 0x31, 0x47, 0x77, 0x51, 0x41, 0x39, 0x6A, 0x39, 0x61, 0x6A, 0x65, 0x70, 0x44, 0x76, 0x56, 0x2B, 0x4A, + 0x48, 0x61, 0x6E, 0x42, 0x73, 0x4D, 0x79, 0x5A, 0x34, 0x6B, 0x30, 0x41, 0x43, 0x74, 0x72, 0x4A, 0x4A, 0x31, 0x76, 0x6E, 0x45, 0x35, 0x42, 0x63, 0x35, 0x50, 0x55, 0x7A, 0x6F, 0x6C, 0x56, 0x74, 0x33, 0x4F, 0x41, 0x4A, 0x0A, 0x54, 0x53, 0x2B, + 0x78, 0x4A, 0x6C, 0x73, 0x6E, 0x64, 0x51, 0x41, 0x4A, 0x78, 0x47, 0x4A, 0x33, 0x4B, 0x51, 0x68, 0x66, 0x6E, 0x6C, 0x6D, 0x73, 0x74, 0x6E, 0x36, 0x74, 0x6E, 0x31, 0x51, 0x77, 0x49, 0x67, 0x50, 0x42, 0x48, 0x6E, 0x46, 0x6B, 0x2F, 0x76, 0x6B, + 0x34, 0x43, 0x70, 0x59, 0x59, 0x33, 0x51, 0x49, 0x55, 0x72, 0x43, 0x50, 0x4C, 0x42, 0x68, 0x77, 0x65, 0x70, 0x48, 0x32, 0x4E, 0x44, 0x64, 0x34, 0x6E, 0x51, 0x65, 0x69, 0x74, 0x32, 0x68, 0x57, 0x33, 0x0A, 0x73, 0x43, 0x50, 0x64, 0x4B, 0x36, + 0x6A, 0x54, 0x32, 0x69, 0x57, 0x48, 0x37, 0x65, 0x68, 0x56, 0x52, 0x45, 0x32, 0x49, 0x39, 0x44, 0x5A, 0x2B, 0x68, 0x4A, 0x70, 0x34, 0x72, 0x50, 0x63, 0x4F, 0x56, 0x6B, 0x6B, 0x4F, 0x31, 0x6A, 0x4D, 0x6C, 0x31, 0x6F, 0x52, 0x51, 0x51, 0x6D, + 0x77, 0x67, 0x45, 0x68, 0x30, 0x71, 0x31, 0x62, 0x36, 0x38, 0x38, 0x6E, 0x43, 0x42, 0x70, 0x48, 0x42, 0x67, 0x76, 0x67, 0x57, 0x31, 0x6D, 0x35, 0x34, 0x45, 0x52, 0x4C, 0x35, 0x68, 0x0A, 0x49, 0x36, 0x7A, 0x70, 0x70, 0x53, 0x53, 0x4D, 0x45, + 0x59, 0x43, 0x55, 0x57, 0x71, 0x4B, 0x69, 0x75, 0x55, 0x6E, 0x53, 0x77, 0x64, 0x7A, 0x52, 0x70, 0x2B, 0x30, 0x78, 0x45, 0x53, 0x79, 0x65, 0x47, 0x61, 0x62, 0x75, 0x34, 0x56, 0x58, 0x68, 0x77, 0x4F, 0x72, 0x50, 0x44, 0x59, 0x54, 0x6B, 0x46, + 0x37, 0x65, 0x69, 0x66, 0x4B, 0x58, 0x65, 0x56, 0x53, 0x55, 0x47, 0x37, 0x73, 0x7A, 0x41, 0x68, 0x31, 0x78, 0x41, 0x32, 0x73, 0x79, 0x56, 0x50, 0x31, 0x58, 0x67, 0x0A, 0x4E, 0x63, 0x65, 0x34, 0x68, 0x4C, 0x36, 0x30, 0x58, 0x63, 0x31, 0x36, + 0x67, 0x77, 0x46, 0x79, 0x37, 0x6F, 0x66, 0x6D, 0x58, 0x78, 0x32, 0x75, 0x74, 0x59, 0x58, 0x47, 0x4A, 0x74, 0x2F, 0x6D, 0x77, 0x5A, 0x72, 0x70, 0x48, 0x67, 0x4A, 0x48, 0x6E, 0x79, 0x71, 0x6F, 0x62, 0x61, 0x6C, 0x62, 0x7A, 0x2B, 0x78, 0x46, + 0x64, 0x33, 0x2B, 0x59, 0x4A, 0x35, 0x6F, 0x79, 0x58, 0x53, 0x72, 0x6A, 0x68, 0x4F, 0x37, 0x46, 0x6D, 0x47, 0x59, 0x76, 0x6C, 0x69, 0x41, 0x64, 0x0A, 0x33, 0x64, 0x6A, 0x44, 0x4A, 0x39, 0x65, 0x77, 0x2B, 0x66, 0x37, 0x5A, 0x66, 0x63, 0x33, + 0x51, 0x6E, 0x34, 0x38, 0x4C, 0x46, 0x46, 0x68, 0x52, 0x6E, 0x79, 0x2B, 0x4C, 0x77, 0x7A, 0x67, 0x74, 0x33, 0x75, 0x69, 0x50, 0x31, 0x6F, 0x32, 0x48, 0x70, 0x50, 0x56, 0x57, 0x51, 0x78, 0x61, 0x5A, 0x4C, 0x50, 0x53, 0x6B, 0x56, 0x72, 0x51, + 0x30, 0x75, 0x47, 0x45, 0x33, 0x79, 0x63, 0x4A, 0x59, 0x67, 0x42, 0x75, 0x67, 0x6C, 0x36, 0x48, 0x38, 0x57, 0x59, 0x33, 0x70, 0x0A, 0x45, 0x66, 0x62, 0x52, 0x44, 0x30, 0x74, 0x56, 0x4E, 0x45, 0x59, 0x71, 0x69, 0x34, 0x59, 0x37, 0x0A, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x57, 0x43, 0x41, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x51, 0x54, 0x43, 0x43, 0x41, 0x79, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x43, 0x44, 0x4C, 0x34, 0x77, 0x44, 0x51, + 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x55, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x46, 0x63, 0x78, 0x45, 0x6A, + 0x41, 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x0A, 0x43, 0x56, 0x52, 0x42, 0x53, 0x56, 0x64, 0x42, 0x54, 0x69, 0x31, 0x44, 0x51, 0x54, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x48, 0x55, + 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x54, 0x56, 0x46, 0x64, 0x44, 0x51, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x55, + 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x0A, 0x51, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x6A, 0x41, 0x32, 0x4D, 0x6A, 0x63, 0x77, 0x4E, 0x6A, 0x49, 0x34, 0x4D, 0x7A, 0x4E, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x44, 0x45, 0x79, + 0x4D, 0x7A, 0x45, 0x78, 0x4E, 0x54, 0x55, 0x35, 0x4E, 0x54, 0x6C, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x52, 0x58, 0x4D, 0x52, 0x49, 0x77, 0x45, 0x41, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x45, 0x77, 0x6C, 0x55, 0x51, 0x55, 0x6C, 0x58, 0x51, 0x55, 0x34, 0x74, 0x51, 0x30, 0x45, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x42, 0x31, 0x4A, 0x76, 0x62, 0x33, 0x51, + 0x67, 0x51, 0x30, 0x45, 0x78, 0x48, 0x44, 0x41, 0x61, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x31, 0x52, 0x58, 0x51, 0x30, 0x45, 0x67, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, + 0x67, 0x0A, 0x51, 0x30, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, + 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x77, 0x42, 0x64, 0x76, 0x49, 0x36, 0x34, 0x7A, 0x45, 0x62, 0x6F, 0x6F, 0x68, 0x37, 0x34, 0x35, 0x4E, 0x6E, 0x48, 0x45, 0x4B, 0x48, 0x31, 0x4A, 0x77, 0x37, 0x57, 0x32, 0x43, 0x0A, 0x6E, + 0x4A, 0x66, 0x46, 0x31, 0x30, 0x78, 0x4F, 0x52, 0x55, 0x6E, 0x4C, 0x51, 0x45, 0x4B, 0x31, 0x45, 0x6A, 0x52, 0x73, 0x47, 0x63, 0x4A, 0x30, 0x70, 0x44, 0x46, 0x66, 0x68, 0x51, 0x4B, 0x58, 0x37, 0x45, 0x4D, 0x7A, 0x43, 0x6C, 0x50, 0x53, 0x6E, + 0x49, 0x79, 0x4F, 0x74, 0x37, 0x68, 0x35, 0x32, 0x79, 0x76, 0x56, 0x61, 0x76, 0x4B, 0x4F, 0x5A, 0x73, 0x54, 0x75, 0x4B, 0x77, 0x45, 0x48, 0x6B, 0x74, 0x53, 0x7A, 0x30, 0x41, 0x4C, 0x66, 0x55, 0x50, 0x5A, 0x56, 0x0A, 0x72, 0x32, 0x59, 0x4F, + 0x79, 0x2B, 0x42, 0x48, 0x59, 0x43, 0x38, 0x72, 0x4D, 0x6A, 0x6B, 0x31, 0x55, 0x6A, 0x6F, 0x6F, 0x67, 0x2F, 0x68, 0x37, 0x46, 0x73, 0x59, 0x59, 0x75, 0x47, 0x4C, 0x57, 0x52, 0x79, 0x57, 0x52, 0x7A, 0x76, 0x41, 0x5A, 0x45, 0x6B, 0x32, 0x74, + 0x59, 0x2F, 0x58, 0x54, 0x50, 0x33, 0x56, 0x66, 0x4B, 0x66, 0x43, 0x68, 0x4D, 0x42, 0x77, 0x71, 0x6F, 0x4A, 0x69, 0x6D, 0x46, 0x62, 0x33, 0x75, 0x2F, 0x52, 0x6B, 0x32, 0x38, 0x4F, 0x4B, 0x52, 0x0A, 0x51, 0x34, 0x2F, 0x36, 0x79, 0x74, 0x59, + 0x51, 0x4A, 0x30, 0x6C, 0x4D, 0x37, 0x39, 0x33, 0x42, 0x38, 0x59, 0x56, 0x77, 0x6D, 0x38, 0x72, 0x71, 0x71, 0x46, 0x70, 0x44, 0x2F, 0x47, 0x32, 0x47, 0x62, 0x33, 0x50, 0x70, 0x4E, 0x30, 0x57, 0x70, 0x38, 0x44, 0x62, 0x48, 0x7A, 0x49, 0x68, + 0x31, 0x48, 0x72, 0x74, 0x73, 0x42, 0x76, 0x2B, 0x62, 0x61, 0x7A, 0x34, 0x58, 0x37, 0x47, 0x47, 0x71, 0x63, 0x58, 0x7A, 0x47, 0x48, 0x61, 0x4C, 0x33, 0x53, 0x65, 0x6B, 0x56, 0x0A, 0x74, 0x54, 0x7A, 0x57, 0x6F, 0x57, 0x48, 0x31, 0x45, 0x66, + 0x63, 0x46, 0x62, 0x78, 0x33, 0x39, 0x45, 0x62, 0x37, 0x51, 0x4D, 0x41, 0x66, 0x43, 0x4B, 0x62, 0x41, 0x4A, 0x54, 0x69, 0x62, 0x63, 0x34, 0x36, 0x4B, 0x6F, 0x6B, 0x57, 0x6F, 0x66, 0x77, 0x70, 0x46, 0x46, 0x69, 0x46, 0x7A, 0x6C, 0x6D, 0x4C, + 0x68, 0x78, 0x70, 0x52, 0x55, 0x5A, 0x79, 0x58, 0x78, 0x31, 0x45, 0x63, 0x78, 0x77, 0x64, 0x45, 0x38, 0x74, 0x6D, 0x78, 0x32, 0x52, 0x52, 0x50, 0x31, 0x57, 0x0A, 0x4B, 0x4B, 0x44, 0x2B, 0x75, 0x34, 0x5A, 0x71, 0x79, 0x50, 0x70, 0x63, 0x43, + 0x31, 0x6A, 0x63, 0x78, 0x6B, 0x74, 0x32, 0x79, 0x4B, 0x73, 0x69, 0x32, 0x58, 0x4D, 0x50, 0x70, 0x66, 0x52, 0x61, 0x41, 0x6F, 0x6B, 0x2F, 0x54, 0x35, 0x34, 0x69, 0x67, 0x75, 0x36, 0x69, 0x64, 0x46, 0x4D, 0x71, 0x50, 0x56, 0x4D, 0x6E, 0x61, + 0x52, 0x31, 0x73, 0x6A, 0x6A, 0x49, 0x73, 0x5A, 0x41, 0x41, 0x6D, 0x59, 0x32, 0x45, 0x32, 0x54, 0x71, 0x4E, 0x47, 0x74, 0x7A, 0x39, 0x39, 0x0A, 0x73, 0x79, 0x32, 0x73, 0x62, 0x5A, 0x43, 0x69, 0x6C, 0x61, 0x4C, 0x4F, 0x7A, 0x39, 0x71, 0x43, + 0x35, 0x77, 0x63, 0x30, 0x47, 0x5A, 0x62, 0x70, 0x75, 0x43, 0x47, 0x71, 0x4B, 0x58, 0x36, 0x6D, 0x4F, 0x4C, 0x36, 0x4F, 0x4B, 0x55, 0x6F, 0x68, 0x5A, 0x6E, 0x6B, 0x66, 0x73, 0x38, 0x4F, 0x31, 0x43, 0x57, 0x66, 0x65, 0x31, 0x74, 0x51, 0x48, + 0x52, 0x76, 0x4D, 0x71, 0x32, 0x75, 0x59, 0x69, 0x4E, 0x32, 0x44, 0x4C, 0x67, 0x62, 0x59, 0x50, 0x6F, 0x41, 0x2F, 0x70, 0x0A, 0x79, 0x4A, 0x56, 0x2F, 0x76, 0x31, 0x57, 0x52, 0x42, 0x58, 0x72, 0x50, 0x50, 0x52, 0x58, 0x41, 0x62, 0x39, 0x34, + 0x4A, 0x6C, 0x41, 0x47, 0x44, 0x31, 0x7A, 0x51, 0x62, 0x7A, 0x45, 0x43, 0x6C, 0x38, 0x4C, 0x69, 0x62, 0x5A, 0x39, 0x57, 0x59, 0x6B, 0x54, 0x75, 0x6E, 0x68, 0x48, 0x69, 0x56, 0x4A, 0x71, 0x52, 0x61, 0x43, 0x50, 0x67, 0x72, 0x64, 0x4C, 0x51, + 0x41, 0x42, 0x44, 0x7A, 0x66, 0x75, 0x42, 0x53, 0x4F, 0x36, 0x4E, 0x2B, 0x70, 0x6A, 0x57, 0x78, 0x6E, 0x0A, 0x6B, 0x6A, 0x4D, 0x64, 0x77, 0x4C, 0x66, 0x53, 0x37, 0x4A, 0x4C, 0x49, 0x76, 0x67, 0x6D, 0x2F, 0x4C, 0x43, 0x6B, 0x46, 0x62, 0x77, + 0x4A, 0x72, 0x6E, 0x75, 0x2B, 0x38, 0x76, 0x79, 0x71, 0x38, 0x57, 0x38, 0x42, 0x51, 0x6A, 0x30, 0x46, 0x77, 0x63, 0x59, 0x65, 0x79, 0x54, 0x62, 0x63, 0x45, 0x71, 0x59, 0x53, 0x6A, 0x4D, 0x71, 0x2B, 0x75, 0x37, 0x6D, 0x73, 0x58, 0x69, 0x37, + 0x4B, 0x78, 0x2F, 0x6D, 0x7A, 0x68, 0x6B, 0x49, 0x79, 0x49, 0x71, 0x4A, 0x64, 0x49, 0x0A, 0x7A, 0x73, 0x68, 0x4E, 0x79, 0x2F, 0x4D, 0x47, 0x7A, 0x31, 0x39, 0x71, 0x43, 0x6B, 0x4B, 0x78, 0x48, 0x68, 0x35, 0x33, 0x4C, 0x34, 0x36, 0x67, 0x35, + 0x70, 0x49, 0x4F, 0x42, 0x76, 0x77, 0x46, 0x49, 0x74, 0x49, 0x6D, 0x34, 0x54, 0x46, 0x52, 0x66, 0x54, 0x4C, 0x63, 0x44, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x79, 0x4D, 0x77, 0x49, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, + 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x0A, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4E, + 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x58, 0x7A, 0x53, 0x42, 0x64, 0x75, 0x2B, 0x57, 0x48, 0x64, 0x58, 0x6C, 0x74, 0x64, 0x6B, 0x43, + 0x59, 0x34, 0x51, 0x57, 0x77, 0x61, 0x36, 0x67, 0x0A, 0x63, 0x46, 0x47, 0x6E, 0x39, 0x30, 0x78, 0x48, 0x4E, 0x63, 0x67, 0x4C, 0x31, 0x79, 0x67, 0x39, 0x69, 0x58, 0x48, 0x5A, 0x71, 0x6A, 0x4E, 0x42, 0x36, 0x68, 0x51, 0x62, 0x62, 0x43, 0x45, + 0x41, 0x77, 0x47, 0x78, 0x43, 0x47, 0x58, 0x36, 0x66, 0x61, 0x56, 0x73, 0x67, 0x51, 0x74, 0x2B, 0x69, 0x30, 0x74, 0x72, 0x45, 0x66, 0x4A, 0x64, 0x4C, 0x6A, 0x62, 0x44, 0x6F, 0x72, 0x4D, 0x6A, 0x75, 0x70, 0x57, 0x6B, 0x45, 0x6D, 0x51, 0x71, + 0x53, 0x70, 0x71, 0x73, 0x6E, 0x0A, 0x4C, 0x68, 0x70, 0x4E, 0x67, 0x62, 0x2B, 0x45, 0x31, 0x48, 0x41, 0x65, 0x72, 0x55, 0x66, 0x2B, 0x2F, 0x55, 0x71, 0x64, 0x4D, 0x2B, 0x44, 0x79, 0x75, 0x63, 0x52, 0x46, 0x43, 0x43, 0x45, 0x4B, 0x32, 0x6D, + 0x6C, 0x70, 0x63, 0x33, 0x49, 0x4E, 0x76, 0x6A, 0x54, 0x2B, 0x6C, 0x49, 0x75, 0x74, 0x77, 0x78, 0x34, 0x31, 0x31, 0x36, 0x4B, 0x44, 0x37, 0x2B, 0x55, 0x34, 0x78, 0x36, 0x57, 0x46, 0x48, 0x36, 0x76, 0x50, 0x4E, 0x4F, 0x77, 0x2F, 0x4B, 0x50, + 0x34, 0x4D, 0x0A, 0x38, 0x56, 0x65, 0x47, 0x54, 0x73, 0x6C, 0x56, 0x39, 0x78, 0x7A, 0x55, 0x32, 0x4B, 0x56, 0x39, 0x42, 0x6E, 0x70, 0x76, 0x31, 0x64, 0x38, 0x51, 0x33, 0x34, 0x46, 0x4F, 0x49, 0x57, 0x57, 0x78, 0x74, 0x75, 0x45, 0x58, 0x65, + 0x5A, 0x56, 0x46, 0x42, 0x73, 0x35, 0x66, 0x7A, 0x4E, 0x78, 0x47, 0x69, 0x57, 0x4E, 0x6F, 0x52, 0x49, 0x32, 0x54, 0x39, 0x47, 0x52, 0x77, 0x6F, 0x44, 0x32, 0x64, 0x4B, 0x41, 0x58, 0x44, 0x4F, 0x58, 0x43, 0x34, 0x59, 0x6E, 0x73, 0x67, 0x0A, + 0x2F, 0x65, 0x54, 0x62, 0x36, 0x51, 0x69, 0x68, 0x75, 0x4A, 0x34, 0x39, 0x43, 0x63, 0x64, 0x50, 0x2B, 0x79, 0x7A, 0x34, 0x6B, 0x33, 0x5A, 0x42, 0x33, 0x6C, 0x4C, 0x67, 0x34, 0x56, 0x66, 0x53, 0x6E, 0x51, 0x4F, 0x38, 0x64, 0x35, 0x37, 0x2B, + 0x6E, 0x69, 0x6C, 0x65, 0x39, 0x38, 0x46, 0x52, 0x59, 0x42, 0x2F, 0x65, 0x32, 0x67, 0x75, 0x79, 0x4C, 0x58, 0x57, 0x33, 0x51, 0x30, 0x69, 0x54, 0x35, 0x2F, 0x5A, 0x35, 0x78, 0x6F, 0x52, 0x64, 0x67, 0x46, 0x6C, 0x67, 0x0A, 0x6C, 0x50, 0x78, + 0x34, 0x6D, 0x49, 0x38, 0x38, 0x6B, 0x31, 0x48, 0x74, 0x51, 0x4A, 0x41, 0x48, 0x33, 0x32, 0x52, 0x6A, 0x4A, 0x4D, 0x74, 0x4F, 0x63, 0x51, 0x57, 0x68, 0x31, 0x35, 0x51, 0x61, 0x69, 0x44, 0x4C, 0x78, 0x49, 0x6E, 0x51, 0x69, 0x72, 0x71, 0x57, + 0x6D, 0x32, 0x42, 0x4A, 0x70, 0x54, 0x47, 0x43, 0x6A, 0x41, 0x75, 0x34, 0x72, 0x37, 0x4E, 0x52, 0x6A, 0x6B, 0x67, 0x74, 0x65, 0x76, 0x69, 0x39, 0x32, 0x61, 0x36, 0x4F, 0x32, 0x4A, 0x72, 0x79, 0x50, 0x0A, 0x41, 0x39, 0x67, 0x4B, 0x38, 0x6B, + 0x78, 0x6B, 0x52, 0x72, 0x30, 0x35, 0x59, 0x75, 0x57, 0x57, 0x36, 0x7A, 0x52, 0x6A, 0x45, 0x53, 0x6A, 0x4D, 0x6C, 0x66, 0x47, 0x74, 0x37, 0x2B, 0x2F, 0x63, 0x67, 0x46, 0x68, 0x49, 0x36, 0x55, 0x75, 0x34, 0x36, 0x6D, 0x57, 0x73, 0x36, 0x66, + 0x79, 0x41, 0x74, 0x62, 0x58, 0x49, 0x52, 0x66, 0x6D, 0x73, 0x77, 0x5A, 0x2F, 0x5A, 0x75, 0x65, 0x70, 0x69, 0x69, 0x49, 0x37, 0x45, 0x38, 0x55, 0x75, 0x44, 0x45, 0x71, 0x33, 0x6D, 0x0A, 0x69, 0x34, 0x54, 0x57, 0x6E, 0x73, 0x4C, 0x72, 0x67, + 0x78, 0x69, 0x66, 0x61, 0x72, 0x73, 0x62, 0x4A, 0x47, 0x41, 0x7A, 0x63, 0x4D, 0x7A, 0x73, 0x39, 0x7A, 0x4C, 0x7A, 0x58, 0x4E, 0x6C, 0x35, 0x66, 0x65, 0x2B, 0x65, 0x70, 0x50, 0x37, 0x4A, 0x49, 0x38, 0x4D, 0x6B, 0x37, 0x68, 0x57, 0x53, 0x73, + 0x54, 0x32, 0x52, 0x54, 0x79, 0x61, 0x47, 0x76, 0x57, 0x5A, 0x7A, 0x4A, 0x42, 0x50, 0x71, 0x70, 0x4B, 0x35, 0x6A, 0x77, 0x61, 0x31, 0x39, 0x68, 0x41, 0x4D, 0x38, 0x0A, 0x45, 0x48, 0x69, 0x47, 0x47, 0x33, 0x6E, 0x6A, 0x78, 0x50, 0x50, 0x79, + 0x42, 0x4A, 0x55, 0x67, 0x72, 0x69, 0x4F, 0x43, 0x78, 0x4C, 0x4D, 0x36, 0x41, 0x47, 0x4B, 0x2F, 0x35, 0x6A, 0x59, 0x6B, 0x34, 0x56, 0x65, 0x36, 0x78, 0x78, 0x36, 0x51, 0x64, 0x64, 0x56, 0x66, 0x50, 0x35, 0x56, 0x68, 0x4B, 0x38, 0x45, 0x37, + 0x7A, 0x65, 0x57, 0x7A, 0x61, 0x47, 0x48, 0x51, 0x52, 0x69, 0x61, 0x70, 0x49, 0x56, 0x4A, 0x70, 0x4C, 0x65, 0x73, 0x75, 0x78, 0x2B, 0x74, 0x33, 0x0A, 0x7A, 0x71, 0x59, 0x36, 0x74, 0x51, 0x4D, 0x7A, 0x54, 0x33, 0x62, 0x52, 0x35, 0x31, 0x78, + 0x55, 0x41, 0x56, 0x33, 0x4C, 0x65, 0x50, 0x54, 0x4A, 0x44, 0x4C, 0x2F, 0x50, 0x45, 0x6F, 0x34, 0x58, 0x4C, 0x53, 0x4E, 0x6F, 0x6C, 0x4F, 0x65, 0x72, 0x2F, 0x71, 0x6D, 0x79, 0x4B, 0x77, 0x62, 0x51, 0x42, 0x4D, 0x30, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x65, 0x6C, 0x69, 0x61, 0x53, 0x6F, 0x6E, 0x65, 0x72, 0x61, 0x20, 0x52, 0x6F, 0x6F, + 0x74, 0x20, 0x43, 0x41, 0x20, 0x76, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, + 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x4F, 0x44, 0x43, 0x43, 0x41, 0x79, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x52, 0x41, + 0x4A, 0x57, 0x2B, 0x46, 0x71, 0x44, 0x33, 0x4C, 0x6B, 0x62, 0x78, 0x65, 0x7A, 0x6D, 0x43, 0x63, 0x76, 0x71, 0x4C, 0x7A, 0x5A, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x46, 0x42, + 0x51, 0x41, 0x77, 0x4E, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x67, 0x77, 0x4C, 0x56, 0x47, 0x56, 0x73, 0x61, 0x57, 0x46, 0x54, 0x62, 0x32, 0x35, 0x6C, 0x63, 0x6D, 0x45, 0x78, 0x48, 0x7A, 0x41, 0x64, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x6C, 0x52, 0x6C, 0x62, 0x47, 0x6C, 0x68, 0x55, 0x32, 0x39, 0x75, 0x5A, 0x58, 0x4A, 0x68, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x64, 0x6A, 0x45, 0x77, + 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x63, 0x78, 0x4D, 0x44, 0x45, 0x34, 0x0A, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x55, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x49, 0x78, 0x4D, 0x44, 0x45, 0x34, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x55, + 0x77, 0x57, 0x6A, 0x41, 0x33, 0x4D, 0x52, 0x51, 0x77, 0x45, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x74, 0x55, 0x5A, 0x57, 0x78, 0x70, 0x59, 0x56, 0x4E, 0x76, 0x62, 0x6D, 0x56, 0x79, 0x59, 0x54, 0x45, 0x66, 0x4D, 0x42, 0x30, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x57, 0x0A, 0x56, 0x47, 0x56, 0x73, 0x61, 0x57, 0x46, 0x54, 0x62, 0x32, 0x35, 0x6C, 0x63, 0x6D, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x32, 0x4D, 0x54, + 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, + 0x49, 0x42, 0x41, 0x4D, 0x4B, 0x2B, 0x0A, 0x36, 0x79, 0x66, 0x77, 0x49, 0x61, 0x50, 0x7A, 0x61, 0x53, 0x5A, 0x56, 0x66, 0x70, 0x33, 0x46, 0x56, 0x52, 0x61, 0x52, 0x58, 0x50, 0x33, 0x76, 0x49, 0x62, 0x39, 0x54, 0x67, 0x48, 0x6F, 0x74, 0x30, + 0x70, 0x47, 0x4D, 0x59, 0x7A, 0x48, 0x77, 0x37, 0x43, 0x54, 0x77, 0x77, 0x36, 0x58, 0x53, 0x63, 0x6E, 0x77, 0x51, 0x62, 0x66, 0x51, 0x33, 0x74, 0x2B, 0x58, 0x6D, 0x66, 0x48, 0x6E, 0x71, 0x6A, 0x4C, 0x57, 0x43, 0x69, 0x36, 0x35, 0x49, 0x74, + 0x71, 0x77, 0x41, 0x0A, 0x33, 0x47, 0x56, 0x31, 0x37, 0x43, 0x70, 0x4E, 0x58, 0x38, 0x47, 0x48, 0x39, 0x53, 0x42, 0x6C, 0x4B, 0x34, 0x47, 0x6F, 0x52, 0x7A, 0x36, 0x4A, 0x49, 0x35, 0x55, 0x77, 0x46, 0x70, 0x42, 0x2F, 0x36, 0x46, 0x63, 0x48, + 0x53, 0x4F, 0x63, 0x5A, 0x72, 0x72, 0x39, 0x46, 0x5A, 0x37, 0x45, 0x33, 0x47, 0x77, 0x59, 0x71, 0x2F, 0x74, 0x37, 0x35, 0x72, 0x48, 0x32, 0x44, 0x2B, 0x31, 0x36, 0x36, 0x35, 0x49, 0x2B, 0x58, 0x5A, 0x37, 0x35, 0x4C, 0x6A, 0x6F, 0x31, 0x6B, + 0x0A, 0x42, 0x31, 0x63, 0x34, 0x56, 0x57, 0x6B, 0x30, 0x4E, 0x6A, 0x30, 0x54, 0x53, 0x4F, 0x39, 0x50, 0x34, 0x74, 0x4E, 0x6D, 0x48, 0x71, 0x54, 0x50, 0x47, 0x72, 0x64, 0x65, 0x4E, 0x6A, 0x50, 0x55, 0x74, 0x41, 0x61, 0x39, 0x47, 0x41, 0x48, + 0x39, 0x64, 0x34, 0x52, 0x51, 0x41, 0x45, 0x58, 0x31, 0x6A, 0x46, 0x33, 0x6F, 0x49, 0x37, 0x78, 0x2B, 0x2F, 0x6A, 0x58, 0x68, 0x37, 0x56, 0x42, 0x37, 0x71, 0x54, 0x43, 0x4E, 0x47, 0x64, 0x4D, 0x4A, 0x6A, 0x6D, 0x68, 0x6E, 0x0A, 0x58, 0x62, + 0x38, 0x38, 0x6C, 0x78, 0x68, 0x54, 0x75, 0x79, 0x6C, 0x69, 0x78, 0x63, 0x70, 0x65, 0x63, 0x73, 0x48, 0x48, 0x6C, 0x74, 0x54, 0x62, 0x4C, 0x61, 0x43, 0x30, 0x48, 0x32, 0x6B, 0x44, 0x37, 0x4F, 0x72, 0x69, 0x55, 0x50, 0x45, 0x4D, 0x50, 0x50, + 0x43, 0x73, 0x38, 0x31, 0x4D, 0x74, 0x38, 0x42, 0x7A, 0x31, 0x37, 0x57, 0x77, 0x35, 0x4F, 0x58, 0x4F, 0x41, 0x46, 0x73, 0x68, 0x53, 0x73, 0x43, 0x50, 0x4E, 0x34, 0x44, 0x37, 0x63, 0x33, 0x54, 0x78, 0x48, 0x0A, 0x6F, 0x4C, 0x73, 0x31, 0x69, + 0x75, 0x4B, 0x59, 0x61, 0x49, 0x75, 0x2B, 0x35, 0x62, 0x39, 0x79, 0x37, 0x74, 0x4C, 0x36, 0x70, 0x65, 0x30, 0x53, 0x37, 0x66, 0x79, 0x59, 0x47, 0x4B, 0x6B, 0x6D, 0x64, 0x74, 0x77, 0x6F, 0x53, 0x78, 0x41, 0x67, 0x48, 0x4E, 0x4E, 0x2F, 0x46, + 0x6E, 0x63, 0x74, 0x37, 0x57, 0x2B, 0x41, 0x39, 0x30, 0x6D, 0x37, 0x55, 0x77, 0x57, 0x37, 0x58, 0x57, 0x6A, 0x48, 0x31, 0x4D, 0x68, 0x31, 0x46, 0x6A, 0x2B, 0x4A, 0x57, 0x6F, 0x76, 0x33, 0x0A, 0x46, 0x30, 0x66, 0x55, 0x54, 0x50, 0x48, 0x53, + 0x69, 0x58, 0x6B, 0x2B, 0x54, 0x54, 0x32, 0x59, 0x71, 0x47, 0x48, 0x65, 0x4F, 0x68, 0x37, 0x53, 0x2B, 0x46, 0x34, 0x44, 0x34, 0x4D, 0x48, 0x4A, 0x48, 0x49, 0x7A, 0x54, 0x6A, 0x55, 0x33, 0x54, 0x6C, 0x54, 0x61, 0x7A, 0x4E, 0x31, 0x39, 0x6A, + 0x59, 0x35, 0x73, 0x7A, 0x46, 0x50, 0x41, 0x74, 0x4A, 0x6D, 0x74, 0x54, 0x66, 0x49, 0x6D, 0x4D, 0x4D, 0x73, 0x4A, 0x75, 0x37, 0x44, 0x30, 0x68, 0x41, 0x44, 0x6E, 0x4A, 0x0A, 0x6F, 0x57, 0x6A, 0x69, 0x55, 0x49, 0x4D, 0x75, 0x73, 0x44, 0x6F, + 0x72, 0x38, 0x7A, 0x61, 0x67, 0x72, 0x43, 0x2F, 0x6B, 0x62, 0x32, 0x48, 0x43, 0x55, 0x51, 0x6B, 0x35, 0x50, 0x6F, 0x74, 0x54, 0x75, 0x62, 0x74, 0x6E, 0x32, 0x74, 0x78, 0x54, 0x75, 0x58, 0x5A, 0x5A, 0x4E, 0x70, 0x31, 0x44, 0x35, 0x53, 0x44, + 0x67, 0x50, 0x54, 0x4A, 0x67, 0x68, 0x53, 0x4A, 0x52, 0x74, 0x38, 0x63, 0x7A, 0x75, 0x39, 0x30, 0x56, 0x4C, 0x36, 0x52, 0x34, 0x70, 0x67, 0x64, 0x37, 0x0A, 0x67, 0x55, 0x59, 0x32, 0x42, 0x49, 0x62, 0x64, 0x65, 0x54, 0x58, 0x48, 0x6C, 0x53, + 0x77, 0x37, 0x73, 0x4B, 0x4D, 0x58, 0x4E, 0x65, 0x56, 0x7A, 0x48, 0x37, 0x52, 0x63, 0x57, 0x65, 0x2F, 0x61, 0x36, 0x68, 0x42, 0x6C, 0x65, 0x33, 0x72, 0x51, 0x66, 0x35, 0x2B, 0x7A, 0x74, 0x43, 0x6F, 0x33, 0x4F, 0x33, 0x43, 0x4C, 0x6D, 0x31, + 0x75, 0x35, 0x4B, 0x37, 0x66, 0x73, 0x73, 0x6C, 0x45, 0x53, 0x6C, 0x31, 0x4D, 0x70, 0x57, 0x74, 0x54, 0x77, 0x45, 0x68, 0x44, 0x63, 0x0A, 0x54, 0x77, 0x4B, 0x37, 0x45, 0x70, 0x49, 0x76, 0x59, 0x74, 0x51, 0x2F, 0x61, 0x55, 0x4E, 0x38, 0x44, + 0x64, 0x62, 0x38, 0x57, 0x48, 0x55, 0x42, 0x69, 0x4A, 0x31, 0x59, 0x46, 0x6B, 0x76, 0x65, 0x75, 0x70, 0x44, 0x2F, 0x52, 0x77, 0x47, 0x4A, 0x42, 0x6D, 0x72, 0x32, 0x58, 0x37, 0x4B, 0x51, 0x61, 0x72, 0x4D, 0x43, 0x70, 0x67, 0x4B, 0x49, 0x76, + 0x37, 0x4E, 0x48, 0x66, 0x69, 0x72, 0x5A, 0x31, 0x66, 0x70, 0x6F, 0x65, 0x44, 0x56, 0x4E, 0x41, 0x67, 0x4D, 0x42, 0x0A, 0x41, 0x41, 0x47, 0x6A, 0x50, 0x7A, 0x41, 0x39, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, + 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x43, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, + 0x42, 0x42, 0x54, 0x77, 0x6A, 0x31, 0x6B, 0x34, 0x41, 0x4C, 0x50, 0x31, 0x6A, 0x35, 0x71, 0x57, 0x0A, 0x44, 0x4E, 0x58, 0x72, 0x2B, 0x6E, 0x75, 0x71, 0x46, 0x2B, 0x67, 0x54, 0x45, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, + 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x76, 0x75, 0x52, 0x63, 0x59, 0x6B, 0x34, 0x6B, 0x39, 0x41, 0x77, 0x49, 0x2F, 0x2F, 0x44, 0x54, 0x44, 0x47, 0x6A, 0x6B, 0x6B, 0x30, 0x6B, + 0x69, 0x50, 0x30, 0x51, 0x6E, 0x62, 0x37, 0x74, 0x74, 0x33, 0x6F, 0x4E, 0x6D, 0x0A, 0x7A, 0x71, 0x6A, 0x4D, 0x44, 0x66, 0x7A, 0x31, 0x6D, 0x67, 0x62, 0x6C, 0x64, 0x78, 0x53, 0x52, 0x36, 0x35, 0x31, 0x42, 0x65, 0x35, 0x6B, 0x71, 0x68, 0x4F, + 0x58, 0x2F, 0x2F, 0x43, 0x48, 0x42, 0x58, 0x66, 0x44, 0x6B, 0x48, 0x31, 0x65, 0x33, 0x64, 0x61, 0x6D, 0x68, 0x58, 0x77, 0x49, 0x6D, 0x2F, 0x39, 0x66, 0x48, 0x39, 0x30, 0x37, 0x65, 0x54, 0x2F, 0x6A, 0x33, 0x48, 0x45, 0x62, 0x41, 0x65, 0x6B, + 0x39, 0x41, 0x4C, 0x43, 0x49, 0x31, 0x38, 0x42, 0x6D, 0x78, 0x0A, 0x30, 0x47, 0x74, 0x6E, 0x4C, 0x4C, 0x43, 0x6F, 0x34, 0x4D, 0x42, 0x41, 0x4E, 0x7A, 0x58, 0x32, 0x68, 0x46, 0x78, 0x63, 0x34, 0x36, 0x39, 0x43, 0x65, 0x50, 0x36, 0x6E, 0x79, + 0x51, 0x31, 0x51, 0x36, 0x67, 0x32, 0x45, 0x64, 0x76, 0x5A, 0x52, 0x37, 0x34, 0x4E, 0x54, 0x78, 0x6E, 0x72, 0x2F, 0x44, 0x6C, 0x5A, 0x4A, 0x4C, 0x6F, 0x39, 0x36, 0x31, 0x67, 0x7A, 0x6D, 0x4A, 0x31, 0x54, 0x6A, 0x54, 0x51, 0x70, 0x67, 0x63, + 0x6D, 0x4C, 0x4E, 0x6B, 0x51, 0x66, 0x57, 0x0A, 0x70, 0x62, 0x2F, 0x49, 0x6D, 0x57, 0x76, 0x74, 0x78, 0x42, 0x6E, 0x6D, 0x71, 0x30, 0x77, 0x52, 0x4F, 0x4D, 0x56, 0x76, 0x4D, 0x65, 0x4A, 0x75, 0x53, 0x63, 0x67, 0x2F, 0x64, 0x6F, 0x41, 0x6D, + 0x41, 0x79, 0x59, 0x70, 0x34, 0x44, 0x62, 0x32, 0x39, 0x69, 0x42, 0x54, 0x34, 0x78, 0x64, 0x77, 0x4E, 0x42, 0x65, 0x64, 0x59, 0x32, 0x67, 0x65, 0x61, 0x2B, 0x7A, 0x44, 0x54, 0x59, 0x61, 0x34, 0x45, 0x7A, 0x41, 0x76, 0x58, 0x55, 0x59, 0x4E, + 0x52, 0x30, 0x50, 0x56, 0x0A, 0x47, 0x36, 0x70, 0x5A, 0x44, 0x72, 0x6C, 0x63, 0x6A, 0x51, 0x5A, 0x49, 0x72, 0x58, 0x53, 0x48, 0x58, 0x38, 0x66, 0x38, 0x4D, 0x56, 0x52, 0x42, 0x45, 0x2B, 0x4C, 0x48, 0x49, 0x51, 0x36, 0x65, 0x34, 0x42, 0x34, + 0x4E, 0x34, 0x63, 0x42, 0x37, 0x51, 0x34, 0x57, 0x51, 0x78, 0x59, 0x70, 0x59, 0x78, 0x6D, 0x55, 0x4B, 0x65, 0x46, 0x66, 0x79, 0x78, 0x69, 0x4D, 0x50, 0x41, 0x64, 0x6B, 0x67, 0x53, 0x39, 0x34, 0x50, 0x2B, 0x35, 0x4B, 0x46, 0x64, 0x53, 0x70, + 0x63, 0x0A, 0x63, 0x34, 0x31, 0x74, 0x65, 0x79, 0x57, 0x52, 0x79, 0x75, 0x35, 0x46, 0x72, 0x67, 0x5A, 0x4C, 0x41, 0x4D, 0x7A, 0x54, 0x73, 0x56, 0x6C, 0x51, 0x32, 0x6A, 0x71, 0x49, 0x4F, 0x79, 0x6C, 0x44, 0x52, 0x6C, 0x36, 0x58, 0x4B, 0x31, + 0x54, 0x4F, 0x55, 0x32, 0x2B, 0x4E, 0x53, 0x75, 0x65, 0x57, 0x2B, 0x72, 0x39, 0x78, 0x44, 0x6B, 0x4B, 0x4C, 0x66, 0x50, 0x30, 0x6F, 0x6F, 0x4E, 0x42, 0x49, 0x79, 0x74, 0x72, 0x45, 0x67, 0x55, 0x79, 0x37, 0x6F, 0x6E, 0x4F, 0x54, 0x0A, 0x4A, + 0x73, 0x6A, 0x72, 0x44, 0x4E, 0x59, 0x6D, 0x69, 0x4C, 0x62, 0x41, 0x4A, 0x4D, 0x2B, 0x37, 0x76, 0x56, 0x76, 0x72, 0x64, 0x58, 0x33, 0x70, 0x43, 0x49, 0x36, 0x47, 0x4D, 0x79, 0x78, 0x35, 0x64, 0x77, 0x6C, 0x70, 0x70, 0x59, 0x6E, 0x38, 0x73, + 0x33, 0x43, 0x51, 0x68, 0x33, 0x61, 0x50, 0x30, 0x79, 0x4B, 0x37, 0x51, 0x73, 0x36, 0x39, 0x63, 0x77, 0x73, 0x67, 0x4A, 0x69, 0x72, 0x51, 0x6D, 0x7A, 0x31, 0x77, 0x48, 0x69, 0x52, 0x73, 0x7A, 0x59, 0x64, 0x32, 0x0A, 0x71, 0x52, 0x65, 0x57, + 0x74, 0x38, 0x38, 0x4E, 0x6B, 0x76, 0x75, 0x4F, 0x47, 0x4B, 0x6D, 0x59, 0x53, 0x64, 0x47, 0x65, 0x2F, 0x6D, 0x42, 0x45, 0x63, 0x69, 0x47, 0x35, 0x47, 0x65, 0x33, 0x43, 0x39, 0x54, 0x48, 0x78, 0x4F, 0x55, 0x69, 0x49, 0x6B, 0x43, 0x52, 0x31, + 0x56, 0x42, 0x61, 0x74, 0x7A, 0x76, 0x54, 0x34, 0x61, 0x52, 0x52, 0x6B, 0x4F, 0x66, 0x75, 0x6A, 0x75, 0x4C, 0x70, 0x77, 0x51, 0x4D, 0x63, 0x6E, 0x48, 0x4C, 0x2F, 0x45, 0x56, 0x6C, 0x50, 0x36, 0x0A, 0x59, 0x32, 0x58, 0x51, 0x38, 0x78, 0x77, + 0x4F, 0x46, 0x76, 0x56, 0x72, 0x68, 0x6C, 0x68, 0x4E, 0x47, 0x4E, 0x54, 0x6B, 0x44, 0x59, 0x36, 0x6C, 0x6E, 0x56, 0x75, 0x52, 0x33, 0x48, 0x59, 0x6B, 0x55, 0x44, 0x2F, 0x47, 0x4B, 0x76, 0x76, 0x5A, 0x74, 0x35, 0x79, 0x31, 0x31, 0x75, 0x62, + 0x51, 0x32, 0x65, 0x67, 0x5A, 0x69, 0x78, 0x56, 0x78, 0x53, 0x4B, 0x32, 0x33, 0x36, 0x74, 0x68, 0x5A, 0x69, 0x4E, 0x53, 0x51, 0x76, 0x78, 0x61, 0x7A, 0x32, 0x65, 0x6D, 0x73, 0x0A, 0x57, 0x57, 0x46, 0x55, 0x79, 0x42, 0x79, 0x36, 0x79, 0x73, + 0x48, 0x4B, 0x34, 0x62, 0x6B, 0x67, 0x54, 0x49, 0x38, 0x36, 0x6B, 0x34, 0x6D, 0x6C, 0x6F, 0x4D, 0x79, 0x2F, 0x30, 0x2F, 0x5A, 0x31, 0x70, 0x48, 0x57, 0x57, 0x62, 0x56, 0x59, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, + 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x2D, 0x54, 0x65, 0x6C, 0x65, 0x53, 0x65, 0x63, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, + 0x6C, 0x61, 0x73, 0x73, 0x20, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x77, 0x7A, 0x43, 0x43, 0x41, 0x71, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, + 0x41, 0x67, 0x49, 0x42, 0x41, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x67, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x0A, 0x49, 0x6C, 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x46, 0x62, 0x6E, 0x52, + 0x6C, 0x63, 0x6E, 0x42, 0x79, 0x61, 0x58, 0x4E, 0x6C, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, + 0x4D, 0x46, 0x6C, 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x55, 0x0A, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x57, 0x35, 0x30, 0x5A, 0x58, 0x49, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x46, 0x51, 0x74, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x56, 0x4E, 0x6C, 0x59, 0x79, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x73, 0x59, 0x58, + 0x4E, 0x7A, 0x49, 0x44, 0x49, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x67, 0x78, 0x0A, 0x4D, 0x44, 0x41, 0x78, 0x4D, 0x54, 0x41, 0x30, 0x4D, 0x44, 0x45, 0x30, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x4D, 0x78, 0x4D, 0x44, 0x41, 0x78, 0x4D, + 0x6A, 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x43, 0x42, 0x67, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x6F, 0x4D, 0x49, 0x6C, 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x0A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x46, 0x62, 0x6E, 0x52, 0x6C, 0x63, 0x6E, 0x42, 0x79, 0x61, 0x58, 0x4E, 0x6C, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, + 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x64, 0x74, 0x59, 0x6B, 0x67, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x46, 0x6C, 0x51, 0x74, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x55, + 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x44, 0x0A, 0x5A, 0x57, 0x35, 0x30, 0x5A, 0x58, 0x49, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x46, 0x51, 0x74, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x56, 0x4E, + 0x6C, 0x59, 0x79, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x73, 0x59, 0x58, 0x4E, 0x7A, 0x49, 0x44, 0x49, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, + 0x47, 0x53, 0x49, 0x62, 0x33, 0x0A, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x71, 0x58, 0x39, 0x6F, 0x62, 0x58, 0x2B, + 0x68, 0x7A, 0x6B, 0x65, 0x58, 0x61, 0x58, 0x50, 0x53, 0x69, 0x35, 0x6B, 0x66, 0x6C, 0x38, 0x32, 0x68, 0x56, 0x59, 0x41, 0x55, 0x64, 0x41, 0x71, 0x53, 0x7A, 0x6D, 0x31, 0x6E, 0x7A, 0x48, 0x6F, 0x71, 0x76, 0x4E, 0x4B, 0x33, 0x38, 0x44, 0x63, + 0x4C, 0x5A, 0x0A, 0x53, 0x42, 0x6E, 0x75, 0x61, 0x59, 0x2F, 0x4A, 0x49, 0x50, 0x77, 0x68, 0x71, 0x67, 0x63, 0x5A, 0x37, 0x62, 0x42, 0x63, 0x72, 0x47, 0x58, 0x48, 0x58, 0x2B, 0x30, 0x43, 0x66, 0x48, 0x74, 0x38, 0x4C, 0x52, 0x76, 0x57, 0x75, + 0x72, 0x6D, 0x41, 0x77, 0x68, 0x69, 0x43, 0x46, 0x6F, 0x54, 0x36, 0x5A, 0x72, 0x41, 0x49, 0x78, 0x6C, 0x51, 0x6A, 0x67, 0x65, 0x54, 0x4E, 0x75, 0x55, 0x6B, 0x2F, 0x39, 0x6B, 0x39, 0x75, 0x4E, 0x30, 0x67, 0x6F, 0x4F, 0x41, 0x2F, 0x46, 0x0A, + 0x76, 0x75, 0x64, 0x6F, 0x63, 0x50, 0x30, 0x35, 0x6C, 0x30, 0x33, 0x53, 0x78, 0x35, 0x69, 0x52, 0x55, 0x4B, 0x72, 0x45, 0x52, 0x4C, 0x4D, 0x6A, 0x66, 0x54, 0x6C, 0x48, 0x36, 0x56, 0x4A, 0x69, 0x31, 0x68, 0x4B, 0x54, 0x58, 0x72, 0x63, 0x78, + 0x6C, 0x6B, 0x49, 0x46, 0x2B, 0x33, 0x61, 0x6E, 0x48, 0x71, 0x50, 0x31, 0x77, 0x76, 0x7A, 0x70, 0x65, 0x73, 0x56, 0x73, 0x71, 0x58, 0x46, 0x50, 0x36, 0x73, 0x74, 0x34, 0x76, 0x47, 0x43, 0x76, 0x78, 0x39, 0x37, 0x30, 0x0A, 0x32, 0x63, 0x75, + 0x2B, 0x66, 0x6A, 0x4F, 0x6C, 0x62, 0x70, 0x53, 0x44, 0x38, 0x44, 0x54, 0x36, 0x49, 0x61, 0x76, 0x71, 0x6A, 0x6E, 0x4B, 0x67, 0x50, 0x36, 0x54, 0x65, 0x4D, 0x46, 0x76, 0x76, 0x68, 0x6B, 0x31, 0x71, 0x6C, 0x56, 0x74, 0x44, 0x52, 0x4B, 0x67, + 0x51, 0x46, 0x52, 0x7A, 0x6C, 0x41, 0x56, 0x66, 0x46, 0x6D, 0x50, 0x48, 0x6D, 0x42, 0x69, 0x69, 0x52, 0x71, 0x69, 0x44, 0x46, 0x74, 0x31, 0x4D, 0x6D, 0x55, 0x55, 0x4F, 0x79, 0x43, 0x78, 0x47, 0x56, 0x0A, 0x57, 0x4F, 0x48, 0x41, 0x44, 0x33, + 0x62, 0x5A, 0x77, 0x49, 0x31, 0x38, 0x67, 0x66, 0x4E, 0x79, 0x63, 0x4A, 0x35, 0x76, 0x2F, 0x68, 0x71, 0x4F, 0x32, 0x56, 0x38, 0x31, 0x78, 0x72, 0x4A, 0x76, 0x4E, 0x48, 0x79, 0x2B, 0x53, 0x45, 0x2F, 0x69, 0x57, 0x6A, 0x6E, 0x58, 0x32, 0x4A, + 0x31, 0x34, 0x6E, 0x70, 0x2B, 0x47, 0x50, 0x67, 0x4E, 0x65, 0x47, 0x59, 0x74, 0x45, 0x6F, 0x74, 0x58, 0x48, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, + 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, + 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x2F, 0x57, 0x53, 0x41, 0x32, 0x41, 0x48, 0x6D, 0x67, 0x6F, 0x43, 0x4A, 0x72, 0x6A, 0x4E, 0x58, 0x79, 0x0A, 0x59, 0x64, 0x4B, 0x34, 0x4C, 0x4D, 0x75, 0x43, 0x53, 0x6A, 0x41, 0x4E, + 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x45, 0x41, 0x4D, 0x51, 0x4F, 0x69, 0x59, 0x51, 0x73, 0x66, 0x64, 0x4F, 0x68, 0x79, 0x4E, 0x73, 0x5A, 0x74, + 0x2B, 0x55, 0x32, 0x65, 0x2B, 0x69, 0x4B, 0x6F, 0x34, 0x59, 0x46, 0x57, 0x7A, 0x38, 0x32, 0x37, 0x6E, 0x2B, 0x71, 0x72, 0x6B, 0x52, 0x6B, 0x34, 0x0A, 0x72, 0x36, 0x70, 0x38, 0x46, 0x55, 0x33, 0x7A, 0x74, 0x71, 0x4F, 0x4E, 0x70, 0x66, 0x53, + 0x4F, 0x39, 0x6B, 0x53, 0x70, 0x70, 0x2B, 0x67, 0x68, 0x6C, 0x61, 0x30, 0x2B, 0x41, 0x47, 0x49, 0x57, 0x69, 0x50, 0x41, 0x43, 0x75, 0x76, 0x78, 0x68, 0x49, 0x2B, 0x59, 0x7A, 0x6D, 0x7A, 0x42, 0x36, 0x61, 0x7A, 0x5A, 0x69, 0x65, 0x36, 0x30, + 0x45, 0x49, 0x34, 0x52, 0x59, 0x5A, 0x65, 0x4C, 0x62, 0x4B, 0x34, 0x72, 0x6E, 0x4A, 0x56, 0x4D, 0x33, 0x59, 0x6C, 0x4E, 0x66, 0x0A, 0x76, 0x4E, 0x6F, 0x42, 0x59, 0x69, 0x6D, 0x69, 0x70, 0x69, 0x64, 0x78, 0x35, 0x6A, 0x6F, 0x69, 0x66, 0x73, + 0x46, 0x76, 0x48, 0x5A, 0x56, 0x77, 0x49, 0x45, 0x6F, 0x48, 0x4E, 0x4E, 0x2F, 0x71, 0x2F, 0x78, 0x57, 0x41, 0x35, 0x62, 0x72, 0x58, 0x65, 0x74, 0x68, 0x62, 0x64, 0x58, 0x77, 0x46, 0x65, 0x69, 0x6C, 0x48, 0x66, 0x6B, 0x43, 0x6F, 0x4D, 0x52, + 0x4E, 0x33, 0x7A, 0x55, 0x41, 0x37, 0x74, 0x46, 0x46, 0x48, 0x65, 0x69, 0x34, 0x52, 0x34, 0x30, 0x63, 0x52, 0x0A, 0x33, 0x70, 0x31, 0x6D, 0x30, 0x49, 0x76, 0x56, 0x56, 0x47, 0x62, 0x36, 0x67, 0x31, 0x58, 0x71, 0x66, 0x4D, 0x49, 0x70, 0x69, + 0x52, 0x76, 0x70, 0x62, 0x37, 0x50, 0x4F, 0x34, 0x67, 0x57, 0x45, 0x79, 0x53, 0x38, 0x2B, 0x65, 0x49, 0x56, 0x69, 0x62, 0x73, 0x6C, 0x66, 0x77, 0x58, 0x68, 0x6A, 0x64, 0x46, 0x6A, 0x41, 0x53, 0x42, 0x67, 0x4D, 0x6D, 0x54, 0x6E, 0x72, 0x70, + 0x4D, 0x77, 0x61, 0x74, 0x58, 0x6C, 0x61, 0x6A, 0x52, 0x57, 0x63, 0x32, 0x42, 0x51, 0x4E, 0x0A, 0x39, 0x6E, 0x6F, 0x48, 0x56, 0x38, 0x63, 0x69, 0x67, 0x77, 0x55, 0x74, 0x50, 0x4A, 0x73, 0x6C, 0x4A, 0x6A, 0x30, 0x59, 0x73, 0x36, 0x6C, 0x44, + 0x66, 0x4D, 0x6A, 0x49, 0x71, 0x32, 0x53, 0x50, 0x44, 0x71, 0x4F, 0x2F, 0x6E, 0x42, 0x75, 0x64, 0x4D, 0x4E, 0x76, 0x61, 0x30, 0x42, 0x6B, 0x75, 0x71, 0x6A, 0x7A, 0x78, 0x2B, 0x7A, 0x4F, 0x41, 0x64, 0x75, 0x54, 0x4E, 0x72, 0x52, 0x6C, 0x50, + 0x42, 0x53, 0x65, 0x4F, 0x45, 0x36, 0x46, 0x75, 0x77, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, + 0x41, 0x74, 0x6F, 0x73, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x32, 0x30, 0x31, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x64, 0x7A, 0x43, 0x43, + 0x41, 0x6C, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x58, 0x44, 0x50, 0x4C, 0x59, 0x69, 0x78, 0x66, 0x73, 0x7A, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, + 0x42, 0x51, 0x41, 0x77, 0x50, 0x44, 0x45, 0x65, 0x4D, 0x42, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x56, 0x51, 0x58, 0x52, 0x76, 0x63, 0x79, 0x42, 0x55, 0x0A, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x47, 0x56, 0x6B, 0x55, 0x6D, 0x39, + 0x76, 0x64, 0x43, 0x41, 0x79, 0x4D, 0x44, 0x45, 0x78, 0x4D, 0x51, 0x30, 0x77, 0x43, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x52, 0x42, 0x64, 0x47, 0x39, 0x7A, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x47, 0x45, 0x77, 0x4A, 0x45, 0x52, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x54, 0x41, 0x33, 0x4D, 0x44, 0x63, 0x78, 0x4E, 0x44, 0x55, 0x34, 0x0A, 0x4D, 0x7A, 0x42, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x44, 0x45, 0x79, 0x4D, 0x7A, + 0x45, 0x79, 0x4D, 0x7A, 0x55, 0x35, 0x4E, 0x54, 0x6C, 0x61, 0x4D, 0x44, 0x77, 0x78, 0x48, 0x6A, 0x41, 0x63, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x55, 0x46, 0x30, 0x62, 0x33, 0x4D, 0x67, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, + 0x52, 0x6C, 0x5A, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x54, 0x45, 0x4E, 0x4D, 0x41, 0x73, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x45, 0x51, 0x58, 0x52, 0x76, 0x63, 0x7A, 0x45, 0x4C, 0x4D, + 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, + 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x56, 0x0A, 0x68, 0x54, 0x75, 0x58, 0x62, 0x79, 0x6F, 0x37, 0x4C, 0x6A, 0x76, 0x50, 0x70, 0x76, 0x4D, 0x70, 0x4E, 0x62, 0x37, 0x50, + 0x47, 0x4B, 0x77, 0x2B, 0x71, 0x74, 0x6E, 0x34, 0x54, 0x61, 0x41, 0x2B, 0x47, 0x6B, 0x65, 0x35, 0x76, 0x4A, 0x72, 0x66, 0x38, 0x76, 0x37, 0x4D, 0x50, 0x6B, 0x66, 0x6F, 0x65, 0x70, 0x62, 0x43, 0x4A, 0x49, 0x34, 0x31, 0x39, 0x4B, 0x6B, 0x4D, + 0x2F, 0x49, 0x4C, 0x39, 0x62, 0x63, 0x46, 0x79, 0x59, 0x69, 0x65, 0x39, 0x36, 0x6D, 0x76, 0x72, 0x0A, 0x35, 0x34, 0x72, 0x4D, 0x56, 0x44, 0x36, 0x51, 0x55, 0x4D, 0x2B, 0x41, 0x31, 0x4A, 0x58, 0x37, 0x36, 0x4C, 0x57, 0x43, 0x31, 0x42, 0x54, + 0x46, 0x74, 0x71, 0x6C, 0x56, 0x4A, 0x56, 0x66, 0x62, 0x73, 0x56, 0x44, 0x32, 0x73, 0x47, 0x42, 0x6B, 0x57, 0x58, 0x70, 0x70, 0x7A, 0x77, 0x4F, 0x33, 0x62, 0x77, 0x32, 0x2B, 0x79, 0x6A, 0x35, 0x76, 0x64, 0x48, 0x4C, 0x71, 0x71, 0x6A, 0x41, + 0x71, 0x63, 0x32, 0x4B, 0x2B, 0x53, 0x5A, 0x46, 0x68, 0x79, 0x42, 0x48, 0x2B, 0x0A, 0x44, 0x67, 0x4D, 0x71, 0x39, 0x32, 0x6F, 0x67, 0x33, 0x41, 0x49, 0x56, 0x44, 0x56, 0x34, 0x56, 0x61, 0x76, 0x7A, 0x6A, 0x67, 0x73, 0x47, 0x31, 0x78, 0x5A, + 0x31, 0x6B, 0x43, 0x57, 0x79, 0x6A, 0x57, 0x5A, 0x67, 0x48, 0x4A, 0x38, 0x63, 0x62, 0x6C, 0x69, 0x74, 0x68, 0x64, 0x48, 0x46, 0x73, 0x51, 0x2F, 0x48, 0x33, 0x4E, 0x59, 0x6B, 0x51, 0x34, 0x4A, 0x37, 0x73, 0x56, 0x61, 0x45, 0x33, 0x49, 0x71, + 0x4B, 0x48, 0x42, 0x41, 0x55, 0x73, 0x52, 0x33, 0x32, 0x30, 0x0A, 0x48, 0x4C, 0x6C, 0x69, 0x4B, 0x57, 0x59, 0x6F, 0x79, 0x72, 0x66, 0x68, 0x6B, 0x2F, 0x57, 0x6B, 0x6C, 0x41, 0x4F, 0x5A, 0x75, 0x58, 0x43, 0x46, 0x74, 0x65, 0x5A, 0x49, 0x36, + 0x6F, 0x31, 0x51, 0x2F, 0x4E, 0x6E, 0x65, 0x7A, 0x47, 0x38, 0x48, 0x44, 0x74, 0x30, 0x4C, 0x63, 0x70, 0x32, 0x41, 0x4D, 0x42, 0x59, 0x48, 0x6C, 0x54, 0x38, 0x6F, 0x44, 0x76, 0x33, 0x46, 0x64, 0x55, 0x39, 0x54, 0x31, 0x6E, 0x53, 0x61, 0x74, + 0x43, 0x51, 0x75, 0x6A, 0x67, 0x4B, 0x52, 0x0A, 0x7A, 0x33, 0x62, 0x46, 0x6D, 0x78, 0x35, 0x56, 0x64, 0x4A, 0x78, 0x34, 0x49, 0x62, 0x48, 0x77, 0x4C, 0x66, 0x45, 0x4C, 0x6E, 0x38, 0x4C, 0x56, 0x6C, 0x68, 0x67, 0x66, 0x38, 0x46, 0x51, 0x69, + 0x65, 0x6F, 0x77, 0x48, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x66, 0x54, 0x42, 0x37, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x6E, 0x70, 0x51, 0x61, 0x78, 0x4C, 0x4B, 0x59, 0x4A, + 0x59, 0x4F, 0x37, 0x52, 0x0A, 0x6C, 0x2B, 0x6C, 0x77, 0x72, 0x72, 0x77, 0x37, 0x47, 0x57, 0x7A, 0x62, 0x49, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, + 0x2F, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x4B, 0x65, 0x6C, 0x42, 0x72, 0x45, 0x73, 0x70, 0x67, 0x6C, 0x67, 0x37, 0x74, 0x47, 0x58, 0x36, 0x58, 0x43, 0x75, 0x76, 0x44, 0x73, + 0x5A, 0x0A, 0x62, 0x4E, 0x73, 0x68, 0x4D, 0x42, 0x67, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x41, 0x51, 0x52, 0x4D, 0x41, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4C, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x41, 0x47, 0x77, 0x4C, 0x51, 0x4D, 0x45, 0x41, 0x51, + 0x45, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x0A, 0x43, + 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x41, 0x6D, 0x64, 0x7A, 0x54, 0x62, 0x6C, 0x45, 0x69, 0x47, 0x4B, 0x6B, 0x47, 0x64, 0x4C, 0x44, 0x34, 0x47, 0x6B, 0x47, 0x44, 0x45, 0x6A, 0x4B, 0x77, 0x4C, 0x56, 0x4C, 0x67, 0x66, 0x75, + 0x58, 0x76, 0x54, 0x42, 0x7A, 0x6E, 0x6B, 0x2B, 0x6A, 0x35, 0x37, 0x73, 0x6A, 0x31, 0x4F, 0x37, 0x5A, 0x38, 0x6A, 0x76, 0x5A, 0x66, 0x7A, 0x61, 0x31, 0x7A, 0x76, 0x37, 0x76, 0x31, 0x41, 0x70, 0x74, 0x2B, 0x68, 0x0A, 0x6B, 0x36, 0x45, 0x4B, + 0x68, 0x71, 0x7A, 0x76, 0x49, 0x4E, 0x42, 0x35, 0x41, 0x62, 0x31, 0x34, 0x39, 0x78, 0x6E, 0x59, 0x4A, 0x44, 0x45, 0x30, 0x42, 0x41, 0x47, 0x6D, 0x75, 0x68, 0x57, 0x61, 0x77, 0x79, 0x66, 0x63, 0x32, 0x45, 0x38, 0x50, 0x7A, 0x42, 0x68, 0x6A, + 0x2F, 0x35, 0x6B, 0x50, 0x44, 0x70, 0x46, 0x72, 0x64, 0x52, 0x62, 0x68, 0x49, 0x66, 0x7A, 0x59, 0x4A, 0x73, 0x64, 0x48, 0x74, 0x36, 0x62, 0x50, 0x57, 0x48, 0x4A, 0x78, 0x66, 0x72, 0x72, 0x68, 0x0A, 0x54, 0x5A, 0x56, 0x48, 0x4F, 0x38, 0x6D, + 0x76, 0x62, 0x61, 0x47, 0x30, 0x77, 0x65, 0x79, 0x4A, 0x39, 0x72, 0x51, 0x50, 0x4F, 0x4C, 0x58, 0x69, 0x5A, 0x4E, 0x77, 0x6C, 0x7A, 0x36, 0x62, 0x62, 0x36, 0x35, 0x70, 0x63, 0x6D, 0x61, 0x48, 0x46, 0x43, 0x4E, 0x37, 0x39, 0x35, 0x74, 0x72, + 0x56, 0x31, 0x6C, 0x70, 0x46, 0x44, 0x4D, 0x53, 0x33, 0x77, 0x72, 0x55, 0x55, 0x37, 0x37, 0x51, 0x52, 0x2F, 0x77, 0x34, 0x56, 0x74, 0x66, 0x58, 0x31, 0x32, 0x38, 0x61, 0x39, 0x0A, 0x36, 0x31, 0x71, 0x6E, 0x38, 0x46, 0x59, 0x69, 0x71, 0x54, + 0x78, 0x6C, 0x56, 0x4D, 0x59, 0x56, 0x71, 0x4C, 0x32, 0x47, 0x6E, 0x73, 0x32, 0x44, 0x6C, 0x6D, 0x68, 0x36, 0x63, 0x59, 0x47, 0x4A, 0x34, 0x51, 0x76, 0x68, 0x36, 0x68, 0x45, 0x62, 0x61, 0x41, 0x6A, 0x4D, 0x61, 0x5A, 0x37, 0x73, 0x6E, 0x6B, + 0x47, 0x65, 0x52, 0x44, 0x49, 0x6D, 0x65, 0x75, 0x4B, 0x48, 0x43, 0x6E, 0x45, 0x39, 0x36, 0x2B, 0x52, 0x61, 0x70, 0x4E, 0x4C, 0x62, 0x78, 0x63, 0x33, 0x47, 0x0A, 0x33, 0x6D, 0x42, 0x2F, 0x75, 0x66, 0x4E, 0x50, 0x52, 0x4A, 0x4C, 0x76, 0x4B, + 0x72, 0x63, 0x59, 0x50, 0x71, 0x63, 0x5A, 0x32, 0x51, 0x74, 0x39, 0x73, 0x54, 0x64, 0x42, 0x51, 0x72, 0x43, 0x36, 0x59, 0x42, 0x33, 0x79, 0x2F, 0x67, 0x6B, 0x52, 0x73, 0x50, 0x43, 0x48, 0x65, 0x36, 0x65, 0x64, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x51, 0x75, 0x6F, 0x56, 0x61, 0x64, 0x69, 0x73, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, + 0x31, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x59, 0x44, 0x43, 0x43, 0x41, 0x30, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x65, 0x46, 0x68, 0x66, 0x4C, + 0x71, 0x30, 0x73, 0x47, 0x55, 0x76, 0x6A, 0x4E, 0x77, 0x63, 0x31, 0x4E, 0x42, 0x4D, 0x6F, 0x74, 0x5A, 0x62, 0x55, 0x5A, 0x5A, 0x4D, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, + 0x51, 0x41, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, 0x30, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x46, 0x46, 0x31, + 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x48, 0x6A, 0x41, 0x63, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x56, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, + 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x0A, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x53, 0x42, 0x48, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x78, 0x4E, 0x7A, 0x49, + 0x33, 0x4E, 0x44, 0x52, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x78, 0x4E, 0x7A, 0x49, 0x33, 0x4E, 0x44, 0x52, 0x61, 0x4D, 0x45, 0x67, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, + 0x54, 0x41, 0x6B, 0x4A, 0x4E, 0x0A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, 0x4D, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, + 0x56, 0x6B, 0x4D, 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x56, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, + 0x45, 0x67, 0x0A, 0x52, 0x7A, 0x4D, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, + 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x67, 0x76, 0x6C, 0x41, 0x51, 0x6A, 0x75, 0x6E, 0x79, 0x62, 0x45, 0x43, 0x30, 0x42, 0x4A, 0x79, 0x46, 0x75, 0x54, 0x48, 0x4B, 0x33, 0x43, 0x33, 0x6B, 0x45, 0x61, 0x6B, 0x45, 0x0A, + 0x50, 0x42, 0x74, 0x56, 0x77, 0x65, 0x64, 0x59, 0x4D, 0x42, 0x30, 0x6B, 0x74, 0x4D, 0x50, 0x76, 0x68, 0x64, 0x36, 0x4D, 0x4C, 0x4F, 0x48, 0x42, 0x50, 0x64, 0x2B, 0x43, 0x35, 0x6B, 0x2B, 0x74, 0x52, 0x34, 0x64, 0x73, 0x37, 0x46, 0x74, 0x4A, + 0x77, 0x55, 0x72, 0x56, 0x75, 0x34, 0x2F, 0x73, 0x68, 0x36, 0x78, 0x2F, 0x67, 0x70, 0x71, 0x47, 0x37, 0x44, 0x30, 0x44, 0x6D, 0x56, 0x49, 0x42, 0x30, 0x6A, 0x57, 0x65, 0x72, 0x4E, 0x72, 0x77, 0x55, 0x38, 0x6C, 0x6D, 0x0A, 0x50, 0x4E, 0x53, + 0x73, 0x41, 0x67, 0x48, 0x61, 0x4A, 0x4E, 0x4D, 0x37, 0x71, 0x41, 0x4A, 0x47, 0x72, 0x36, 0x51, 0x63, 0x34, 0x2F, 0x68, 0x7A, 0x57, 0x48, 0x61, 0x33, 0x39, 0x67, 0x36, 0x51, 0x44, 0x62, 0x58, 0x77, 0x7A, 0x38, 0x7A, 0x36, 0x2B, 0x63, 0x5A, + 0x4D, 0x35, 0x63, 0x4F, 0x47, 0x4D, 0x41, 0x71, 0x4E, 0x46, 0x33, 0x34, 0x31, 0x36, 0x38, 0x58, 0x66, 0x75, 0x77, 0x36, 0x63, 0x77, 0x49, 0x32, 0x48, 0x34, 0x34, 0x67, 0x34, 0x68, 0x57, 0x66, 0x36, 0x0A, 0x50, 0x73, 0x65, 0x72, 0x34, 0x42, + 0x4F, 0x63, 0x42, 0x52, 0x69, 0x59, 0x7A, 0x35, 0x50, 0x31, 0x73, 0x5A, 0x4B, 0x30, 0x2F, 0x43, 0x50, 0x54, 0x7A, 0x39, 0x58, 0x45, 0x4A, 0x30, 0x6E, 0x67, 0x6E, 0x6A, 0x79, 0x62, 0x43, 0x4B, 0x4F, 0x4C, 0x58, 0x53, 0x6F, 0x68, 0x34, 0x50, + 0x77, 0x35, 0x71, 0x6C, 0x50, 0x61, 0x66, 0x58, 0x37, 0x50, 0x47, 0x67, 0x6C, 0x54, 0x76, 0x46, 0x30, 0x46, 0x42, 0x4D, 0x2B, 0x68, 0x53, 0x6F, 0x2B, 0x4C, 0x64, 0x6F, 0x49, 0x4E, 0x0A, 0x6F, 0x66, 0x6A, 0x53, 0x78, 0x78, 0x52, 0x33, 0x57, + 0x35, 0x41, 0x32, 0x42, 0x34, 0x47, 0x62, 0x50, 0x67, 0x62, 0x36, 0x55, 0x6C, 0x35, 0x6A, 0x78, 0x61, 0x59, 0x41, 0x2F, 0x71, 0x58, 0x70, 0x55, 0x68, 0x74, 0x53, 0x74, 0x5A, 0x49, 0x35, 0x63, 0x67, 0x4D, 0x4A, 0x59, 0x72, 0x32, 0x77, 0x59, + 0x42, 0x5A, 0x75, 0x70, 0x74, 0x30, 0x6C, 0x77, 0x67, 0x4E, 0x6D, 0x33, 0x66, 0x4D, 0x45, 0x30, 0x55, 0x44, 0x69, 0x54, 0x6F, 0x75, 0x47, 0x39, 0x47, 0x2F, 0x6C, 0x0A, 0x67, 0x36, 0x41, 0x6E, 0x68, 0x46, 0x34, 0x45, 0x77, 0x66, 0x57, 0x51, + 0x76, 0x54, 0x41, 0x39, 0x78, 0x4F, 0x2B, 0x6F, 0x61, 0x62, 0x77, 0x34, 0x6D, 0x36, 0x53, 0x6B, 0x6C, 0x74, 0x46, 0x69, 0x32, 0x6D, 0x6E, 0x41, 0x41, 0x5A, 0x61, 0x75, 0x79, 0x38, 0x52, 0x52, 0x4E, 0x4F, 0x6F, 0x4D, 0x71, 0x76, 0x38, 0x68, + 0x6A, 0x6C, 0x6D, 0x50, 0x53, 0x6C, 0x7A, 0x6B, 0x59, 0x5A, 0x71, 0x6E, 0x30, 0x75, 0x6B, 0x71, 0x65, 0x49, 0x31, 0x52, 0x50, 0x54, 0x6F, 0x56, 0x0A, 0x37, 0x71, 0x4A, 0x5A, 0x6A, 0x71, 0x6C, 0x63, 0x33, 0x73, 0x58, 0x35, 0x6B, 0x43, 0x4C, + 0x6C, 0x69, 0x45, 0x56, 0x78, 0x33, 0x5A, 0x47, 0x5A, 0x62, 0x48, 0x71, 0x66, 0x50, 0x54, 0x32, 0x59, 0x66, 0x46, 0x37, 0x32, 0x76, 0x68, 0x5A, 0x6F, 0x6F, 0x46, 0x36, 0x75, 0x43, 0x79, 0x50, 0x38, 0x57, 0x67, 0x2B, 0x71, 0x49, 0x6E, 0x59, + 0x74, 0x79, 0x61, 0x45, 0x51, 0x48, 0x65, 0x54, 0x54, 0x52, 0x43, 0x4F, 0x51, 0x69, 0x4A, 0x2F, 0x47, 0x4B, 0x75, 0x62, 0x58, 0x0A, 0x39, 0x5A, 0x71, 0x7A, 0x57, 0x42, 0x34, 0x76, 0x4D, 0x49, 0x6B, 0x49, 0x47, 0x31, 0x53, 0x69, 0x74, 0x5A, + 0x67, 0x6A, 0x37, 0x41, 0x68, 0x33, 0x48, 0x4A, 0x56, 0x64, 0x59, 0x64, 0x48, 0x4C, 0x69, 0x5A, 0x78, 0x66, 0x6F, 0x6B, 0x71, 0x52, 0x6D, 0x75, 0x38, 0x68, 0x71, 0x6B, 0x6B, 0x57, 0x43, 0x4B, 0x69, 0x39, 0x59, 0x53, 0x67, 0x78, 0x79, 0x58, + 0x53, 0x74, 0x68, 0x66, 0x62, 0x5A, 0x78, 0x62, 0x47, 0x4C, 0x30, 0x65, 0x55, 0x51, 0x4D, 0x6B, 0x31, 0x66, 0x0A, 0x69, 0x79, 0x41, 0x36, 0x50, 0x45, 0x6B, 0x66, 0x4D, 0x34, 0x56, 0x5A, 0x44, 0x64, 0x76, 0x4C, 0x43, 0x58, 0x56, 0x44, 0x61, + 0x58, 0x50, 0x37, 0x61, 0x33, 0x46, 0x39, 0x38, 0x4E, 0x2F, 0x45, 0x54, 0x48, 0x33, 0x47, 0x6F, 0x79, 0x37, 0x49, 0x6C, 0x58, 0x6E, 0x4C, 0x63, 0x36, 0x4B, 0x4F, 0x54, 0x6B, 0x30, 0x6B, 0x2B, 0x31, 0x37, 0x6B, 0x42, 0x4C, 0x35, 0x79, 0x47, + 0x36, 0x59, 0x6E, 0x4C, 0x55, 0x6C, 0x61, 0x6D, 0x58, 0x72, 0x58, 0x58, 0x41, 0x6B, 0x67, 0x0A, 0x74, 0x33, 0x2B, 0x55, 0x75, 0x55, 0x2F, 0x78, 0x44, 0x52, 0x78, 0x65, 0x69, 0x45, 0x49, 0x62, 0x45, 0x62, 0x66, 0x6E, 0x6B, 0x64, 0x75, 0x65, + 0x62, 0x50, 0x52, 0x71, 0x33, 0x34, 0x77, 0x47, 0x6D, 0x41, 0x4F, 0x74, 0x7A, 0x43, 0x6A, 0x76, 0x70, 0x55, 0x66, 0x7A, 0x55, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, + 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, + 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x6F, 0x35, 0x66, 0x57, 0x38, 0x31, 0x36, 0x69, 0x45, 0x4F, 0x47, 0x72, 0x52, 0x5A, 0x38, 0x38, 0x46, 0x32, 0x51, 0x38, 0x37, 0x67, 0x46, 0x77, 0x6E, 0x4D, 0x77, + 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x42, 0x6A, 0x36, 0x57, 0x33, 0x58, 0x38, 0x50, 0x6E, 0x72, 0x48, 0x58, 0x33, + 0x66, 0x48, 0x79, 0x74, 0x2F, 0x50, 0x58, 0x38, 0x4D, 0x53, 0x78, 0x45, 0x42, 0x64, 0x31, 0x44, 0x4B, 0x71, 0x75, 0x47, 0x72, 0x58, 0x31, 0x52, 0x55, 0x56, 0x52, 0x70, 0x67, 0x6A, 0x70, 0x65, 0x61, 0x51, 0x57, 0x78, 0x69, 0x5A, 0x54, 0x4F, + 0x4F, 0x74, 0x51, 0x71, 0x4F, 0x43, 0x0A, 0x4D, 0x54, 0x61, 0x49, 0x7A, 0x65, 0x6E, 0x37, 0x78, 0x41, 0x53, 0x57, 0x53, 0x49, 0x73, 0x42, 0x78, 0x34, 0x30, 0x42, 0x7A, 0x31, 0x73, 0x7A, 0x42, 0x70, 0x5A, 0x47, 0x5A, 0x6E, 0x51, 0x64, 0x54, + 0x2B, 0x33, 0x42, 0x74, 0x72, 0x6D, 0x30, 0x44, 0x57, 0x48, 0x4D, 0x59, 0x33, 0x37, 0x58, 0x4C, 0x6E, 0x65, 0x4D, 0x6C, 0x68, 0x77, 0x71, 0x49, 0x32, 0x68, 0x72, 0x68, 0x56, 0x64, 0x32, 0x63, 0x44, 0x4D, 0x54, 0x2F, 0x75, 0x46, 0x50, 0x70, + 0x69, 0x4E, 0x33, 0x0A, 0x47, 0x50, 0x6F, 0x61, 0x6A, 0x4F, 0x69, 0x39, 0x5A, 0x63, 0x6E, 0x50, 0x50, 0x2F, 0x54, 0x4A, 0x46, 0x39, 0x7A, 0x72, 0x78, 0x37, 0x7A, 0x41, 0x42, 0x43, 0x34, 0x74, 0x52, 0x69, 0x39, 0x70, 0x5A, 0x73, 0x4D, 0x62, + 0x6A, 0x2F, 0x37, 0x73, 0x50, 0x74, 0x50, 0x4B, 0x6C, 0x4C, 0x39, 0x32, 0x43, 0x69, 0x55, 0x4E, 0x71, 0x58, 0x73, 0x43, 0x48, 0x4B, 0x6E, 0x51, 0x4F, 0x31, 0x38, 0x4C, 0x77, 0x49, 0x45, 0x36, 0x50, 0x57, 0x54, 0x68, 0x76, 0x36, 0x63, 0x74, + 0x0A, 0x54, 0x72, 0x31, 0x4E, 0x78, 0x4E, 0x67, 0x70, 0x78, 0x69, 0x49, 0x59, 0x30, 0x4D, 0x57, 0x73, 0x63, 0x67, 0x4B, 0x43, 0x50, 0x36, 0x6F, 0x36, 0x6F, 0x6A, 0x6F, 0x69, 0x6C, 0x7A, 0x48, 0x64, 0x43, 0x47, 0x50, 0x44, 0x64, 0x52, 0x53, + 0x35, 0x59, 0x43, 0x67, 0x74, 0x57, 0x32, 0x6A, 0x67, 0x46, 0x71, 0x6C, 0x6D, 0x67, 0x69, 0x4E, 0x52, 0x39, 0x65, 0x74, 0x54, 0x32, 0x44, 0x47, 0x62, 0x65, 0x2B, 0x6D, 0x33, 0x6E, 0x55, 0x76, 0x72, 0x69, 0x42, 0x62, 0x50, 0x0A, 0x2B, 0x56, + 0x30, 0x34, 0x69, 0x6B, 0x6B, 0x77, 0x6A, 0x2B, 0x33, 0x78, 0x36, 0x78, 0x6E, 0x30, 0x64, 0x78, 0x6F, 0x78, 0x47, 0x45, 0x31, 0x6E, 0x56, 0x47, 0x77, 0x76, 0x62, 0x32, 0x58, 0x35, 0x32, 0x7A, 0x33, 0x73, 0x49, 0x65, 0x78, 0x65, 0x39, 0x50, + 0x53, 0x4C, 0x79, 0x6D, 0x42, 0x6C, 0x56, 0x4E, 0x46, 0x78, 0x5A, 0x50, 0x54, 0x35, 0x70, 0x71, 0x4F, 0x42, 0x4D, 0x7A, 0x59, 0x7A, 0x63, 0x66, 0x43, 0x6B, 0x65, 0x46, 0x39, 0x4F, 0x72, 0x59, 0x4D, 0x68, 0x0A, 0x33, 0x6A, 0x52, 0x4A, 0x6A, + 0x65, 0x68, 0x5A, 0x72, 0x4A, 0x33, 0x79, 0x64, 0x6C, 0x6F, 0x32, 0x38, 0x68, 0x50, 0x30, 0x72, 0x2B, 0x41, 0x4A, 0x78, 0x32, 0x45, 0x71, 0x62, 0x50, 0x66, 0x67, 0x6E, 0x61, 0x36, 0x37, 0x68, 0x6B, 0x6F, 0x6F, 0x62, 0x79, 0x37, 0x75, 0x74, + 0x48, 0x6E, 0x4E, 0x6B, 0x44, 0x50, 0x44, 0x73, 0x33, 0x62, 0x36, 0x39, 0x66, 0x42, 0x73, 0x6E, 0x51, 0x47, 0x51, 0x2B, 0x70, 0x36, 0x51, 0x39, 0x70, 0x78, 0x79, 0x7A, 0x30, 0x66, 0x61, 0x0A, 0x77, 0x78, 0x2F, 0x6B, 0x4E, 0x53, 0x42, 0x54, + 0x38, 0x6C, 0x54, 0x52, 0x33, 0x32, 0x47, 0x44, 0x70, 0x67, 0x4C, 0x69, 0x4A, 0x54, 0x6A, 0x65, 0x68, 0x54, 0x49, 0x74, 0x58, 0x6E, 0x4F, 0x51, 0x55, 0x6C, 0x31, 0x43, 0x78, 0x4D, 0x34, 0x39, 0x53, 0x2B, 0x48, 0x35, 0x47, 0x59, 0x51, 0x64, + 0x31, 0x61, 0x4A, 0x51, 0x7A, 0x45, 0x48, 0x37, 0x51, 0x52, 0x54, 0x44, 0x76, 0x64, 0x62, 0x4A, 0x57, 0x71, 0x4E, 0x6A, 0x5A, 0x67, 0x4B, 0x41, 0x76, 0x51, 0x55, 0x36, 0x0A, 0x4F, 0x30, 0x65, 0x63, 0x37, 0x41, 0x41, 0x6D, 0x54, 0x50, 0x57, + 0x49, 0x55, 0x62, 0x2B, 0x6F, 0x49, 0x33, 0x38, 0x59, 0x42, 0x37, 0x41, 0x4C, 0x37, 0x59, 0x73, 0x6D, 0x6F, 0x57, 0x54, 0x54, 0x59, 0x55, 0x72, 0x72, 0x58, 0x4A, 0x2F, 0x65, 0x73, 0x36, 0x39, 0x6E, 0x41, 0x37, 0x4D, 0x66, 0x33, 0x57, 0x31, + 0x64, 0x61, 0x57, 0x68, 0x70, 0x71, 0x31, 0x34, 0x36, 0x37, 0x48, 0x78, 0x70, 0x76, 0x4D, 0x63, 0x37, 0x68, 0x55, 0x36, 0x65, 0x46, 0x62, 0x6D, 0x30, 0x0A, 0x46, 0x55, 0x2F, 0x44, 0x6C, 0x58, 0x70, 0x59, 0x31, 0x38, 0x6C, 0x73, 0x36, 0x57, + 0x79, 0x35, 0x38, 0x79, 0x6C, 0x6A, 0x58, 0x72, 0x51, 0x73, 0x38, 0x43, 0x30, 0x39, 0x37, 0x56, 0x70, 0x6C, 0x34, 0x4B, 0x6C, 0x62, 0x51, 0x4D, 0x4A, 0x49, 0x6D, 0x59, 0x46, 0x74, 0x6E, 0x68, 0x38, 0x47, 0x4B, 0x6A, 0x77, 0x53, 0x74, 0x49, + 0x73, 0x50, 0x6D, 0x36, 0x49, 0x6B, 0x38, 0x4B, 0x61, 0x4E, 0x31, 0x6E, 0x72, 0x67, 0x53, 0x37, 0x5A, 0x6B, 0x6C, 0x6D, 0x4F, 0x56, 0x0A, 0x68, 0x4D, 0x4A, 0x4B, 0x7A, 0x52, 0x77, 0x75, 0x4A, 0x49, 0x63, 0x7A, 0x59, 0x4F, 0x58, 0x44, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x51, 0x75, 0x6F, 0x56, 0x61, 0x64, 0x69, 0x73, 0x20, 0x52, 0x6F, 0x6F, 0x74, + 0x20, 0x43, 0x41, 0x20, 0x32, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, + 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x59, 0x44, 0x43, 0x43, 0x41, 0x30, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x52, + 0x46, 0x63, 0x30, 0x4A, 0x46, 0x75, 0x42, 0x69, 0x5A, 0x73, 0x31, 0x38, 0x73, 0x36, 0x34, 0x4B, 0x7A, 0x74, 0x62, 0x70, 0x79, 0x62, 0x77, 0x64, 0x53, 0x67, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, + 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, 0x30, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, + 0x45, 0x46, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x48, 0x6A, 0x41, 0x63, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x56, 0x46, 0x31, + 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x0A, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x69, 0x42, 0x48, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x54, 0x49, + 0x78, 0x4F, 0x44, 0x55, 0x35, 0x4D, 0x7A, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x78, 0x4F, 0x44, 0x55, 0x35, 0x4D, 0x7A, 0x4A, 0x61, 0x4D, 0x45, 0x67, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4A, 0x4E, 0x0A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, 0x4D, 0x61, 0x57, + 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x56, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, + 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x0A, 0x52, 0x7A, 0x4D, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, + 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x68, 0x72, 0x69, 0x57, 0x79, 0x41, 0x52, 0x6A, 0x63, 0x56, 0x34, 0x67, 0x2F, 0x52, 0x75, 0x76, 0x35, 0x72, 0x2B, 0x4C, 0x72, 0x49, 0x33, 0x48, 0x69, 0x6D, + 0x74, 0x46, 0x68, 0x0A, 0x5A, 0x69, 0x46, 0x66, 0x71, 0x71, 0x38, 0x6E, 0x55, 0x65, 0x56, 0x75, 0x47, 0x78, 0x62, 0x55, 0x4C, 0x58, 0x31, 0x51, 0x73, 0x46, 0x4E, 0x33, 0x76, 0x58, 0x67, 0x36, 0x59, 0x4F, 0x4A, 0x6B, 0x41, 0x70, 0x74, 0x38, + 0x68, 0x70, 0x76, 0x57, 0x47, 0x6F, 0x36, 0x74, 0x2F, 0x78, 0x38, 0x56, 0x66, 0x39, 0x57, 0x56, 0x48, 0x68, 0x4C, 0x4C, 0x35, 0x68, 0x53, 0x45, 0x42, 0x4D, 0x48, 0x66, 0x4E, 0x72, 0x4D, 0x57, 0x6E, 0x34, 0x72, 0x6A, 0x79, 0x64, 0x75, 0x59, + 0x0A, 0x4E, 0x4D, 0x37, 0x59, 0x4D, 0x78, 0x63, 0x6F, 0x52, 0x76, 0x79, 0x6E, 0x79, 0x66, 0x44, 0x53, 0x74, 0x4E, 0x56, 0x4E, 0x43, 0x58, 0x4A, 0x4A, 0x2B, 0x66, 0x4B, 0x48, 0x34, 0x36, 0x6E, 0x61, 0x66, 0x61, 0x46, 0x39, 0x61, 0x37, 0x49, + 0x36, 0x4A, 0x61, 0x6C, 0x74, 0x55, 0x6B, 0x53, 0x73, 0x2B, 0x4C, 0x35, 0x75, 0x2B, 0x39, 0x79, 0x6D, 0x63, 0x35, 0x47, 0x51, 0x59, 0x61, 0x59, 0x44, 0x46, 0x43, 0x44, 0x79, 0x35, 0x34, 0x65, 0x6A, 0x69, 0x4B, 0x32, 0x74, 0x0A, 0x6F, 0x49, + 0x7A, 0x2F, 0x70, 0x67, 0x73, 0x6C, 0x55, 0x69, 0x58, 0x6E, 0x46, 0x67, 0x48, 0x56, 0x79, 0x37, 0x67, 0x31, 0x67, 0x51, 0x79, 0x6A, 0x4F, 0x2F, 0x44, 0x68, 0x34, 0x66, 0x78, 0x61, 0x58, 0x63, 0x36, 0x41, 0x63, 0x57, 0x33, 0x34, 0x53, 0x61, + 0x73, 0x2B, 0x4F, 0x37, 0x71, 0x34, 0x31, 0x34, 0x41, 0x42, 0x2B, 0x36, 0x58, 0x72, 0x57, 0x37, 0x50, 0x46, 0x58, 0x6D, 0x41, 0x71, 0x4D, 0x61, 0x43, 0x76, 0x4E, 0x2B, 0x67, 0x67, 0x4F, 0x70, 0x2B, 0x6F, 0x0A, 0x4D, 0x69, 0x77, 0x4D, 0x7A, + 0x41, 0x6B, 0x64, 0x30, 0x35, 0x36, 0x4F, 0x58, 0x62, 0x78, 0x4D, 0x6D, 0x4F, 0x37, 0x46, 0x47, 0x6D, 0x68, 0x37, 0x37, 0x46, 0x4F, 0x6D, 0x36, 0x52, 0x51, 0x31, 0x6F, 0x39, 0x2F, 0x4E, 0x67, 0x4A, 0x38, 0x4D, 0x53, 0x50, 0x73, 0x63, 0x39, + 0x50, 0x47, 0x2F, 0x53, 0x72, 0x6A, 0x36, 0x31, 0x59, 0x78, 0x78, 0x53, 0x73, 0x63, 0x66, 0x72, 0x66, 0x35, 0x42, 0x6D, 0x72, 0x4F, 0x44, 0x58, 0x66, 0x4B, 0x45, 0x56, 0x75, 0x2B, 0x6C, 0x0A, 0x56, 0x30, 0x50, 0x4F, 0x4B, 0x61, 0x32, 0x4D, + 0x71, 0x31, 0x57, 0x2F, 0x78, 0x50, 0x74, 0x62, 0x41, 0x64, 0x30, 0x6A, 0x49, 0x61, 0x46, 0x59, 0x41, 0x49, 0x37, 0x44, 0x30, 0x47, 0x6F, 0x54, 0x37, 0x52, 0x50, 0x6A, 0x45, 0x69, 0x75, 0x41, 0x33, 0x47, 0x66, 0x6D, 0x6C, 0x62, 0x4C, 0x4E, + 0x48, 0x69, 0x4A, 0x75, 0x4B, 0x76, 0x68, 0x42, 0x31, 0x50, 0x4C, 0x4B, 0x46, 0x41, 0x65, 0x4E, 0x69, 0x6C, 0x55, 0x53, 0x78, 0x6D, 0x6E, 0x31, 0x75, 0x49, 0x5A, 0x6F, 0x0A, 0x4C, 0x31, 0x4E, 0x65, 0x73, 0x4E, 0x4B, 0x71, 0x49, 0x63, 0x47, + 0x59, 0x35, 0x6A, 0x44, 0x6A, 0x5A, 0x31, 0x58, 0x48, 0x6D, 0x32, 0x36, 0x73, 0x47, 0x61, 0x68, 0x56, 0x70, 0x6B, 0x55, 0x47, 0x30, 0x43, 0x4D, 0x36, 0x32, 0x2B, 0x74, 0x6C, 0x58, 0x53, 0x6F, 0x52, 0x45, 0x66, 0x41, 0x37, 0x54, 0x38, 0x70, + 0x74, 0x39, 0x44, 0x54, 0x45, 0x63, 0x65, 0x54, 0x2F, 0x41, 0x46, 0x72, 0x32, 0x58, 0x4B, 0x34, 0x6A, 0x59, 0x49, 0x56, 0x7A, 0x38, 0x65, 0x51, 0x51, 0x0A, 0x73, 0x53, 0x57, 0x75, 0x31, 0x5A, 0x4B, 0x37, 0x45, 0x38, 0x45, 0x4D, 0x34, 0x44, + 0x6E, 0x61, 0x74, 0x44, 0x6C, 0x58, 0x74, 0x61, 0x73, 0x31, 0x71, 0x6E, 0x49, 0x68, 0x4F, 0x34, 0x4D, 0x31, 0x35, 0x7A, 0x48, 0x66, 0x65, 0x69, 0x46, 0x75, 0x75, 0x44, 0x49, 0x49, 0x66, 0x52, 0x30, 0x79, 0x6B, 0x52, 0x56, 0x4B, 0x59, 0x6E, + 0x4C, 0x50, 0x34, 0x33, 0x65, 0x68, 0x76, 0x4E, 0x55, 0x52, 0x47, 0x33, 0x59, 0x42, 0x5A, 0x77, 0x6A, 0x67, 0x51, 0x51, 0x76, 0x44, 0x0A, 0x36, 0x78, 0x56, 0x75, 0x2B, 0x4B, 0x51, 0x5A, 0x32, 0x61, 0x4B, 0x72, 0x72, 0x2B, 0x49, 0x6E, 0x55, + 0x6C, 0x59, 0x72, 0x41, 0x6F, 0x6F, 0x73, 0x46, 0x43, 0x54, 0x35, 0x76, 0x30, 0x49, 0x43, 0x76, 0x79, 0x62, 0x49, 0x78, 0x6F, 0x2F, 0x67, 0x62, 0x6A, 0x68, 0x39, 0x55, 0x79, 0x33, 0x6C, 0x37, 0x5A, 0x69, 0x7A, 0x6C, 0x57, 0x4E, 0x6F, 0x66, + 0x2F, 0x6B, 0x31, 0x39, 0x4E, 0x2B, 0x49, 0x78, 0x57, 0x41, 0x31, 0x6B, 0x73, 0x42, 0x38, 0x61, 0x52, 0x78, 0x68, 0x0A, 0x6C, 0x52, 0x62, 0x51, 0x36, 0x39, 0x34, 0x4C, 0x72, 0x7A, 0x34, 0x45, 0x45, 0x45, 0x56, 0x6C, 0x57, 0x46, 0x41, 0x34, + 0x72, 0x30, 0x6A, 0x79, 0x57, 0x62, 0x59, 0x57, 0x38, 0x6A, 0x77, 0x4E, 0x6B, 0x41, 0x4C, 0x47, 0x63, 0x43, 0x34, 0x42, 0x72, 0x54, 0x77, 0x56, 0x31, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, + 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, + 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x37, 0x65, 0x64, 0x76, 0x64, 0x6C, 0x71, 0x2F, 0x59, 0x4F, 0x78, 0x4A, 0x57, 0x38, 0x61, 0x6C, 0x64, 0x37, 0x74, 0x79, 0x46, 0x6E, 0x47, + 0x62, 0x78, 0x44, 0x30, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4A, 0x48, 0x66, 0x67, 0x44, 0x39, 0x44, 0x43, 0x58, + 0x35, 0x78, 0x77, 0x76, 0x66, 0x72, 0x73, 0x34, 0x69, 0x50, 0x34, 0x56, 0x47, 0x79, 0x76, 0x44, 0x31, 0x31, 0x2B, 0x53, 0x68, 0x64, 0x79, 0x4C, 0x79, 0x5A, 0x6D, 0x33, 0x74, 0x64, 0x71, 0x75, 0x58, 0x4B, 0x34, 0x51, 0x72, 0x33, 0x36, 0x4C, + 0x4C, 0x54, 0x6E, 0x39, 0x31, 0x6E, 0x4D, 0x58, 0x36, 0x36, 0x0A, 0x41, 0x61, 0x72, 0x48, 0x61, 0x6B, 0x45, 0x37, 0x6B, 0x4E, 0x51, 0x49, 0x58, 0x4C, 0x4A, 0x67, 0x61, 0x70, 0x44, 0x77, 0x79, 0x4D, 0x34, 0x44, 0x59, 0x76, 0x6D, 0x4C, 0x37, + 0x66, 0x74, 0x75, 0x4B, 0x74, 0x77, 0x47, 0x54, 0x54, 0x77, 0x70, 0x44, 0x34, 0x6B, 0x57, 0x69, 0x6C, 0x68, 0x4D, 0x53, 0x41, 0x2F, 0x6F, 0x68, 0x47, 0x48, 0x71, 0x50, 0x48, 0x4B, 0x6D, 0x64, 0x2B, 0x52, 0x43, 0x72, 0x6F, 0x69, 0x6A, 0x51, + 0x31, 0x68, 0x35, 0x66, 0x71, 0x37, 0x4B, 0x0A, 0x70, 0x56, 0x4D, 0x4E, 0x71, 0x54, 0x31, 0x77, 0x76, 0x53, 0x41, 0x5A, 0x59, 0x61, 0x52, 0x73, 0x4F, 0x50, 0x78, 0x44, 0x4D, 0x75, 0x48, 0x42, 0x52, 0x2F, 0x2F, 0x34, 0x37, 0x50, 0x45, 0x52, + 0x49, 0x6A, 0x4B, 0x57, 0x6E, 0x4D, 0x4C, 0x32, 0x57, 0x32, 0x6D, 0x57, 0x65, 0x79, 0x41, 0x4D, 0x51, 0x30, 0x47, 0x61, 0x57, 0x2F, 0x5A, 0x5A, 0x47, 0x59, 0x6A, 0x65, 0x56, 0x59, 0x67, 0x33, 0x55, 0x51, 0x74, 0x34, 0x58, 0x41, 0x6F, 0x65, + 0x6F, 0x30, 0x4C, 0x39, 0x0A, 0x78, 0x35, 0x32, 0x49, 0x44, 0x38, 0x44, 0x79, 0x65, 0x41, 0x49, 0x6B, 0x56, 0x4A, 0x4F, 0x76, 0x69, 0x59, 0x65, 0x49, 0x79, 0x55, 0x71, 0x41, 0x48, 0x65, 0x72, 0x51, 0x62, 0x6A, 0x35, 0x68, 0x4C, 0x6A, 0x61, + 0x37, 0x4E, 0x51, 0x34, 0x6E, 0x6C, 0x76, 0x31, 0x6D, 0x4E, 0x44, 0x74, 0x68, 0x63, 0x6E, 0x50, 0x78, 0x46, 0x6C, 0x78, 0x48, 0x42, 0x6C, 0x52, 0x4A, 0x41, 0x48, 0x70, 0x59, 0x45, 0x72, 0x41, 0x4B, 0x37, 0x34, 0x58, 0x39, 0x73, 0x62, 0x67, + 0x7A, 0x0A, 0x64, 0x57, 0x71, 0x54, 0x48, 0x42, 0x4C, 0x6D, 0x59, 0x46, 0x35, 0x76, 0x48, 0x58, 0x2F, 0x4A, 0x48, 0x79, 0x50, 0x4C, 0x68, 0x47, 0x47, 0x66, 0x48, 0x6F, 0x4A, 0x45, 0x2B, 0x56, 0x2B, 0x74, 0x59, 0x6C, 0x55, 0x6B, 0x6D, 0x6C, + 0x4B, 0x59, 0x37, 0x56, 0x48, 0x6E, 0x6F, 0x58, 0x36, 0x58, 0x4F, 0x75, 0x59, 0x76, 0x48, 0x78, 0x48, 0x61, 0x55, 0x34, 0x41, 0x73, 0x68, 0x5A, 0x36, 0x72, 0x4E, 0x52, 0x44, 0x62, 0x49, 0x6C, 0x39, 0x71, 0x78, 0x56, 0x36, 0x58, 0x0A, 0x55, + 0x2F, 0x49, 0x79, 0x41, 0x67, 0x6B, 0x77, 0x6F, 0x31, 0x6A, 0x77, 0x44, 0x51, 0x48, 0x56, 0x63, 0x73, 0x61, 0x78, 0x66, 0x47, 0x6C, 0x37, 0x77, 0x2F, 0x55, 0x32, 0x52, 0x63, 0x78, 0x68, 0x62, 0x6C, 0x35, 0x4D, 0x6C, 0x4D, 0x56, 0x65, 0x72, + 0x75, 0x67, 0x4F, 0x58, 0x6F, 0x75, 0x2F, 0x39, 0x38, 0x33, 0x67, 0x37, 0x61, 0x45, 0x4F, 0x47, 0x7A, 0x50, 0x75, 0x56, 0x42, 0x6A, 0x2B, 0x44, 0x37, 0x37, 0x76, 0x66, 0x6F, 0x52, 0x72, 0x51, 0x2B, 0x4E, 0x77, 0x0A, 0x6D, 0x4E, 0x74, 0x64, + 0x64, 0x62, 0x49, 0x4E, 0x57, 0x51, 0x65, 0x46, 0x46, 0x53, 0x4D, 0x35, 0x31, 0x76, 0x48, 0x66, 0x71, 0x53, 0x59, 0x50, 0x31, 0x6B, 0x6A, 0x48, 0x73, 0x36, 0x59, 0x69, 0x39, 0x54, 0x4D, 0x33, 0x57, 0x70, 0x56, 0x48, 0x6E, 0x33, 0x75, 0x36, + 0x47, 0x42, 0x56, 0x76, 0x2F, 0x39, 0x59, 0x55, 0x5A, 0x49, 0x4E, 0x4A, 0x30, 0x67, 0x70, 0x6E, 0x49, 0x64, 0x73, 0x50, 0x4E, 0x57, 0x4E, 0x67, 0x4B, 0x43, 0x4C, 0x6A, 0x73, 0x5A, 0x57, 0x44, 0x0A, 0x7A, 0x59, 0x57, 0x6D, 0x33, 0x53, 0x38, + 0x50, 0x35, 0x32, 0x64, 0x53, 0x62, 0x72, 0x73, 0x76, 0x68, 0x58, 0x7A, 0x31, 0x53, 0x6E, 0x50, 0x6E, 0x78, 0x54, 0x37, 0x41, 0x76, 0x53, 0x45, 0x53, 0x42, 0x54, 0x2F, 0x38, 0x74, 0x77, 0x4E, 0x4A, 0x41, 0x6C, 0x76, 0x49, 0x4A, 0x65, 0x62, + 0x69, 0x56, 0x44, 0x6A, 0x31, 0x65, 0x59, 0x65, 0x4D, 0x48, 0x56, 0x4F, 0x79, 0x54, 0x6F, 0x56, 0x37, 0x42, 0x6A, 0x6A, 0x48, 0x4C, 0x50, 0x6A, 0x34, 0x73, 0x48, 0x4B, 0x4E, 0x0A, 0x4A, 0x65, 0x56, 0x33, 0x55, 0x76, 0x51, 0x44, 0x48, 0x45, + 0x69, 0x6D, 0x55, 0x46, 0x2B, 0x49, 0x49, 0x44, 0x42, 0x75, 0x38, 0x6F, 0x4A, 0x44, 0x71, 0x7A, 0x32, 0x58, 0x68, 0x4F, 0x64, 0x54, 0x2B, 0x79, 0x48, 0x42, 0x54, 0x77, 0x38, 0x69, 0x6D, 0x6F, 0x61, 0x34, 0x57, 0x53, 0x72, 0x32, 0x52, 0x7A, + 0x30, 0x5A, 0x69, 0x43, 0x33, 0x6F, 0x68, 0x65, 0x47, 0x65, 0x37, 0x49, 0x55, 0x49, 0x61, 0x72, 0x46, 0x73, 0x4E, 0x4D, 0x6B, 0x64, 0x37, 0x45, 0x67, 0x72, 0x0A, 0x4F, 0x33, 0x6A, 0x74, 0x5A, 0x73, 0x53, 0x4F, 0x65, 0x57, 0x6D, 0x44, 0x33, + 0x6E, 0x2B, 0x4D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x51, 0x75, 0x6F, 0x56, 0x61, 0x64, 0x69, 0x73, 0x20, + 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x33, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x59, 0x44, 0x43, 0x43, 0x41, 0x30, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, + 0x67, 0x49, 0x55, 0x4C, 0x76, 0x57, 0x62, 0x41, 0x69, 0x69, 0x6E, 0x32, 0x33, 0x72, 0x2F, 0x31, 0x61, 0x4F, 0x70, 0x37, 0x72, 0x30, 0x44, 0x6F, 0x4D, 0x38, 0x53, 0x61, 0x68, 0x30, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, + 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, 0x30, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x6F, 0x54, 0x45, 0x46, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x48, 0x6A, 0x41, 0x63, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, + 0x46, 0x56, 0x46, 0x31, 0x62, 0x31, 0x5A, 0x68, 0x5A, 0x47, 0x6C, 0x7A, 0x49, 0x46, 0x4A, 0x76, 0x0A, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x79, 0x42, 0x48, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x6A, 0x41, + 0x78, 0x4D, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x49, 0x32, 0x4D, 0x7A, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x49, 0x32, 0x4D, 0x7A, 0x4A, 0x61, 0x4D, 0x45, 0x67, 0x78, 0x43, 0x7A, 0x41, + 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4A, 0x4E, 0x0A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, + 0x42, 0x4D, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x56, 0x52, 0x64, 0x57, 0x39, 0x57, 0x59, 0x57, 0x52, 0x70, 0x63, 0x79, 0x42, 0x53, 0x62, 0x32, + 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x4D, 0x67, 0x0A, 0x52, 0x7A, 0x4D, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, + 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x7A, 0x79, 0x77, 0x34, 0x51, 0x5A, 0x34, 0x37, 0x71, 0x46, 0x4A, 0x65, 0x6E, 0x4D, 0x69, 0x6F, 0x4B, 0x56, 0x6A, 0x5A, 0x2F, 0x61, + 0x45, 0x7A, 0x48, 0x73, 0x32, 0x38, 0x36, 0x0A, 0x49, 0x78, 0x53, 0x52, 0x2F, 0x78, 0x6C, 0x2F, 0x70, 0x63, 0x71, 0x73, 0x37, 0x72, 0x4E, 0x32, 0x6E, 0x58, 0x72, 0x70, 0x69, 0x78, 0x75, 0x72, 0x61, 0x7A, 0x48, 0x62, 0x2B, 0x67, 0x74, 0x54, + 0x54, 0x4B, 0x2F, 0x46, 0x70, 0x52, 0x70, 0x35, 0x50, 0x49, 0x70, 0x4D, 0x2F, 0x36, 0x7A, 0x66, 0x4A, 0x64, 0x35, 0x4F, 0x32, 0x59, 0x49, 0x79, 0x43, 0x30, 0x54, 0x65, 0x79, 0x74, 0x75, 0x4D, 0x72, 0x4B, 0x4E, 0x75, 0x46, 0x6F, 0x4D, 0x37, + 0x70, 0x6D, 0x52, 0x4C, 0x0A, 0x4D, 0x6F, 0x6E, 0x37, 0x46, 0x68, 0x59, 0x34, 0x66, 0x75, 0x74, 0x44, 0x34, 0x74, 0x4E, 0x30, 0x53, 0x73, 0x4A, 0x69, 0x43, 0x6E, 0x4D, 0x4B, 0x33, 0x55, 0x6D, 0x7A, 0x56, 0x39, 0x4B, 0x77, 0x43, 0x6F, 0x57, + 0x64, 0x63, 0x54, 0x7A, 0x65, 0x6F, 0x38, 0x76, 0x41, 0x4D, 0x76, 0x4D, 0x42, 0x4F, 0x53, 0x42, 0x44, 0x47, 0x7A, 0x58, 0x52, 0x55, 0x37, 0x4F, 0x78, 0x37, 0x73, 0x57, 0x54, 0x61, 0x59, 0x49, 0x2B, 0x46, 0x72, 0x55, 0x6F, 0x52, 0x71, 0x48, + 0x65, 0x0A, 0x36, 0x6F, 0x6B, 0x4A, 0x37, 0x55, 0x4F, 0x34, 0x42, 0x55, 0x61, 0x4B, 0x68, 0x76, 0x56, 0x5A, 0x52, 0x37, 0x34, 0x62, 0x62, 0x77, 0x45, 0x68, 0x45, 0x4C, 0x6E, 0x39, 0x71, 0x64, 0x49, 0x6F, 0x79, 0x68, 0x41, 0x35, 0x43, 0x63, + 0x6F, 0x54, 0x4E, 0x73, 0x2B, 0x63, 0x72, 0x61, 0x31, 0x41, 0x64, 0x48, 0x6B, 0x72, 0x41, 0x6A, 0x38, 0x30, 0x2F, 0x2F, 0x6F, 0x67, 0x61, 0x58, 0x33, 0x54, 0x37, 0x6D, 0x48, 0x31, 0x75, 0x72, 0x50, 0x6E, 0x4D, 0x4E, 0x41, 0x33, 0x0A, 0x49, + 0x34, 0x5A, 0x79, 0x59, 0x55, 0x55, 0x70, 0x53, 0x46, 0x6C, 0x6F, 0x62, 0x33, 0x65, 0x6D, 0x4C, 0x6F, 0x47, 0x2B, 0x42, 0x30, 0x31, 0x76, 0x72, 0x38, 0x37, 0x45, 0x52, 0x52, 0x4F, 0x52, 0x46, 0x48, 0x41, 0x47, 0x6A, 0x78, 0x2B, 0x66, 0x2B, + 0x49, 0x64, 0x70, 0x73, 0x51, 0x37, 0x76, 0x77, 0x34, 0x6B, 0x5A, 0x36, 0x2B, 0x6F, 0x63, 0x59, 0x66, 0x78, 0x36, 0x62, 0x49, 0x72, 0x63, 0x31, 0x67, 0x4D, 0x4C, 0x6E, 0x69, 0x61, 0x36, 0x45, 0x74, 0x33, 0x55, 0x0A, 0x56, 0x44, 0x6D, 0x72, + 0x4A, 0x71, 0x4D, 0x7A, 0x36, 0x6E, 0x57, 0x42, 0x32, 0x69, 0x33, 0x4E, 0x44, 0x30, 0x2F, 0x6B, 0x41, 0x39, 0x48, 0x76, 0x46, 0x5A, 0x63, 0x62, 0x61, 0x35, 0x44, 0x46, 0x41, 0x70, 0x43, 0x54, 0x5A, 0x67, 0x49, 0x68, 0x73, 0x55, 0x66, 0x65, + 0x69, 0x35, 0x70, 0x4B, 0x67, 0x4C, 0x6C, 0x56, 0x6A, 0x37, 0x57, 0x69, 0x4C, 0x38, 0x44, 0x57, 0x4D, 0x32, 0x66, 0x61, 0x66, 0x73, 0x53, 0x6E, 0x74, 0x41, 0x52, 0x45, 0x36, 0x30, 0x66, 0x37, 0x0A, 0x35, 0x6C, 0x69, 0x35, 0x39, 0x77, 0x7A, + 0x77, 0x65, 0x79, 0x75, 0x78, 0x77, 0x48, 0x41, 0x70, 0x77, 0x30, 0x42, 0x69, 0x4C, 0x54, 0x74, 0x49, 0x61, 0x64, 0x77, 0x6A, 0x50, 0x45, 0x6A, 0x72, 0x65, 0x77, 0x6C, 0x35, 0x71, 0x57, 0x33, 0x61, 0x71, 0x44, 0x43, 0x59, 0x7A, 0x34, 0x42, + 0x79, 0x41, 0x34, 0x69, 0x6D, 0x57, 0x30, 0x61, 0x75, 0x63, 0x6E, 0x6C, 0x38, 0x43, 0x41, 0x4D, 0x68, 0x5A, 0x61, 0x36, 0x33, 0x34, 0x52, 0x79, 0x6C, 0x73, 0x53, 0x71, 0x69, 0x0A, 0x4D, 0x64, 0x35, 0x6D, 0x42, 0x50, 0x66, 0x41, 0x64, 0x4F, + 0x68, 0x78, 0x33, 0x76, 0x38, 0x39, 0x57, 0x63, 0x79, 0x57, 0x4A, 0x68, 0x4B, 0x4C, 0x68, 0x5A, 0x56, 0x58, 0x47, 0x71, 0x74, 0x72, 0x64, 0x51, 0x74, 0x45, 0x50, 0x52, 0x45, 0x6F, 0x50, 0x48, 0x74, 0x68, 0x74, 0x2B, 0x4B, 0x50, 0x5A, 0x30, + 0x2F, 0x6C, 0x37, 0x44, 0x78, 0x4D, 0x59, 0x49, 0x42, 0x70, 0x56, 0x7A, 0x67, 0x65, 0x41, 0x56, 0x75, 0x4E, 0x56, 0x65, 0x6A, 0x48, 0x33, 0x38, 0x44, 0x4D, 0x0A, 0x64, 0x79, 0x4D, 0x30, 0x53, 0x58, 0x56, 0x38, 0x39, 0x70, 0x67, 0x52, 0x36, + 0x79, 0x33, 0x65, 0x37, 0x55, 0x45, 0x75, 0x46, 0x41, 0x55, 0x43, 0x66, 0x2B, 0x44, 0x2B, 0x49, 0x4F, 0x73, 0x31, 0x35, 0x78, 0x47, 0x73, 0x49, 0x73, 0x35, 0x58, 0x50, 0x64, 0x37, 0x4A, 0x4D, 0x47, 0x30, 0x51, 0x41, 0x34, 0x58, 0x4E, 0x38, + 0x66, 0x2B, 0x4D, 0x46, 0x72, 0x58, 0x42, 0x73, 0x6A, 0x36, 0x49, 0x62, 0x47, 0x42, 0x2F, 0x6B, 0x45, 0x2B, 0x56, 0x39, 0x2F, 0x59, 0x74, 0x0A, 0x72, 0x51, 0x45, 0x35, 0x42, 0x77, 0x54, 0x36, 0x64, 0x59, 0x42, 0x39, 0x76, 0x30, 0x6C, 0x51, + 0x37, 0x65, 0x2F, 0x4A, 0x78, 0x48, 0x77, 0x63, 0x36, 0x34, 0x42, 0x2B, 0x32, 0x37, 0x62, 0x51, 0x33, 0x52, 0x50, 0x2B, 0x79, 0x64, 0x4F, 0x63, 0x31, 0x37, 0x4B, 0x58, 0x71, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, + 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, + 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x78, 0x68, 0x66, 0x51, 0x76, 0x4B, 0x6A, 0x71, 0x41, 0x6B, 0x50, 0x79, 0x47, 0x77, 0x61, 0x5A, 0x58, 0x53, 0x75, + 0x51, 0x49, 0x4C, 0x6E, 0x58, 0x6E, 0x4F, 0x51, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x44, 0x52, 0x68, 0x32, 0x56, + 0x61, 0x31, 0x45, 0x6F, 0x64, 0x56, 0x54, 0x64, 0x32, 0x6A, 0x4E, 0x54, 0x46, 0x47, 0x75, 0x36, 0x51, 0x48, 0x63, 0x72, 0x78, 0x66, 0x59, 0x57, 0x4C, 0x6F, 0x70, 0x66, 0x73, 0x4C, 0x4E, 0x37, 0x45, 0x38, 0x74, 0x72, 0x50, 0x36, 0x4B, 0x5A, + 0x31, 0x2F, 0x41, 0x76, 0x57, 0x6B, 0x79, 0x61, 0x69, 0x54, 0x74, 0x33, 0x70, 0x78, 0x0A, 0x4B, 0x47, 0x6D, 0x50, 0x63, 0x2B, 0x46, 0x53, 0x6B, 0x4E, 0x72, 0x56, 0x76, 0x6A, 0x72, 0x6C, 0x74, 0x33, 0x5A, 0x71, 0x56, 0x6F, 0x41, 0x68, 0x33, + 0x31, 0x33, 0x6D, 0x36, 0x54, 0x71, 0x65, 0x35, 0x54, 0x37, 0x32, 0x6F, 0x6D, 0x6E, 0x48, 0x4B, 0x67, 0x71, 0x77, 0x47, 0x45, 0x66, 0x63, 0x49, 0x48, 0x42, 0x39, 0x55, 0x71, 0x4D, 0x2B, 0x57, 0x58, 0x7A, 0x42, 0x75, 0x73, 0x6E, 0x49, 0x46, + 0x55, 0x42, 0x68, 0x79, 0x6E, 0x4C, 0x57, 0x63, 0x4B, 0x7A, 0x53, 0x0A, 0x74, 0x2F, 0x41, 0x63, 0x35, 0x49, 0x59, 0x70, 0x38, 0x4D, 0x37, 0x76, 0x61, 0x47, 0x50, 0x51, 0x74, 0x53, 0x43, 0x4B, 0x46, 0x57, 0x47, 0x61, 0x66, 0x6F, 0x61, 0x59, + 0x74, 0x4D, 0x6E, 0x43, 0x64, 0x76, 0x76, 0x4D, 0x75, 0x6A, 0x41, 0x57, 0x7A, 0x4B, 0x4E, 0x68, 0x78, 0x6E, 0x51, 0x54, 0x35, 0x57, 0x76, 0x76, 0x6F, 0x78, 0x58, 0x71, 0x41, 0x2F, 0x34, 0x54, 0x69, 0x32, 0x54, 0x6B, 0x30, 0x38, 0x48, 0x53, + 0x36, 0x49, 0x54, 0x37, 0x53, 0x64, 0x45, 0x51, 0x0A, 0x54, 0x58, 0x6C, 0x6D, 0x36, 0x36, 0x72, 0x39, 0x39, 0x49, 0x30, 0x78, 0x48, 0x6E, 0x41, 0x55, 0x72, 0x64, 0x7A, 0x65, 0x5A, 0x78, 0x4E, 0x4D, 0x67, 0x52, 0x56, 0x68, 0x76, 0x4C, 0x66, + 0x5A, 0x6B, 0x58, 0x64, 0x78, 0x47, 0x59, 0x46, 0x67, 0x75, 0x2F, 0x42, 0x59, 0x70, 0x62, 0x57, 0x63, 0x43, 0x2F, 0x65, 0x50, 0x49, 0x6C, 0x55, 0x6E, 0x77, 0x45, 0x73, 0x42, 0x62, 0x54, 0x75, 0x5A, 0x44, 0x64, 0x51, 0x64, 0x6D, 0x32, 0x4E, + 0x6E, 0x4C, 0x39, 0x44, 0x75, 0x0A, 0x44, 0x63, 0x70, 0x6D, 0x76, 0x4A, 0x52, 0x50, 0x70, 0x71, 0x33, 0x74, 0x2F, 0x4F, 0x35, 0x6A, 0x72, 0x46, 0x63, 0x2F, 0x5A, 0x53, 0x58, 0x50, 0x73, 0x6F, 0x61, 0x50, 0x30, 0x41, 0x6A, 0x2F, 0x75, 0x48, + 0x59, 0x55, 0x62, 0x74, 0x37, 0x6C, 0x4A, 0x2B, 0x79, 0x72, 0x65, 0x4C, 0x56, 0x54, 0x75, 0x62, 0x59, 0x2F, 0x36, 0x43, 0x44, 0x35, 0x30, 0x71, 0x69, 0x2B, 0x59, 0x55, 0x62, 0x4B, 0x68, 0x34, 0x79, 0x45, 0x38, 0x2F, 0x6E, 0x78, 0x6F, 0x47, + 0x69, 0x62, 0x0A, 0x49, 0x68, 0x36, 0x42, 0x4A, 0x70, 0x73, 0x51, 0x42, 0x4A, 0x46, 0x78, 0x77, 0x41, 0x59, 0x66, 0x33, 0x4B, 0x44, 0x54, 0x75, 0x56, 0x61, 0x6E, 0x34, 0x35, 0x67, 0x74, 0x66, 0x34, 0x4F, 0x64, 0x33, 0x34, 0x77, 0x72, 0x6E, + 0x44, 0x4B, 0x4F, 0x4D, 0x70, 0x54, 0x77, 0x41, 0x54, 0x77, 0x69, 0x4B, 0x70, 0x39, 0x44, 0x77, 0x69, 0x37, 0x44, 0x6D, 0x44, 0x6B, 0x48, 0x4F, 0x48, 0x76, 0x38, 0x58, 0x67, 0x42, 0x43, 0x48, 0x2F, 0x4D, 0x79, 0x4A, 0x6E, 0x6D, 0x44, 0x0A, + 0x68, 0x50, 0x62, 0x6C, 0x38, 0x4D, 0x46, 0x52, 0x45, 0x73, 0x41, 0x4C, 0x48, 0x67, 0x51, 0x6A, 0x44, 0x46, 0x53, 0x6C, 0x54, 0x43, 0x39, 0x4A, 0x78, 0x55, 0x72, 0x52, 0x74, 0x6D, 0x35, 0x67, 0x44, 0x57, 0x76, 0x38, 0x61, 0x34, 0x75, 0x46, + 0x4A, 0x47, 0x53, 0x33, 0x69, 0x51, 0x36, 0x72, 0x4A, 0x55, 0x64, 0x62, 0x50, 0x4D, 0x39, 0x2B, 0x53, 0x62, 0x33, 0x48, 0x36, 0x51, 0x72, 0x47, 0x32, 0x76, 0x64, 0x2B, 0x44, 0x68, 0x63, 0x49, 0x30, 0x30, 0x69, 0x58, 0x0A, 0x30, 0x48, 0x47, + 0x53, 0x38, 0x41, 0x38, 0x35, 0x50, 0x6A, 0x52, 0x71, 0x48, 0x48, 0x33, 0x59, 0x38, 0x69, 0x4B, 0x75, 0x75, 0x32, 0x6E, 0x30, 0x4D, 0x37, 0x53, 0x6D, 0x53, 0x46, 0x58, 0x52, 0x44, 0x77, 0x34, 0x6D, 0x36, 0x4F, 0x79, 0x32, 0x43, 0x79, 0x32, + 0x6E, 0x68, 0x54, 0x58, 0x4E, 0x2F, 0x56, 0x6E, 0x49, 0x6E, 0x39, 0x48, 0x4E, 0x50, 0x6C, 0x6F, 0x70, 0x4E, 0x4C, 0x6B, 0x39, 0x68, 0x4D, 0x36, 0x78, 0x5A, 0x64, 0x52, 0x5A, 0x6B, 0x5A, 0x46, 0x57, 0x0A, 0x64, 0x53, 0x48, 0x42, 0x64, 0x35, + 0x37, 0x35, 0x65, 0x75, 0x46, 0x67, 0x6E, 0x64, 0x4F, 0x74, 0x42, 0x42, 0x6A, 0x30, 0x66, 0x4F, 0x74, 0x65, 0x6B, 0x34, 0x39, 0x54, 0x53, 0x69, 0x49, 0x70, 0x2B, 0x45, 0x67, 0x72, 0x50, 0x6B, 0x32, 0x47, 0x72, 0x46, 0x74, 0x2F, 0x79, 0x77, + 0x61, 0x5A, 0x57, 0x57, 0x44, 0x59, 0x57, 0x47, 0x57, 0x56, 0x6A, 0x55, 0x54, 0x52, 0x39, 0x33, 0x39, 0x2B, 0x4A, 0x33, 0x39, 0x39, 0x72, 0x6F, 0x44, 0x31, 0x42, 0x30, 0x79, 0x32, 0x0A, 0x50, 0x70, 0x78, 0x78, 0x56, 0x4A, 0x6B, 0x45, 0x53, + 0x2F, 0x31, 0x59, 0x2B, 0x5A, 0x6A, 0x30, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, + 0x65, 0x72, 0x74, 0x20, 0x41, 0x73, 0x73, 0x75, 0x72, 0x65, 0x64, 0x20, 0x49, 0x44, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, + 0x49, 0x49, 0x44, 0x6C, 0x6A, 0x43, 0x43, 0x41, 0x6E, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x35, 0x4D, 0x63, 0x4F, 0x74, 0x59, 0x35, 0x5A, 0x2B, 0x70, 0x6E, 0x49, 0x37, 0x2F, 0x44, 0x72, 0x35, 0x72, 0x30, 0x53, + 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, + 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x51, 0x77, 0x0A, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x44, 0x45, 0x78, 0x74, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x42, 0x63, 0x33, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x6B, 0x49, 0x45, 0x6C, 0x45, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, 0x7A, 0x49, + 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x4D, 0x77, 0x4F, 0x44, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x0A, 0x4D, 0x54, 0x45, 0x31, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, + 0x41, 0x77, 0x57, 0x6A, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, + 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x0A, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, + 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x74, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x42, 0x63, + 0x33, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x6B, 0x49, 0x45, 0x6C, 0x45, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, 0x7A, 0x49, 0x77, 0x0A, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, + 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x44, 0x5A, 0x35, 0x79, 0x67, 0x76, 0x55, 0x6A, 0x38, 0x32, 0x63, 0x6B, 0x6D, 0x49, + 0x6B, 0x7A, 0x54, 0x7A, 0x2B, 0x47, 0x6F, 0x65, 0x4D, 0x56, 0x53, 0x41, 0x6E, 0x36, 0x31, 0x55, 0x51, 0x62, 0x56, 0x48, 0x0A, 0x33, 0x35, 0x61, 0x6F, 0x31, 0x4B, 0x2B, 0x41, 0x4C, 0x62, 0x6B, 0x4B, 0x7A, 0x33, 0x58, 0x39, 0x69, 0x61, 0x56, + 0x39, 0x4A, 0x50, 0x72, 0x6A, 0x49, 0x67, 0x77, 0x72, 0x76, 0x4A, 0x55, 0x58, 0x43, 0x7A, 0x4F, 0x2F, 0x47, 0x55, 0x31, 0x42, 0x42, 0x70, 0x41, 0x41, 0x76, 0x51, 0x78, 0x4E, 0x45, 0x50, 0x34, 0x48, 0x74, 0x65, 0x63, 0x63, 0x62, 0x69, 0x4A, + 0x56, 0x4D, 0x57, 0x57, 0x58, 0x76, 0x64, 0x4D, 0x58, 0x30, 0x68, 0x35, 0x69, 0x38, 0x39, 0x76, 0x71, 0x0A, 0x62, 0x46, 0x43, 0x4D, 0x50, 0x34, 0x51, 0x4D, 0x6C, 0x73, 0x2B, 0x33, 0x79, 0x77, 0x50, 0x67, 0x79, 0x6D, 0x32, 0x68, 0x46, 0x45, + 0x77, 0x62, 0x69, 0x64, 0x33, 0x74, 0x41, 0x4C, 0x42, 0x53, 0x66, 0x4B, 0x2B, 0x52, 0x62, 0x4C, 0x45, 0x34, 0x45, 0x39, 0x48, 0x70, 0x45, 0x67, 0x6A, 0x41, 0x41, 0x4C, 0x41, 0x63, 0x4B, 0x78, 0x48, 0x61, 0x64, 0x33, 0x41, 0x32, 0x6D, 0x36, + 0x37, 0x4F, 0x65, 0x59, 0x66, 0x63, 0x67, 0x6E, 0x44, 0x6D, 0x43, 0x58, 0x52, 0x77, 0x0A, 0x56, 0x57, 0x6D, 0x76, 0x6F, 0x32, 0x69, 0x66, 0x76, 0x39, 0x32, 0x32, 0x65, 0x62, 0x50, 0x79, 0x6E, 0x58, 0x41, 0x70, 0x56, 0x66, 0x53, 0x72, 0x2F, + 0x35, 0x56, 0x68, 0x38, 0x38, 0x6C, 0x41, 0x62, 0x78, 0x33, 0x52, 0x76, 0x70, 0x4F, 0x37, 0x30, 0x34, 0x67, 0x71, 0x75, 0x35, 0x32, 0x2F, 0x63, 0x6C, 0x70, 0x57, 0x63, 0x54, 0x73, 0x2F, 0x31, 0x50, 0x50, 0x52, 0x43, 0x76, 0x34, 0x6F, 0x37, + 0x36, 0x50, 0x75, 0x32, 0x5A, 0x6D, 0x76, 0x41, 0x39, 0x4F, 0x50, 0x0A, 0x59, 0x4C, 0x66, 0x79, 0x6B, 0x71, 0x47, 0x78, 0x76, 0x59, 0x6D, 0x4A, 0x48, 0x7A, 0x44, 0x4E, 0x77, 0x36, 0x59, 0x75, 0x59, 0x6A, 0x4F, 0x75, 0x46, 0x67, 0x4A, 0x33, + 0x52, 0x46, 0x72, 0x6E, 0x67, 0x51, 0x6F, 0x38, 0x70, 0x30, 0x51, 0x75, 0x65, 0x62, 0x67, 0x2F, 0x42, 0x4C, 0x78, 0x63, 0x6F, 0x49, 0x66, 0x68, 0x47, 0x36, 0x39, 0x52, 0x6A, 0x73, 0x33, 0x73, 0x4C, 0x50, 0x72, 0x34, 0x2F, 0x6D, 0x33, 0x77, + 0x4F, 0x6E, 0x79, 0x71, 0x69, 0x2B, 0x52, 0x6E, 0x0A, 0x6C, 0x54, 0x47, 0x4E, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, + 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, + 0x57, 0x42, 0x42, 0x54, 0x4F, 0x0A, 0x77, 0x30, 0x71, 0x35, 0x6D, 0x56, 0x58, 0x79, 0x75, 0x4E, 0x74, 0x67, 0x76, 0x36, 0x6C, 0x2B, 0x76, 0x56, 0x61, 0x31, 0x6C, 0x7A, 0x61, 0x6E, 0x31, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, + 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x45, 0x41, 0x79, 0x71, 0x56, 0x56, 0x6A, 0x4F, 0x50, 0x49, 0x51, 0x57, 0x35, 0x70, 0x4A, 0x36, 0x64, 0x31, 0x45, 0x65, 0x38, 0x38, 0x68, 0x6A, + 0x5A, 0x76, 0x0A, 0x30, 0x70, 0x33, 0x47, 0x65, 0x44, 0x67, 0x64, 0x61, 0x5A, 0x61, 0x69, 0x6B, 0x6D, 0x6B, 0x75, 0x4F, 0x47, 0x79, 0x62, 0x66, 0x51, 0x54, 0x55, 0x69, 0x61, 0x57, 0x78, 0x4D, 0x54, 0x65, 0x4B, 0x79, 0x53, 0x48, 0x4D, 0x71, + 0x32, 0x7A, 0x4E, 0x69, 0x78, 0x79, 0x61, 0x31, 0x72, 0x39, 0x49, 0x30, 0x6A, 0x4A, 0x6D, 0x77, 0x59, 0x72, 0x41, 0x38, 0x79, 0x38, 0x36, 0x37, 0x38, 0x44, 0x6A, 0x31, 0x4A, 0x47, 0x47, 0x30, 0x56, 0x44, 0x6A, 0x41, 0x39, 0x74, 0x7A, 0x0A, + 0x64, 0x32, 0x39, 0x4B, 0x4F, 0x56, 0x50, 0x74, 0x33, 0x69, 0x62, 0x48, 0x74, 0x58, 0x32, 0x76, 0x4B, 0x30, 0x4C, 0x52, 0x64, 0x57, 0x4C, 0x6A, 0x53, 0x69, 0x73, 0x43, 0x78, 0x31, 0x42, 0x4C, 0x34, 0x47, 0x6E, 0x69, 0x6C, 0x6D, 0x77, 0x4F, + 0x52, 0x47, 0x59, 0x51, 0x52, 0x49, 0x2B, 0x74, 0x42, 0x65, 0x76, 0x34, 0x65, 0x61, 0x79, 0x6D, 0x47, 0x2B, 0x67, 0x33, 0x4E, 0x4A, 0x31, 0x54, 0x79, 0x57, 0x47, 0x71, 0x6F, 0x6C, 0x4B, 0x76, 0x53, 0x6E, 0x41, 0x57, 0x0A, 0x68, 0x73, 0x49, + 0x36, 0x79, 0x4C, 0x45, 0x54, 0x63, 0x44, 0x62, 0x59, 0x7A, 0x2B, 0x37, 0x30, 0x43, 0x6A, 0x54, 0x56, 0x57, 0x30, 0x7A, 0x39, 0x42, 0x35, 0x79, 0x69, 0x75, 0x74, 0x6B, 0x42, 0x63, 0x6C, 0x7A, 0x7A, 0x54, 0x63, 0x48, 0x64, 0x44, 0x72, 0x45, + 0x63, 0x44, 0x63, 0x52, 0x6A, 0x76, 0x71, 0x33, 0x30, 0x46, 0x50, 0x75, 0x4A, 0x37, 0x4B, 0x4A, 0x42, 0x44, 0x6B, 0x7A, 0x4D, 0x79, 0x46, 0x64, 0x41, 0x30, 0x47, 0x34, 0x44, 0x71, 0x73, 0x30, 0x4D, 0x0A, 0x6A, 0x6F, 0x6D, 0x5A, 0x6D, 0x57, + 0x7A, 0x77, 0x50, 0x44, 0x43, 0x76, 0x4F, 0x4E, 0x39, 0x76, 0x76, 0x4B, 0x4F, 0x2B, 0x4B, 0x53, 0x41, 0x6E, 0x71, 0x33, 0x54, 0x2F, 0x45, 0x79, 0x4A, 0x34, 0x33, 0x70, 0x64, 0x53, 0x56, 0x52, 0x36, 0x44, 0x74, 0x56, 0x51, 0x67, 0x41, 0x2B, + 0x36, 0x75, 0x77, 0x45, 0x39, 0x57, 0x33, 0x6A, 0x66, 0x4D, 0x77, 0x33, 0x2B, 0x71, 0x42, 0x43, 0x65, 0x37, 0x30, 0x33, 0x65, 0x34, 0x59, 0x74, 0x73, 0x58, 0x66, 0x4A, 0x77, 0x6F, 0x0A, 0x49, 0x68, 0x4E, 0x7A, 0x62, 0x4D, 0x38, 0x6D, 0x39, + 0x59, 0x6F, 0x70, 0x35, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, + 0x65, 0x72, 0x74, 0x20, 0x41, 0x73, 0x73, 0x75, 0x72, 0x65, 0x64, 0x20, 0x49, 0x44, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, + 0x49, 0x49, 0x43, 0x52, 0x6A, 0x43, 0x43, 0x41, 0x63, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x36, 0x46, 0x61, 0x2B, 0x68, 0x33, 0x66, 0x6F, 0x4C, 0x56, 0x4A, 0x52, 0x4B, 0x2F, 0x4E, 0x4A, 0x4B, 0x42, 0x73, 0x37, + 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x56, + 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, + 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x74, + 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x42, 0x63, 0x33, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x6B, 0x49, 0x45, 0x6C, 0x45, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, 0x7A, 0x4D, 0x77, 0x48, 0x68, 0x63, + 0x4E, 0x4D, 0x54, 0x4D, 0x77, 0x4F, 0x44, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x31, 0x0A, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, + 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, + 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x0A, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, + 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x74, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x42, 0x63, 0x33, 0x4E, 0x31, 0x63, + 0x6D, 0x56, 0x6B, 0x49, 0x45, 0x6C, 0x45, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, 0x7A, 0x4D, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x0A, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, + 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x51, 0x5A, 0x35, 0x37, 0x79, 0x73, 0x52, 0x47, 0x58, 0x74, 0x7A, 0x62, 0x67, 0x2F, 0x57, 0x50, 0x75, 0x4E, 0x73, 0x56, 0x65, 0x70, 0x52, 0x43, 0x30, 0x46, 0x46, 0x66, 0x4C, 0x76, + 0x43, 0x2F, 0x38, 0x51, 0x64, 0x4A, 0x2B, 0x31, 0x59, 0x6C, 0x4A, 0x66, 0x5A, 0x6E, 0x34, 0x66, 0x35, 0x64, 0x77, 0x62, 0x0A, 0x52, 0x58, 0x6B, 0x4C, 0x7A, 0x4D, 0x5A, 0x54, 0x43, 0x70, 0x32, 0x4E, 0x58, 0x51, 0x4C, 0x5A, 0x71, 0x56, 0x6E, + 0x65, 0x41, 0x6C, 0x72, 0x32, 0x6C, 0x53, 0x6F, 0x4F, 0x6A, 0x54, 0x68, 0x4B, 0x69, 0x6B, 0x6E, 0x47, 0x76, 0x4D, 0x59, 0x44, 0x4F, 0x41, 0x64, 0x66, 0x56, 0x64, 0x70, 0x2B, 0x43, 0x57, 0x37, 0x69, 0x66, 0x31, 0x37, 0x51, 0x52, 0x53, 0x41, + 0x50, 0x57, 0x58, 0x59, 0x51, 0x31, 0x71, 0x41, 0x6B, 0x38, 0x43, 0x33, 0x65, 0x4E, 0x76, 0x4A, 0x73, 0x0A, 0x4B, 0x54, 0x6D, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, + 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, + 0x51, 0x57, 0x42, 0x42, 0x54, 0x4C, 0x30, 0x4C, 0x32, 0x70, 0x34, 0x5A, 0x67, 0x46, 0x0A, 0x55, 0x61, 0x46, 0x4E, 0x4E, 0x36, 0x4B, 0x44, 0x65, 0x63, 0x36, 0x4E, 0x48, 0x53, 0x72, 0x6B, 0x68, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, + 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6E, 0x41, 0x44, 0x42, 0x6B, 0x41, 0x6A, 0x41, 0x6C, 0x70, 0x49, 0x46, 0x46, 0x41, 0x6D, 0x73, 0x53, 0x53, 0x33, 0x56, 0x30, 0x54, 0x38, 0x67, 0x6A, 0x34, 0x33, 0x44, 0x79, 0x64, + 0x58, 0x4C, 0x65, 0x66, 0x49, 0x6E, 0x77, 0x7A, 0x35, 0x46, 0x79, 0x0A, 0x59, 0x5A, 0x35, 0x65, 0x45, 0x4A, 0x4A, 0x5A, 0x56, 0x72, 0x6D, 0x44, 0x78, 0x78, 0x44, 0x6E, 0x4F, 0x4F, 0x6C, 0x59, 0x4A, 0x6A, 0x5A, 0x39, 0x31, 0x65, 0x51, 0x30, + 0x68, 0x6A, 0x6B, 0x43, 0x4D, 0x48, 0x77, 0x32, 0x55, 0x2F, 0x41, 0x77, 0x35, 0x57, 0x4A, 0x6A, 0x4F, 0x70, 0x6E, 0x69, 0x74, 0x71, 0x4D, 0x37, 0x6D, 0x7A, 0x54, 0x36, 0x48, 0x74, 0x6F, 0x51, 0x6B, 0x6E, 0x46, 0x65, 0x6B, 0x52, 0x4F, 0x6E, + 0x33, 0x61, 0x52, 0x75, 0x6B, 0x73, 0x77, 0x79, 0x0A, 0x31, 0x76, 0x55, 0x68, 0x5A, 0x73, 0x63, 0x76, 0x36, 0x70, 0x5A, 0x6A, 0x61, 0x6D, 0x56, 0x46, 0x6B, 0x70, 0x55, 0x42, 0x74, 0x41, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, + 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, + 0x74, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, + 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x6A, 0x6A, 0x43, 0x43, 0x41, 0x6E, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x41, 0x7A, 0x72, + 0x78, 0x35, 0x71, 0x63, 0x52, 0x71, 0x61, 0x43, 0x37, 0x4B, 0x47, 0x53, 0x78, 0x48, 0x51, 0x6E, 0x36, 0x35, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, + 0x68, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, + 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, + 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x41, 0x77, 0x0A, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, + 0x57, 0x77, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x4D, 0x6A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x7A, 0x41, 0x34, 0x4D, 0x44, 0x45, 0x78, 0x4D, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4F, + 0x44, 0x41, 0x78, 0x4D, 0x54, 0x55, 0x78, 0x0A, 0x4D, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x47, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x55, 0x77, + 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, + 0x45, 0x48, 0x64, 0x33, 0x0A, 0x64, 0x79, 0x35, 0x6B, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, 0x56, 0x79, 0x64, 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, 0x78, 0x49, 0x44, 0x41, 0x65, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x30, 0x52, + 0x70, 0x5A, 0x32, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x79, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, + 0x71, 0x0A, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x75, 0x7A, 0x66, 0x4E, 0x4E, 0x4E, + 0x78, 0x37, 0x61, 0x38, 0x6D, 0x79, 0x61, 0x4A, 0x43, 0x74, 0x53, 0x6E, 0x58, 0x2F, 0x52, 0x72, 0x6F, 0x68, 0x43, 0x67, 0x69, 0x4E, 0x39, 0x52, 0x6C, 0x55, 0x79, 0x66, 0x75, 0x49, 0x32, 0x2F, 0x4F, 0x75, 0x38, 0x6A, 0x71, 0x4A, 0x0A, 0x6B, + 0x54, 0x78, 0x36, 0x35, 0x71, 0x73, 0x47, 0x47, 0x6D, 0x76, 0x50, 0x72, 0x43, 0x33, 0x6F, 0x58, 0x67, 0x6B, 0x6B, 0x52, 0x4C, 0x70, 0x69, 0x6D, 0x6E, 0x37, 0x57, 0x6F, 0x36, 0x68, 0x2B, 0x34, 0x46, 0x52, 0x31, 0x49, 0x41, 0x57, 0x73, 0x55, + 0x4C, 0x65, 0x63, 0x59, 0x78, 0x70, 0x73, 0x4D, 0x4E, 0x7A, 0x61, 0x48, 0x78, 0x6D, 0x78, 0x31, 0x78, 0x37, 0x65, 0x2F, 0x64, 0x66, 0x67, 0x79, 0x35, 0x53, 0x44, 0x4E, 0x36, 0x37, 0x73, 0x48, 0x30, 0x4E, 0x4F, 0x0A, 0x33, 0x58, 0x73, 0x73, + 0x30, 0x72, 0x30, 0x75, 0x70, 0x53, 0x2F, 0x6B, 0x71, 0x62, 0x69, 0x74, 0x4F, 0x74, 0x53, 0x5A, 0x70, 0x4C, 0x59, 0x6C, 0x36, 0x5A, 0x74, 0x72, 0x41, 0x47, 0x43, 0x53, 0x59, 0x50, 0x39, 0x50, 0x49, 0x55, 0x6B, 0x59, 0x39, 0x32, 0x65, 0x51, + 0x71, 0x32, 0x45, 0x47, 0x6E, 0x49, 0x2F, 0x79, 0x75, 0x75, 0x6D, 0x30, 0x36, 0x5A, 0x49, 0x79, 0x61, 0x37, 0x58, 0x7A, 0x56, 0x2B, 0x68, 0x64, 0x47, 0x38, 0x32, 0x4D, 0x48, 0x61, 0x75, 0x56, 0x0A, 0x42, 0x4A, 0x56, 0x4A, 0x38, 0x7A, 0x55, + 0x74, 0x6C, 0x75, 0x4E, 0x4A, 0x62, 0x64, 0x31, 0x33, 0x34, 0x2F, 0x74, 0x4A, 0x53, 0x37, 0x53, 0x73, 0x56, 0x51, 0x65, 0x70, 0x6A, 0x35, 0x57, 0x7A, 0x74, 0x43, 0x4F, 0x37, 0x54, 0x47, 0x31, 0x46, 0x38, 0x50, 0x61, 0x70, 0x73, 0x70, 0x55, + 0x77, 0x74, 0x50, 0x31, 0x4D, 0x56, 0x59, 0x77, 0x6E, 0x53, 0x6C, 0x63, 0x55, 0x66, 0x49, 0x4B, 0x64, 0x7A, 0x58, 0x4F, 0x53, 0x30, 0x78, 0x5A, 0x4B, 0x42, 0x67, 0x79, 0x4D, 0x0A, 0x55, 0x4E, 0x47, 0x50, 0x48, 0x67, 0x6D, 0x2B, 0x46, 0x36, + 0x48, 0x6D, 0x49, 0x63, 0x72, 0x39, 0x67, 0x2B, 0x55, 0x51, 0x76, 0x49, 0x4F, 0x6C, 0x43, 0x73, 0x52, 0x6E, 0x4B, 0x50, 0x5A, 0x7A, 0x46, 0x42, 0x51, 0x39, 0x52, 0x6E, 0x62, 0x44, 0x68, 0x78, 0x53, 0x4A, 0x49, 0x54, 0x52, 0x4E, 0x72, 0x77, + 0x39, 0x46, 0x44, 0x4B, 0x5A, 0x4A, 0x6F, 0x62, 0x71, 0x37, 0x6E, 0x4D, 0x57, 0x78, 0x4D, 0x34, 0x4D, 0x70, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x0A, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, + 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x64, 0x42, + 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x54, 0x69, 0x4A, 0x55, 0x49, 0x42, 0x69, 0x56, 0x35, 0x75, 0x4E, 0x75, 0x0A, 0x35, 0x67, 0x2F, 0x36, 0x2B, 0x72, 0x6B, 0x53, 0x37, 0x51, 0x59, 0x58, 0x6A, 0x7A, 0x6B, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x47, 0x42, 0x6E, 0x4B, 0x4A, 0x52, 0x76, 0x44, 0x6B, 0x68, 0x6A, 0x36, 0x7A, 0x48, 0x64, + 0x36, 0x6D, 0x63, 0x59, 0x31, 0x59, 0x6C, 0x39, 0x50, 0x4D, 0x57, 0x4C, 0x53, 0x6E, 0x2F, 0x70, 0x76, 0x74, 0x73, 0x72, 0x0A, 0x46, 0x39, 0x2B, 0x77, 0x58, 0x33, 0x4E, 0x33, 0x4B, 0x6A, 0x49, 0x54, 0x4F, 0x59, 0x46, 0x6E, 0x51, 0x6F, 0x51, + 0x6A, 0x38, 0x6B, 0x56, 0x6E, 0x4E, 0x65, 0x79, 0x49, 0x76, 0x2F, 0x69, 0x50, 0x73, 0x47, 0x45, 0x4D, 0x4E, 0x4B, 0x53, 0x75, 0x49, 0x45, 0x79, 0x45, 0x78, 0x74, 0x76, 0x34, 0x4E, 0x65, 0x46, 0x32, 0x32, 0x64, 0x2B, 0x6D, 0x51, 0x72, 0x76, + 0x48, 0x52, 0x41, 0x69, 0x47, 0x66, 0x7A, 0x5A, 0x30, 0x4A, 0x46, 0x72, 0x61, 0x62, 0x41, 0x30, 0x55, 0x0A, 0x57, 0x54, 0x57, 0x39, 0x38, 0x6B, 0x6E, 0x64, 0x74, 0x68, 0x2F, 0x4A, 0x73, 0x77, 0x31, 0x48, 0x4B, 0x6A, 0x32, 0x5A, 0x4C, 0x37, + 0x74, 0x63, 0x75, 0x37, 0x58, 0x55, 0x49, 0x4F, 0x47, 0x5A, 0x58, 0x31, 0x4E, 0x47, 0x46, 0x64, 0x74, 0x6F, 0x6D, 0x2F, 0x44, 0x7A, 0x4D, 0x4E, 0x55, 0x2B, 0x4D, 0x65, 0x4B, 0x4E, 0x68, 0x4A, 0x37, 0x6A, 0x69, 0x74, 0x72, 0x61, 0x6C, 0x6A, + 0x34, 0x31, 0x45, 0x36, 0x56, 0x66, 0x38, 0x50, 0x6C, 0x77, 0x55, 0x48, 0x42, 0x48, 0x0A, 0x51, 0x52, 0x46, 0x58, 0x47, 0x55, 0x37, 0x41, 0x6A, 0x36, 0x34, 0x47, 0x78, 0x4A, 0x55, 0x54, 0x46, 0x79, 0x38, 0x62, 0x4A, 0x5A, 0x39, 0x31, 0x38, + 0x72, 0x47, 0x4F, 0x6D, 0x61, 0x46, 0x76, 0x45, 0x37, 0x46, 0x42, 0x63, 0x66, 0x36, 0x49, 0x4B, 0x73, 0x68, 0x50, 0x45, 0x43, 0x42, 0x56, 0x31, 0x2F, 0x4D, 0x55, 0x52, 0x65, 0x58, 0x67, 0x52, 0x50, 0x54, 0x71, 0x68, 0x35, 0x55, 0x79, 0x6B, + 0x77, 0x37, 0x2B, 0x55, 0x30, 0x62, 0x36, 0x4C, 0x4A, 0x33, 0x2F, 0x0A, 0x69, 0x79, 0x4B, 0x35, 0x53, 0x39, 0x6B, 0x4A, 0x52, 0x61, 0x54, 0x65, 0x70, 0x4C, 0x69, 0x61, 0x57, 0x4E, 0x30, 0x62, 0x66, 0x56, 0x4B, 0x66, 0x6A, 0x6C, 0x6C, 0x44, + 0x69, 0x49, 0x47, 0x6B, 0x6E, 0x69, 0x62, 0x56, 0x62, 0x36, 0x33, 0x64, 0x44, 0x63, 0x59, 0x33, 0x66, 0x65, 0x30, 0x44, 0x6B, 0x68, 0x76, 0x6C, 0x64, 0x31, 0x39, 0x32, 0x37, 0x6A, 0x79, 0x4E, 0x78, 0x46, 0x31, 0x57, 0x57, 0x36, 0x4C, 0x5A, + 0x5A, 0x6D, 0x36, 0x7A, 0x4E, 0x54, 0x66, 0x6C, 0x0A, 0x4D, 0x72, 0x59, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, + 0x43, 0x50, 0x7A, 0x43, 0x43, 0x41, 0x63, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x42, 0x56, 0x56, 0x57, 0x76, 0x50, 0x4A, 0x65, 0x70, 0x44, 0x55, 0x31, 0x77, 0x36, 0x51, 0x50, 0x31, 0x61, 0x74, 0x46, 0x63, 0x6A, 0x41, + 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x68, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, + 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, + 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x64, 0x45, 0x61, + 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x7A, 0x41, 0x34, 0x4D, + 0x44, 0x45, 0x78, 0x4D, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4F, 0x44, 0x41, 0x78, 0x4D, 0x54, 0x55, 0x78, 0x4D, 0x6A, 0x41, 0x77, 0x0A, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x47, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x4A, + 0x62, 0x6D, 0x4D, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, 0x64, 0x33, 0x64, 0x79, 0x35, 0x6B, 0x0A, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, 0x56, 0x79, 0x64, 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, + 0x78, 0x49, 0x44, 0x41, 0x65, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x30, 0x52, 0x70, 0x5A, 0x32, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, + 0x30, 0x49, 0x45, 0x63, 0x7A, 0x4D, 0x48, 0x59, 0x77, 0x45, 0x41, 0x59, 0x48, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x43, 0x0A, 0x41, 0x51, 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x33, 0x61, + 0x66, 0x5A, 0x75, 0x34, 0x71, 0x34, 0x43, 0x2F, 0x73, 0x4C, 0x66, 0x79, 0x48, 0x53, 0x38, 0x4C, 0x36, 0x2B, 0x63, 0x2F, 0x4D, 0x7A, 0x58, 0x52, 0x71, 0x38, 0x4E, 0x4F, 0x72, 0x65, 0x78, 0x70, 0x75, 0x38, 0x30, 0x4A, 0x58, 0x32, 0x38, 0x4D, + 0x7A, 0x51, 0x43, 0x37, 0x70, 0x68, 0x57, 0x31, 0x46, 0x47, 0x66, 0x70, 0x34, 0x74, 0x6E, 0x2B, 0x36, 0x4F, 0x0A, 0x59, 0x77, 0x77, 0x58, 0x37, 0x41, 0x64, 0x77, 0x39, 0x63, 0x2B, 0x45, 0x4C, 0x6B, 0x43, 0x44, 0x6E, 0x4F, 0x67, 0x2F, 0x51, + 0x57, 0x30, 0x37, 0x72, 0x64, 0x4F, 0x6B, 0x46, 0x46, 0x6B, 0x32, 0x65, 0x4A, 0x30, 0x44, 0x51, 0x2B, 0x34, 0x51, 0x45, 0x32, 0x78, 0x79, 0x33, 0x71, 0x36, 0x49, 0x70, 0x36, 0x46, 0x72, 0x74, 0x55, 0x50, 0x4F, 0x5A, 0x39, 0x77, 0x6A, 0x2F, + 0x77, 0x4D, 0x63, 0x6F, 0x2B, 0x49, 0x2B, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x73, 0x39, 0x74, 0x49, 0x70, 0x50, 0x6D, 0x68, + 0x78, 0x64, 0x69, 0x75, 0x4E, 0x6B, 0x48, 0x4D, 0x45, 0x57, 0x4E, 0x70, 0x0A, 0x59, 0x69, 0x6D, 0x38, 0x53, 0x38, 0x59, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, + 0x77, 0x5A, 0x51, 0x49, 0x78, 0x41, 0x4B, 0x32, 0x38, 0x38, 0x6D, 0x77, 0x2F, 0x45, 0x6B, 0x72, 0x52, 0x4C, 0x54, 0x6E, 0x44, 0x43, 0x67, 0x6D, 0x58, 0x63, 0x2F, 0x53, 0x49, 0x4E, 0x6F, 0x79, 0x49, 0x4A, 0x37, 0x76, 0x6D, 0x69, 0x49, 0x31, + 0x51, 0x68, 0x61, 0x64, 0x6A, 0x2B, 0x5A, 0x34, 0x79, 0x0A, 0x33, 0x6D, 0x61, 0x54, 0x44, 0x2F, 0x48, 0x4D, 0x73, 0x51, 0x6D, 0x50, 0x33, 0x57, 0x79, 0x72, 0x2B, 0x6D, 0x74, 0x2F, 0x6F, 0x41, 0x49, 0x77, 0x4F, 0x57, 0x5A, 0x62, 0x77, 0x6D, + 0x53, 0x4E, 0x75, 0x4A, 0x35, 0x51, 0x33, 0x4B, 0x6A, 0x56, 0x53, 0x61, 0x4C, 0x74, 0x78, 0x39, 0x7A, 0x52, 0x53, 0x58, 0x38, 0x58, 0x41, 0x62, 0x6A, 0x49, 0x68, 0x6F, 0x39, 0x4F, 0x6A, 0x49, 0x67, 0x72, 0x71, 0x4A, 0x71, 0x70, 0x69, 0x73, + 0x58, 0x52, 0x41, 0x4C, 0x33, 0x34, 0x0A, 0x56, 0x4F, 0x4B, 0x61, 0x35, 0x56, 0x74, 0x38, 0x73, 0x79, 0x63, 0x58, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x34, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x6B, 0x44, 0x43, 0x43, 0x41, 0x33, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x42, 0x5A, 0x73, 0x62, 0x56, 0x35, 0x36, 0x4F, 0x49, 0x54, 0x4C, 0x69, 0x4F, 0x51, 0x65, + 0x39, 0x70, 0x33, 0x64, 0x31, 0x58, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x69, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, + 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x45, 0x77, 0x0A, 0x48, + 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x68, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x47, 0x56, 0x6B, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, + 0x7A, 0x51, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x4D, 0x77, 0x4F, 0x44, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x31, 0x0A, 0x4D, 0x54, 0x49, 0x77, + 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x69, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, + 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x42, 0x33, 0x0A, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x47, 0x6C, + 0x6E, 0x61, 0x57, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4D, 0x53, 0x45, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x68, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, + 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x47, 0x56, 0x6B, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, 0x7A, 0x51, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x0A, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, + 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x2F, 0x35, 0x70, 0x42, 0x7A, 0x61, 0x4E, 0x36, 0x37, 0x35, 0x46, 0x31, 0x4B, 0x50, 0x44, + 0x41, 0x69, 0x4D, 0x47, 0x6B, 0x7A, 0x37, 0x4D, 0x4B, 0x6E, 0x4A, 0x53, 0x37, 0x4A, 0x49, 0x54, 0x33, 0x79, 0x69, 0x74, 0x68, 0x5A, 0x77, 0x75, 0x45, 0x70, 0x0A, 0x70, 0x7A, 0x31, 0x59, 0x71, 0x33, 0x61, 0x61, 0x7A, 0x61, 0x35, 0x37, 0x47, + 0x34, 0x51, 0x4E, 0x78, 0x44, 0x41, 0x66, 0x38, 0x78, 0x75, 0x6B, 0x4F, 0x42, 0x62, 0x72, 0x56, 0x73, 0x61, 0x58, 0x62, 0x52, 0x32, 0x72, 0x73, 0x6E, 0x6E, 0x79, 0x79, 0x68, 0x48, 0x53, 0x35, 0x46, 0x2F, 0x57, 0x42, 0x54, 0x78, 0x53, 0x44, + 0x31, 0x49, 0x66, 0x78, 0x70, 0x34, 0x56, 0x70, 0x58, 0x36, 0x2B, 0x6E, 0x36, 0x6C, 0x58, 0x46, 0x6C, 0x6C, 0x56, 0x63, 0x71, 0x39, 0x6F, 0x0A, 0x6B, 0x33, 0x44, 0x43, 0x73, 0x72, 0x70, 0x31, 0x6D, 0x57, 0x70, 0x7A, 0x4D, 0x70, 0x54, 0x52, + 0x45, 0x45, 0x51, 0x51, 0x4C, 0x74, 0x2B, 0x43, 0x38, 0x77, 0x65, 0x45, 0x35, 0x6E, 0x51, 0x37, 0x62, 0x58, 0x48, 0x69, 0x4C, 0x51, 0x77, 0x62, 0x37, 0x69, 0x44, 0x56, 0x79, 0x53, 0x41, 0x64, 0x59, 0x79, 0x6B, 0x74, 0x7A, 0x75, 0x78, 0x65, + 0x54, 0x73, 0x69, 0x54, 0x2B, 0x43, 0x46, 0x68, 0x6D, 0x7A, 0x54, 0x72, 0x42, 0x63, 0x5A, 0x65, 0x37, 0x46, 0x73, 0x61, 0x0A, 0x76, 0x4F, 0x76, 0x4A, 0x7A, 0x38, 0x32, 0x73, 0x4E, 0x45, 0x42, 0x66, 0x73, 0x58, 0x70, 0x6D, 0x37, 0x6E, 0x66, + 0x49, 0x53, 0x4B, 0x68, 0x6D, 0x56, 0x31, 0x65, 0x66, 0x56, 0x46, 0x69, 0x4F, 0x44, 0x43, 0x75, 0x33, 0x54, 0x36, 0x63, 0x77, 0x32, 0x56, 0x62, 0x75, 0x79, 0x6E, 0x74, 0x64, 0x34, 0x36, 0x33, 0x4A, 0x54, 0x31, 0x37, 0x6C, 0x4E, 0x65, 0x63, + 0x78, 0x79, 0x39, 0x71, 0x54, 0x58, 0x74, 0x79, 0x4F, 0x6A, 0x34, 0x44, 0x61, 0x74, 0x70, 0x47, 0x59, 0x0A, 0x51, 0x4A, 0x42, 0x35, 0x77, 0x33, 0x6A, 0x48, 0x74, 0x72, 0x48, 0x45, 0x74, 0x57, 0x6F, 0x59, 0x4F, 0x41, 0x4D, 0x51, 0x6A, 0x64, + 0x6A, 0x55, 0x4E, 0x36, 0x51, 0x75, 0x42, 0x58, 0x32, 0x49, 0x39, 0x59, 0x49, 0x2B, 0x45, 0x4A, 0x46, 0x77, 0x71, 0x31, 0x57, 0x43, 0x51, 0x54, 0x4C, 0x58, 0x32, 0x77, 0x52, 0x7A, 0x4B, 0x6D, 0x36, 0x52, 0x41, 0x58, 0x77, 0x68, 0x54, 0x4E, + 0x53, 0x38, 0x72, 0x68, 0x73, 0x44, 0x64, 0x56, 0x31, 0x34, 0x5A, 0x74, 0x6B, 0x36, 0x0A, 0x4D, 0x55, 0x53, 0x61, 0x4D, 0x30, 0x43, 0x2F, 0x43, 0x4E, 0x64, 0x61, 0x53, 0x61, 0x54, 0x43, 0x35, 0x71, 0x6D, 0x67, 0x5A, 0x39, 0x32, 0x6B, 0x4A, + 0x37, 0x79, 0x68, 0x54, 0x7A, 0x6D, 0x31, 0x45, 0x56, 0x67, 0x58, 0x39, 0x79, 0x52, 0x63, 0x52, 0x6F, 0x39, 0x6B, 0x39, 0x38, 0x46, 0x70, 0x69, 0x48, 0x61, 0x59, 0x64, 0x6A, 0x31, 0x5A, 0x58, 0x55, 0x4A, 0x32, 0x68, 0x34, 0x6D, 0x58, 0x61, + 0x58, 0x70, 0x49, 0x38, 0x4F, 0x43, 0x69, 0x45, 0x68, 0x74, 0x6D, 0x0A, 0x6D, 0x6E, 0x54, 0x4B, 0x33, 0x6B, 0x73, 0x65, 0x35, 0x77, 0x35, 0x6A, 0x72, 0x75, 0x62, 0x55, 0x37, 0x35, 0x4B, 0x53, 0x4F, 0x70, 0x34, 0x39, 0x33, 0x41, 0x44, 0x6B, + 0x52, 0x53, 0x57, 0x4A, 0x74, 0x70, 0x70, 0x45, 0x47, 0x53, 0x74, 0x2B, 0x77, 0x4A, 0x53, 0x30, 0x30, 0x6D, 0x46, 0x74, 0x36, 0x7A, 0x50, 0x5A, 0x78, 0x64, 0x39, 0x4C, 0x42, 0x41, 0x44, 0x4D, 0x66, 0x52, 0x79, 0x56, 0x77, 0x34, 0x2F, 0x33, + 0x49, 0x62, 0x4B, 0x79, 0x45, 0x62, 0x65, 0x37, 0x0A, 0x66, 0x2F, 0x4C, 0x56, 0x6A, 0x48, 0x41, 0x73, 0x51, 0x57, 0x43, 0x71, 0x73, 0x57, 0x4D, 0x59, 0x52, 0x4A, 0x55, 0x61, 0x64, 0x6D, 0x4A, 0x2B, 0x39, 0x6F, 0x43, 0x77, 0x2B, 0x2B, 0x68, + 0x6B, 0x70, 0x6A, 0x50, 0x52, 0x69, 0x51, 0x66, 0x68, 0x76, 0x62, 0x66, 0x6D, 0x51, 0x36, 0x51, 0x59, 0x75, 0x4B, 0x5A, 0x33, 0x41, 0x65, 0x45, 0x50, 0x6C, 0x41, 0x77, 0x68, 0x48, 0x62, 0x4A, 0x55, 0x4B, 0x53, 0x57, 0x4A, 0x62, 0x4F, 0x55, + 0x4F, 0x55, 0x6C, 0x46, 0x48, 0x0A, 0x64, 0x4C, 0x34, 0x6D, 0x72, 0x4C, 0x5A, 0x42, 0x64, 0x64, 0x35, 0x36, 0x72, 0x46, 0x2B, 0x4E, 0x50, 0x38, 0x6D, 0x38, 0x30, 0x30, 0x45, 0x52, 0x45, 0x6C, 0x76, 0x6C, 0x45, 0x46, 0x44, 0x72, 0x4D, 0x63, + 0x58, 0x4B, 0x63, 0x68, 0x59, 0x69, 0x43, 0x64, 0x39, 0x38, 0x54, 0x48, 0x55, 0x2F, 0x59, 0x2B, 0x77, 0x68, 0x58, 0x38, 0x51, 0x67, 0x55, 0x57, 0x74, 0x76, 0x73, 0x61, 0x75, 0x47, 0x69, 0x30, 0x2F, 0x43, 0x31, 0x6B, 0x56, 0x66, 0x6E, 0x53, + 0x44, 0x38, 0x0A, 0x6F, 0x52, 0x37, 0x46, 0x77, 0x49, 0x2B, 0x69, 0x73, 0x58, 0x34, 0x4B, 0x4A, 0x70, 0x6E, 0x31, 0x35, 0x47, 0x6B, 0x76, 0x6D, 0x42, 0x30, 0x74, 0x39, 0x64, 0x6D, 0x70, 0x73, 0x68, 0x33, 0x6C, 0x47, 0x77, 0x49, 0x44, 0x41, + 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, + 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x37, 0x4E, 0x66, 0x6A, 0x67, 0x74, 0x4A, 0x78, 0x58, 0x57, 0x52, 0x4D, + 0x33, 0x79, 0x35, 0x6E, 0x50, 0x2B, 0x65, 0x36, 0x6D, 0x4B, 0x34, 0x63, 0x44, 0x30, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x0A, 0x67, 0x67, 0x49, + 0x42, 0x41, 0x4C, 0x74, 0x68, 0x32, 0x58, 0x32, 0x70, 0x62, 0x4C, 0x34, 0x58, 0x78, 0x4A, 0x45, 0x62, 0x77, 0x36, 0x47, 0x69, 0x41, 0x49, 0x33, 0x6A, 0x5A, 0x47, 0x67, 0x50, 0x56, 0x73, 0x39, 0x33, 0x72, 0x6E, 0x44, 0x35, 0x2F, 0x5A, 0x70, + 0x4B, 0x6D, 0x62, 0x6E, 0x4A, 0x65, 0x46, 0x77, 0x4D, 0x44, 0x46, 0x2F, 0x6B, 0x35, 0x68, 0x51, 0x70, 0x56, 0x67, 0x73, 0x32, 0x53, 0x56, 0x31, 0x45, 0x59, 0x2B, 0x43, 0x74, 0x6E, 0x4A, 0x59, 0x59, 0x0A, 0x5A, 0x68, 0x73, 0x6A, 0x44, 0x54, + 0x31, 0x35, 0x36, 0x57, 0x31, 0x72, 0x31, 0x6C, 0x54, 0x34, 0x30, 0x6A, 0x7A, 0x42, 0x51, 0x30, 0x43, 0x75, 0x48, 0x56, 0x44, 0x31, 0x55, 0x76, 0x79, 0x51, 0x4F, 0x37, 0x75, 0x59, 0x6D, 0x57, 0x6C, 0x72, 0x78, 0x38, 0x47, 0x6E, 0x71, 0x47, + 0x69, 0x6B, 0x4A, 0x39, 0x79, 0x64, 0x2B, 0x53, 0x65, 0x75, 0x4D, 0x49, 0x57, 0x35, 0x39, 0x6D, 0x64, 0x4E, 0x4F, 0x6A, 0x36, 0x50, 0x57, 0x54, 0x6B, 0x69, 0x55, 0x30, 0x54, 0x72, 0x0A, 0x79, 0x46, 0x30, 0x44, 0x79, 0x75, 0x31, 0x51, 0x65, + 0x6E, 0x31, 0x69, 0x49, 0x51, 0x71, 0x41, 0x79, 0x48, 0x4E, 0x6D, 0x30, 0x61, 0x41, 0x46, 0x59, 0x46, 0x2F, 0x6F, 0x70, 0x62, 0x53, 0x6E, 0x72, 0x36, 0x6A, 0x33, 0x62, 0x54, 0x57, 0x63, 0x66, 0x46, 0x71, 0x4B, 0x31, 0x71, 0x49, 0x34, 0x6D, + 0x66, 0x4E, 0x34, 0x69, 0x2F, 0x52, 0x4E, 0x30, 0x69, 0x41, 0x4C, 0x33, 0x67, 0x54, 0x75, 0x6A, 0x4A, 0x74, 0x48, 0x67, 0x58, 0x49, 0x4E, 0x77, 0x42, 0x51, 0x79, 0x0A, 0x37, 0x7A, 0x42, 0x5A, 0x4C, 0x71, 0x37, 0x67, 0x63, 0x66, 0x4A, 0x57, + 0x35, 0x47, 0x71, 0x58, 0x62, 0x35, 0x4A, 0x51, 0x62, 0x5A, 0x61, 0x4E, 0x61, 0x48, 0x71, 0x61, 0x73, 0x6A, 0x59, 0x55, 0x65, 0x67, 0x62, 0x79, 0x4A, 0x4C, 0x6B, 0x4A, 0x45, 0x56, 0x44, 0x58, 0x43, 0x4C, 0x47, 0x34, 0x69, 0x58, 0x71, 0x45, + 0x49, 0x32, 0x46, 0x43, 0x4B, 0x65, 0x57, 0x6A, 0x7A, 0x61, 0x49, 0x67, 0x51, 0x64, 0x66, 0x52, 0x6E, 0x47, 0x54, 0x5A, 0x36, 0x69, 0x61, 0x68, 0x0A, 0x69, 0x78, 0x54, 0x58, 0x54, 0x42, 0x6D, 0x79, 0x55, 0x45, 0x46, 0x78, 0x50, 0x54, 0x39, + 0x4E, 0x63, 0x43, 0x4F, 0x47, 0x44, 0x45, 0x72, 0x63, 0x67, 0x64, 0x4C, 0x4D, 0x4D, 0x70, 0x53, 0x45, 0x44, 0x51, 0x67, 0x4A, 0x6C, 0x78, 0x78, 0x50, 0x77, 0x4F, 0x35, 0x72, 0x49, 0x48, 0x51, 0x77, 0x30, 0x75, 0x41, 0x35, 0x4E, 0x42, 0x43, + 0x46, 0x49, 0x52, 0x55, 0x42, 0x43, 0x4F, 0x68, 0x56, 0x4D, 0x74, 0x35, 0x78, 0x53, 0x64, 0x6B, 0x6F, 0x46, 0x31, 0x42, 0x4E, 0x0A, 0x35, 0x72, 0x35, 0x4E, 0x30, 0x58, 0x57, 0x73, 0x30, 0x4D, 0x72, 0x37, 0x51, 0x62, 0x68, 0x44, 0x70, 0x61, + 0x72, 0x54, 0x77, 0x77, 0x56, 0x45, 0x54, 0x79, 0x77, 0x32, 0x6D, 0x2B, 0x4C, 0x36, 0x34, 0x6B, 0x57, 0x34, 0x49, 0x31, 0x4E, 0x73, 0x42, 0x6D, 0x39, 0x6E, 0x56, 0x58, 0x39, 0x47, 0x74, 0x55, 0x77, 0x2F, 0x62, 0x69, 0x68, 0x61, 0x65, 0x53, + 0x62, 0x53, 0x70, 0x4B, 0x68, 0x69, 0x6C, 0x39, 0x49, 0x65, 0x34, 0x75, 0x31, 0x4B, 0x69, 0x37, 0x77, 0x62, 0x0A, 0x2F, 0x55, 0x64, 0x4B, 0x44, 0x64, 0x39, 0x6E, 0x5A, 0x6E, 0x36, 0x79, 0x57, 0x30, 0x48, 0x51, 0x4F, 0x2B, 0x54, 0x30, 0x4F, + 0x2F, 0x51, 0x45, 0x59, 0x2B, 0x6E, 0x76, 0x77, 0x6C, 0x51, 0x41, 0x55, 0x61, 0x43, 0x4B, 0x4B, 0x73, 0x6E, 0x4F, 0x65, 0x4D, 0x7A, 0x56, 0x36, 0x6F, 0x63, 0x45, 0x47, 0x4C, 0x50, 0x4F, 0x72, 0x30, 0x6D, 0x49, 0x72, 0x2F, 0x4F, 0x53, 0x6D, + 0x62, 0x61, 0x7A, 0x35, 0x6D, 0x45, 0x50, 0x30, 0x6F, 0x55, 0x41, 0x35, 0x31, 0x41, 0x61, 0x0A, 0x35, 0x42, 0x75, 0x56, 0x6E, 0x52, 0x6D, 0x68, 0x75, 0x5A, 0x79, 0x78, 0x6D, 0x37, 0x45, 0x41, 0x48, 0x75, 0x2F, 0x51, 0x44, 0x30, 0x39, 0x43, + 0x62, 0x4D, 0x6B, 0x4B, 0x76, 0x4F, 0x35, 0x44, 0x2B, 0x6A, 0x70, 0x78, 0x70, 0x63, 0x68, 0x4E, 0x4A, 0x71, 0x55, 0x31, 0x2F, 0x59, 0x6C, 0x64, 0x76, 0x49, 0x56, 0x69, 0x48, 0x54, 0x4C, 0x53, 0x6F, 0x43, 0x74, 0x55, 0x37, 0x5A, 0x70, 0x58, + 0x77, 0x64, 0x76, 0x36, 0x45, 0x4D, 0x38, 0x5A, 0x74, 0x34, 0x74, 0x4B, 0x0A, 0x47, 0x34, 0x38, 0x42, 0x74, 0x69, 0x65, 0x56, 0x55, 0x2B, 0x69, 0x32, 0x69, 0x57, 0x31, 0x62, 0x76, 0x47, 0x6A, 0x55, 0x49, 0x2B, 0x69, 0x4C, 0x55, 0x61, 0x4A, + 0x57, 0x2B, 0x66, 0x43, 0x6D, 0x67, 0x4B, 0x44, 0x57, 0x48, 0x72, 0x4F, 0x38, 0x44, 0x77, 0x39, 0x54, 0x64, 0x53, 0x6D, 0x71, 0x36, 0x68, 0x4E, 0x33, 0x35, 0x4E, 0x36, 0x4D, 0x67, 0x53, 0x47, 0x74, 0x42, 0x78, 0x42, 0x48, 0x45, 0x61, 0x32, + 0x48, 0x50, 0x51, 0x66, 0x52, 0x64, 0x62, 0x7A, 0x50, 0x0A, 0x38, 0x32, 0x5A, 0x2B, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x0A, 0x43, 0x4F, 0x4D, 0x4F, 0x44, 0x4F, 0x20, 0x52, 0x53, 0x41, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, + 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x32, 0x44, 0x43, 0x43, 0x41, 0x38, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, + 0x54, 0x4B, 0x72, 0x35, 0x79, 0x74, 0x74, 0x6A, 0x62, 0x2B, 0x41, 0x66, 0x39, 0x30, 0x37, 0x59, 0x57, 0x77, 0x4F, 0x47, 0x6E, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, + 0x41, 0x44, 0x43, 0x42, 0x68, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x30, 0x49, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x45, 0x6B, 0x64, + 0x79, 0x5A, 0x57, 0x46, 0x30, 0x5A, 0x58, 0x49, 0x67, 0x54, 0x57, 0x46, 0x75, 0x59, 0x32, 0x68, 0x6C, 0x63, 0x33, 0x52, 0x6C, 0x63, 0x6A, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x48, 0x55, 0x32, 0x46, + 0x73, 0x5A, 0x6D, 0x39, 0x79, 0x5A, 0x44, 0x45, 0x61, 0x4D, 0x42, 0x67, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x52, 0x51, 0x30, 0x39, 0x4E, 0x54, 0x30, 0x52, 0x50, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, + 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x49, 0x6B, 0x4E, 0x50, 0x54, 0x55, 0x39, 0x45, 0x54, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, + 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x0A, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x54, 0x45, 0x35, 0x4D, + 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x34, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x43, 0x42, 0x68, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x0A, 0x52, 0x30, 0x49, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x45, 0x6B, 0x64, 0x79, 0x5A, 0x57, 0x46, 0x30, 0x5A, 0x58, 0x49, 0x67, 0x54, 0x57, 0x46, 0x75, + 0x59, 0x32, 0x68, 0x6C, 0x63, 0x33, 0x52, 0x6C, 0x63, 0x6A, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x48, 0x55, 0x32, 0x46, 0x73, 0x5A, 0x6D, 0x39, 0x79, 0x5A, 0x44, 0x45, 0x61, 0x4D, 0x42, 0x67, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x68, 0x4D, 0x52, 0x51, 0x30, 0x39, 0x4E, 0x54, 0x30, 0x52, 0x50, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x45, 0x78, 0x70, 0x62, 0x57, 0x6C, 0x30, 0x5A, 0x57, 0x51, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x4D, 0x54, 0x49, 0x6B, 0x4E, 0x50, 0x54, 0x55, 0x39, 0x45, 0x54, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, + 0x42, 0x0A, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, + 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x52, 0x36, 0x46, 0x53, 0x53, 0x30, 0x67, 0x70, 0x57, 0x73, 0x61, 0x77, 0x4E, 0x4A, 0x4E, 0x33, 0x46, 0x7A, 0x30, 0x52, 0x6E, 0x0A, 0x64, + 0x4A, 0x6B, 0x72, 0x4E, 0x36, 0x4E, 0x39, 0x49, 0x33, 0x41, 0x41, 0x63, 0x62, 0x78, 0x54, 0x33, 0x38, 0x54, 0x36, 0x4B, 0x68, 0x4B, 0x50, 0x53, 0x33, 0x38, 0x51, 0x56, 0x72, 0x32, 0x66, 0x63, 0x48, 0x4B, 0x33, 0x59, 0x58, 0x2F, 0x4A, 0x53, + 0x77, 0x38, 0x58, 0x70, 0x7A, 0x33, 0x6A, 0x73, 0x41, 0x52, 0x68, 0x37, 0x76, 0x38, 0x52, 0x6C, 0x38, 0x66, 0x30, 0x68, 0x6A, 0x34, 0x4B, 0x2B, 0x6A, 0x35, 0x63, 0x2B, 0x5A, 0x50, 0x6D, 0x4E, 0x48, 0x72, 0x5A, 0x0A, 0x46, 0x47, 0x76, 0x6E, + 0x6E, 0x4C, 0x4F, 0x46, 0x6F, 0x49, 0x4A, 0x36, 0x64, 0x71, 0x39, 0x78, 0x6B, 0x4E, 0x66, 0x73, 0x2F, 0x51, 0x33, 0x36, 0x6E, 0x47, 0x7A, 0x36, 0x33, 0x37, 0x43, 0x43, 0x39, 0x42, 0x52, 0x2B, 0x2B, 0x62, 0x37, 0x45, 0x70, 0x69, 0x39, 0x50, + 0x66, 0x35, 0x6C, 0x2F, 0x74, 0x66, 0x78, 0x6E, 0x51, 0x33, 0x4B, 0x39, 0x44, 0x41, 0x44, 0x57, 0x69, 0x65, 0x74, 0x72, 0x4C, 0x4E, 0x50, 0x74, 0x6A, 0x35, 0x67, 0x63, 0x46, 0x4B, 0x74, 0x2B, 0x0A, 0x35, 0x65, 0x4E, 0x75, 0x2F, 0x4E, 0x69, + 0x6F, 0x35, 0x4A, 0x49, 0x6B, 0x32, 0x6B, 0x4E, 0x72, 0x59, 0x72, 0x68, 0x56, 0x2F, 0x65, 0x72, 0x42, 0x76, 0x47, 0x79, 0x32, 0x69, 0x2F, 0x4D, 0x4F, 0x6A, 0x5A, 0x72, 0x6B, 0x6D, 0x32, 0x78, 0x70, 0x6D, 0x66, 0x68, 0x34, 0x53, 0x44, 0x42, + 0x46, 0x31, 0x61, 0x33, 0x68, 0x44, 0x54, 0x78, 0x46, 0x59, 0x50, 0x77, 0x79, 0x6C, 0x6C, 0x45, 0x6E, 0x76, 0x47, 0x66, 0x44, 0x79, 0x69, 0x36, 0x32, 0x61, 0x2B, 0x70, 0x47, 0x0A, 0x78, 0x38, 0x63, 0x67, 0x6F, 0x4C, 0x45, 0x66, 0x5A, 0x64, + 0x35, 0x49, 0x43, 0x4C, 0x71, 0x6B, 0x54, 0x71, 0x6E, 0x79, 0x67, 0x30, 0x59, 0x33, 0x68, 0x4F, 0x76, 0x6F, 0x7A, 0x49, 0x46, 0x49, 0x51, 0x32, 0x64, 0x4F, 0x63, 0x69, 0x71, 0x62, 0x58, 0x4C, 0x31, 0x4D, 0x47, 0x79, 0x69, 0x4B, 0x58, 0x43, + 0x4A, 0x37, 0x74, 0x4B, 0x75, 0x59, 0x32, 0x65, 0x37, 0x67, 0x55, 0x59, 0x50, 0x44, 0x43, 0x55, 0x5A, 0x4F, 0x62, 0x54, 0x36, 0x5A, 0x2B, 0x70, 0x55, 0x58, 0x0A, 0x32, 0x6E, 0x77, 0x7A, 0x56, 0x30, 0x45, 0x38, 0x6A, 0x56, 0x48, 0x74, 0x43, + 0x37, 0x5A, 0x63, 0x72, 0x79, 0x78, 0x6A, 0x47, 0x74, 0x39, 0x58, 0x79, 0x44, 0x2B, 0x38, 0x36, 0x56, 0x33, 0x45, 0x6D, 0x36, 0x39, 0x46, 0x6D, 0x65, 0x4B, 0x6A, 0x57, 0x69, 0x53, 0x30, 0x75, 0x71, 0x6C, 0x57, 0x50, 0x63, 0x39, 0x76, 0x71, + 0x76, 0x39, 0x4A, 0x57, 0x4C, 0x37, 0x77, 0x71, 0x50, 0x2F, 0x30, 0x75, 0x4B, 0x33, 0x70, 0x4E, 0x2F, 0x75, 0x36, 0x75, 0x50, 0x51, 0x4C, 0x0A, 0x4F, 0x76, 0x6E, 0x6F, 0x51, 0x30, 0x49, 0x65, 0x69, 0x64, 0x69, 0x45, 0x79, 0x78, 0x50, 0x78, + 0x32, 0x62, 0x76, 0x68, 0x69, 0x57, 0x43, 0x34, 0x6A, 0x43, 0x68, 0x57, 0x72, 0x42, 0x51, 0x64, 0x6E, 0x41, 0x72, 0x6E, 0x63, 0x65, 0x76, 0x50, 0x44, 0x74, 0x30, 0x39, 0x71, 0x5A, 0x61, 0x68, 0x53, 0x4C, 0x30, 0x38, 0x39, 0x36, 0x2B, 0x31, + 0x44, 0x53, 0x4A, 0x4D, 0x77, 0x42, 0x47, 0x42, 0x37, 0x46, 0x59, 0x37, 0x39, 0x74, 0x4F, 0x69, 0x34, 0x6C, 0x75, 0x33, 0x0A, 0x73, 0x67, 0x51, 0x69, 0x55, 0x70, 0x57, 0x41, 0x6B, 0x32, 0x6E, 0x6F, 0x6A, 0x6B, 0x78, 0x6C, 0x38, 0x5A, 0x45, + 0x44, 0x4C, 0x58, 0x42, 0x30, 0x41, 0x75, 0x71, 0x4C, 0x5A, 0x78, 0x55, 0x70, 0x61, 0x56, 0x49, 0x43, 0x75, 0x39, 0x66, 0x66, 0x55, 0x47, 0x70, 0x56, 0x52, 0x72, 0x2B, 0x67, 0x6F, 0x79, 0x68, 0x68, 0x66, 0x33, 0x44, 0x51, 0x77, 0x36, 0x4B, + 0x71, 0x4C, 0x43, 0x47, 0x71, 0x52, 0x38, 0x34, 0x6F, 0x6E, 0x41, 0x5A, 0x46, 0x64, 0x72, 0x2B, 0x43, 0x0A, 0x47, 0x43, 0x65, 0x30, 0x31, 0x61, 0x36, 0x30, 0x79, 0x31, 0x44, 0x6D, 0x61, 0x2F, 0x52, 0x4D, 0x68, 0x6E, 0x45, 0x77, 0x36, 0x61, + 0x62, 0x66, 0x46, 0x6F, 0x62, 0x67, 0x32, 0x50, 0x39, 0x41, 0x33, 0x66, 0x76, 0x51, 0x51, 0x6F, 0x68, 0x2F, 0x6F, 0x7A, 0x4D, 0x36, 0x4C, 0x6C, 0x77, 0x65, 0x51, 0x52, 0x47, 0x42, 0x59, 0x38, 0x34, 0x59, 0x63, 0x57, 0x73, 0x72, 0x37, 0x4B, + 0x61, 0x4B, 0x74, 0x7A, 0x46, 0x63, 0x4F, 0x6D, 0x70, 0x48, 0x34, 0x4D, 0x4E, 0x35, 0x0A, 0x57, 0x64, 0x59, 0x67, 0x47, 0x71, 0x2F, 0x79, 0x61, 0x70, 0x69, 0x71, 0x63, 0x72, 0x78, 0x58, 0x53, 0x74, 0x4A, 0x4C, 0x6E, 0x62, 0x73, 0x51, 0x2F, + 0x4C, 0x42, 0x4D, 0x51, 0x65, 0x58, 0x74, 0x48, 0x54, 0x31, 0x65, 0x4B, 0x4A, 0x32, 0x63, 0x7A, 0x4C, 0x2B, 0x7A, 0x55, 0x64, 0x71, 0x6E, 0x52, 0x2B, 0x57, 0x45, 0x55, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, + 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x0A, 0x46, 0x67, 0x51, 0x55, 0x75, 0x36, 0x39, 0x2B, 0x41, 0x6A, 0x33, 0x36, 0x70, 0x76, 0x45, 0x38, 0x68, 0x49, 0x36, 0x74, 0x37, 0x6A, 0x69, 0x59, 0x37, 0x4E, 0x6B, 0x79, + 0x4D, 0x74, 0x51, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, + 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x41, 0x72, 0x78, 0x31, 0x55, 0x61, + 0x45, 0x74, 0x36, 0x35, 0x52, 0x75, 0x32, 0x79, 0x79, 0x54, 0x55, 0x45, 0x55, 0x41, 0x4A, 0x4E, 0x4D, 0x6E, 0x4D, 0x76, 0x6C, 0x77, 0x46, 0x54, 0x50, 0x6F, 0x43, 0x57, 0x4F, 0x41, 0x76, 0x6E, 0x39, 0x73, 0x4B, 0x49, 0x4E, 0x39, 0x53, 0x43, + 0x59, 0x50, 0x42, 0x4D, 0x74, 0x0A, 0x72, 0x46, 0x61, 0x69, 0x73, 0x4E, 0x5A, 0x2B, 0x45, 0x5A, 0x4C, 0x70, 0x4C, 0x72, 0x71, 0x65, 0x4C, 0x70, 0x70, 0x79, 0x73, 0x62, 0x30, 0x5A, 0x52, 0x47, 0x78, 0x68, 0x4E, 0x61, 0x4B, 0x61, 0x74, 0x42, + 0x59, 0x53, 0x61, 0x56, 0x71, 0x4D, 0x34, 0x64, 0x63, 0x2B, 0x70, 0x42, 0x72, 0x6F, 0x4C, 0x77, 0x50, 0x30, 0x72, 0x6D, 0x45, 0x64, 0x45, 0x42, 0x73, 0x71, 0x70, 0x49, 0x74, 0x36, 0x78, 0x66, 0x34, 0x46, 0x70, 0x75, 0x48, 0x41, 0x31, 0x73, + 0x6A, 0x2B, 0x0A, 0x6E, 0x71, 0x36, 0x50, 0x4B, 0x37, 0x6F, 0x39, 0x6D, 0x66, 0x6A, 0x59, 0x63, 0x77, 0x6C, 0x59, 0x52, 0x6D, 0x36, 0x6D, 0x6E, 0x50, 0x54, 0x58, 0x4A, 0x39, 0x4F, 0x56, 0x32, 0x6A, 0x65, 0x44, 0x63, 0x68, 0x7A, 0x54, 0x63, + 0x2B, 0x43, 0x69, 0x52, 0x35, 0x6B, 0x44, 0x4F, 0x46, 0x33, 0x56, 0x53, 0x58, 0x6B, 0x41, 0x4B, 0x52, 0x7A, 0x48, 0x37, 0x4A, 0x73, 0x67, 0x48, 0x41, 0x63, 0x6B, 0x61, 0x56, 0x64, 0x34, 0x73, 0x6A, 0x6E, 0x38, 0x4F, 0x6F, 0x53, 0x67, 0x0A, + 0x74, 0x5A, 0x78, 0x38, 0x6A, 0x62, 0x38, 0x75, 0x6B, 0x32, 0x49, 0x6E, 0x74, 0x7A, 0x6E, 0x61, 0x46, 0x78, 0x69, 0x75, 0x76, 0x54, 0x77, 0x4A, 0x61, 0x50, 0x2B, 0x45, 0x6D, 0x7A, 0x7A, 0x56, 0x31, 0x67, 0x73, 0x44, 0x34, 0x31, 0x65, 0x65, + 0x46, 0x50, 0x66, 0x52, 0x36, 0x30, 0x2F, 0x49, 0x76, 0x59, 0x63, 0x6A, 0x74, 0x37, 0x5A, 0x4A, 0x51, 0x33, 0x6D, 0x46, 0x58, 0x4C, 0x72, 0x72, 0x6B, 0x67, 0x75, 0x68, 0x78, 0x75, 0x68, 0x6F, 0x71, 0x45, 0x77, 0x57, 0x0A, 0x73, 0x52, 0x71, + 0x5A, 0x43, 0x75, 0x68, 0x54, 0x4C, 0x4A, 0x4B, 0x37, 0x6F, 0x51, 0x6B, 0x59, 0x64, 0x51, 0x78, 0x6C, 0x71, 0x48, 0x76, 0x4C, 0x49, 0x37, 0x63, 0x61, 0x77, 0x69, 0x69, 0x46, 0x77, 0x78, 0x76, 0x2F, 0x30, 0x43, 0x74, 0x69, 0x37, 0x36, 0x52, + 0x37, 0x43, 0x5A, 0x47, 0x59, 0x5A, 0x34, 0x77, 0x55, 0x41, 0x63, 0x31, 0x6F, 0x42, 0x6D, 0x70, 0x6A, 0x49, 0x58, 0x55, 0x44, 0x67, 0x49, 0x69, 0x4B, 0x62, 0x6F, 0x48, 0x47, 0x68, 0x66, 0x4B, 0x70, 0x0A, 0x70, 0x43, 0x33, 0x6E, 0x39, 0x4B, + 0x55, 0x6B, 0x45, 0x45, 0x65, 0x44, 0x79, 0x73, 0x33, 0x30, 0x6A, 0x58, 0x6C, 0x59, 0x73, 0x51, 0x61, 0x62, 0x35, 0x78, 0x6F, 0x71, 0x32, 0x5A, 0x30, 0x42, 0x31, 0x35, 0x52, 0x39, 0x37, 0x51, 0x4E, 0x4B, 0x79, 0x76, 0x44, 0x62, 0x36, 0x4B, + 0x6B, 0x42, 0x50, 0x76, 0x56, 0x57, 0x6D, 0x63, 0x6B, 0x65, 0x6A, 0x6B, 0x6B, 0x39, 0x75, 0x2B, 0x55, 0x4A, 0x75, 0x65, 0x42, 0x50, 0x53, 0x5A, 0x49, 0x39, 0x46, 0x6F, 0x4A, 0x41, 0x0A, 0x7A, 0x4D, 0x78, 0x5A, 0x78, 0x75, 0x59, 0x36, 0x37, + 0x52, 0x49, 0x75, 0x61, 0x54, 0x78, 0x73, 0x6C, 0x62, 0x48, 0x39, 0x71, 0x68, 0x31, 0x37, 0x66, 0x34, 0x61, 0x2B, 0x48, 0x67, 0x34, 0x79, 0x52, 0x76, 0x76, 0x37, 0x45, 0x34, 0x39, 0x31, 0x66, 0x30, 0x79, 0x4C, 0x53, 0x30, 0x5A, 0x6A, 0x2F, + 0x67, 0x41, 0x30, 0x51, 0x48, 0x44, 0x42, 0x77, 0x37, 0x6D, 0x68, 0x33, 0x61, 0x5A, 0x77, 0x34, 0x67, 0x53, 0x7A, 0x51, 0x62, 0x7A, 0x70, 0x67, 0x4A, 0x48, 0x71, 0x0A, 0x5A, 0x4A, 0x78, 0x36, 0x34, 0x53, 0x49, 0x44, 0x71, 0x5A, 0x78, 0x75, + 0x62, 0x77, 0x35, 0x6C, 0x54, 0x32, 0x79, 0x48, 0x68, 0x31, 0x37, 0x7A, 0x62, 0x71, 0x44, 0x35, 0x64, 0x61, 0x57, 0x62, 0x51, 0x4F, 0x68, 0x54, 0x73, 0x69, 0x65, 0x64, 0x53, 0x72, 0x6E, 0x41, 0x64, 0x79, 0x47, 0x4E, 0x2F, 0x34, 0x66, 0x79, + 0x33, 0x72, 0x79, 0x4D, 0x37, 0x78, 0x66, 0x66, 0x74, 0x30, 0x6B, 0x4C, 0x30, 0x66, 0x4A, 0x75, 0x4D, 0x41, 0x73, 0x61, 0x44, 0x6B, 0x35, 0x32, 0x0A, 0x37, 0x52, 0x48, 0x38, 0x39, 0x65, 0x6C, 0x57, 0x73, 0x6E, 0x32, 0x2F, 0x78, 0x32, 0x30, + 0x4B, 0x6B, 0x34, 0x79, 0x6C, 0x30, 0x4D, 0x43, 0x32, 0x48, 0x62, 0x34, 0x36, 0x54, 0x70, 0x53, 0x69, 0x31, 0x32, 0x35, 0x73, 0x43, 0x38, 0x4B, 0x4B, 0x66, 0x50, 0x6F, 0x67, 0x38, 0x38, 0x54, 0x6B, 0x35, 0x63, 0x30, 0x4E, 0x71, 0x4D, 0x75, + 0x52, 0x6B, 0x72, 0x46, 0x38, 0x68, 0x65, 0x79, 0x31, 0x46, 0x47, 0x6C, 0x6D, 0x44, 0x6F, 0x4C, 0x6E, 0x7A, 0x63, 0x37, 0x49, 0x0A, 0x4C, 0x61, 0x5A, 0x52, 0x66, 0x79, 0x48, 0x42, 0x4E, 0x56, 0x4F, 0x46, 0x42, 0x6B, 0x70, 0x64, 0x6E, 0x36, + 0x32, 0x37, 0x47, 0x31, 0x39, 0x30, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x55, 0x53, 0x45, 0x52, 0x54, 0x72, + 0x75, 0x73, 0x74, 0x20, 0x52, 0x53, 0x41, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x33, 0x6A, 0x43, 0x43, 0x41, 0x38, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x41, 0x66, + 0x31, 0x74, 0x4D, 0x50, 0x79, 0x6A, 0x79, 0x6C, 0x47, 0x6F, 0x47, 0x37, 0x78, 0x6B, 0x44, 0x6A, 0x55, 0x44, 0x4C, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, + 0x43, 0x42, 0x69, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x43, 0x6B, 0x35, 0x6C, 0x64, + 0x79, 0x42, 0x4B, 0x5A, 0x58, 0x4A, 0x7A, 0x5A, 0x58, 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x30, 0x70, 0x6C, 0x63, 0x6E, 0x4E, 0x6C, 0x65, 0x53, 0x42, 0x44, 0x61, 0x58, 0x52, 0x35, 0x4D, + 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x45, 0x78, 0x56, 0x55, 0x61, 0x47, 0x55, 0x67, 0x56, 0x56, 0x4E, 0x46, 0x55, 0x6C, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x35, 0x6C, 0x64, 0x48, 0x64, 0x76, + 0x63, 0x6D, 0x73, 0x78, 0x4C, 0x6A, 0x41, 0x73, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4A, 0x56, 0x56, 0x54, 0x52, 0x56, 0x4A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x51, 0x32, 0x56, 0x79, + 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x0A, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x6A, 0x41, + 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x34, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x43, 0x42, 0x69, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x43, 0x6B, 0x35, 0x6C, 0x64, 0x79, 0x42, 0x4B, 0x5A, 0x58, 0x4A, 0x7A, 0x5A, 0x58, + 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x30, 0x70, 0x6C, 0x63, 0x6E, 0x4E, 0x6C, 0x65, 0x53, 0x42, 0x44, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x4B, 0x0A, 0x45, 0x78, 0x56, 0x55, 0x61, 0x47, 0x55, 0x67, 0x56, 0x56, 0x4E, 0x46, 0x55, 0x6C, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x35, 0x6C, 0x64, 0x48, 0x64, 0x76, 0x63, 0x6D, 0x73, 0x78, 0x4C, 0x6A, 0x41, 0x73, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4A, 0x56, 0x56, 0x54, 0x52, 0x56, 0x4A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x0A, + 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, + 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x41, 0x45, 0x6D, 0x55, 0x58, 0x4E, 0x67, 0x37, 0x44, 0x32, 0x77, 0x69, 0x7A, 0x0A, 0x30, 0x4B, 0x78, + 0x58, 0x44, 0x58, 0x62, 0x74, 0x7A, 0x53, 0x66, 0x54, 0x54, 0x4B, 0x31, 0x51, 0x67, 0x32, 0x48, 0x69, 0x71, 0x69, 0x42, 0x4E, 0x43, 0x53, 0x31, 0x6B, 0x43, 0x64, 0x7A, 0x4F, 0x69, 0x5A, 0x2F, 0x4D, 0x50, 0x61, 0x6E, 0x73, 0x39, 0x73, 0x2F, + 0x42, 0x33, 0x50, 0x48, 0x54, 0x73, 0x64, 0x5A, 0x37, 0x4E, 0x79, 0x67, 0x52, 0x4B, 0x30, 0x66, 0x61, 0x4F, 0x63, 0x61, 0x38, 0x4F, 0x68, 0x6D, 0x30, 0x58, 0x36, 0x61, 0x39, 0x66, 0x5A, 0x32, 0x6A, 0x0A, 0x59, 0x30, 0x4B, 0x32, 0x64, 0x76, + 0x4B, 0x70, 0x4F, 0x79, 0x75, 0x52, 0x2B, 0x4F, 0x4A, 0x76, 0x30, 0x4F, 0x77, 0x57, 0x49, 0x4A, 0x41, 0x4A, 0x50, 0x75, 0x4C, 0x6F, 0x64, 0x4D, 0x6B, 0x59, 0x74, 0x4A, 0x48, 0x55, 0x59, 0x6D, 0x54, 0x62, 0x66, 0x36, 0x4D, 0x47, 0x38, 0x59, + 0x67, 0x59, 0x61, 0x70, 0x41, 0x69, 0x50, 0x4C, 0x7A, 0x2B, 0x45, 0x2F, 0x43, 0x48, 0x46, 0x48, 0x76, 0x32, 0x35, 0x42, 0x2B, 0x4F, 0x31, 0x4F, 0x52, 0x52, 0x78, 0x68, 0x46, 0x6E, 0x0A, 0x52, 0x67, 0x68, 0x52, 0x79, 0x34, 0x59, 0x55, 0x56, + 0x44, 0x2B, 0x38, 0x4D, 0x2F, 0x35, 0x2B, 0x62, 0x4A, 0x7A, 0x2F, 0x46, 0x70, 0x30, 0x59, 0x76, 0x56, 0x47, 0x4F, 0x4E, 0x61, 0x61, 0x6E, 0x5A, 0x73, 0x68, 0x79, 0x5A, 0x39, 0x73, 0x68, 0x5A, 0x72, 0x48, 0x55, 0x6D, 0x33, 0x67, 0x44, 0x77, + 0x46, 0x41, 0x36, 0x36, 0x4D, 0x7A, 0x77, 0x33, 0x4C, 0x79, 0x65, 0x54, 0x50, 0x36, 0x76, 0x42, 0x5A, 0x59, 0x31, 0x48, 0x31, 0x64, 0x61, 0x74, 0x2F, 0x2F, 0x4F, 0x0A, 0x2B, 0x54, 0x32, 0x33, 0x4C, 0x4C, 0x62, 0x32, 0x56, 0x4E, 0x33, 0x49, + 0x35, 0x78, 0x49, 0x36, 0x54, 0x61, 0x35, 0x4D, 0x69, 0x72, 0x64, 0x63, 0x6D, 0x72, 0x53, 0x33, 0x49, 0x44, 0x33, 0x4B, 0x66, 0x79, 0x49, 0x30, 0x72, 0x6E, 0x34, 0x37, 0x61, 0x47, 0x59, 0x42, 0x52, 0x4F, 0x63, 0x42, 0x54, 0x6B, 0x5A, 0x54, + 0x6D, 0x7A, 0x4E, 0x67, 0x39, 0x35, 0x53, 0x2B, 0x55, 0x7A, 0x65, 0x51, 0x63, 0x30, 0x50, 0x7A, 0x4D, 0x73, 0x4E, 0x54, 0x37, 0x39, 0x75, 0x71, 0x0A, 0x2F, 0x6E, 0x52, 0x4F, 0x61, 0x63, 0x64, 0x72, 0x6A, 0x47, 0x43, 0x54, 0x33, 0x73, 0x54, + 0x48, 0x44, 0x4E, 0x2F, 0x68, 0x4D, 0x71, 0x37, 0x4D, 0x6B, 0x7A, 0x74, 0x52, 0x65, 0x4A, 0x56, 0x6E, 0x69, 0x2B, 0x34, 0x39, 0x56, 0x76, 0x34, 0x4D, 0x30, 0x47, 0x6B, 0x50, 0x47, 0x77, 0x2F, 0x7A, 0x4A, 0x53, 0x5A, 0x72, 0x4D, 0x32, 0x33, + 0x33, 0x62, 0x6B, 0x66, 0x36, 0x63, 0x30, 0x50, 0x6C, 0x66, 0x67, 0x36, 0x6C, 0x5A, 0x72, 0x45, 0x70, 0x66, 0x44, 0x4B, 0x45, 0x0A, 0x59, 0x31, 0x57, 0x4A, 0x78, 0x41, 0x33, 0x42, 0x6B, 0x31, 0x51, 0x77, 0x47, 0x52, 0x4F, 0x73, 0x30, 0x33, + 0x30, 0x33, 0x70, 0x2B, 0x74, 0x64, 0x4F, 0x6D, 0x77, 0x31, 0x58, 0x4E, 0x74, 0x42, 0x31, 0x78, 0x4C, 0x61, 0x71, 0x55, 0x6B, 0x4C, 0x33, 0x39, 0x69, 0x41, 0x69, 0x67, 0x6D, 0x54, 0x59, 0x6F, 0x36, 0x31, 0x5A, 0x73, 0x38, 0x6C, 0x69, 0x4D, + 0x32, 0x45, 0x75, 0x4C, 0x45, 0x2F, 0x70, 0x44, 0x6B, 0x50, 0x32, 0x51, 0x4B, 0x65, 0x36, 0x78, 0x4A, 0x4D, 0x0A, 0x6C, 0x58, 0x7A, 0x7A, 0x61, 0x77, 0x57, 0x70, 0x58, 0x68, 0x61, 0x44, 0x7A, 0x4C, 0x68, 0x6E, 0x34, 0x75, 0x67, 0x54, 0x6E, + 0x63, 0x78, 0x62, 0x67, 0x74, 0x4E, 0x4D, 0x73, 0x2B, 0x31, 0x62, 0x2F, 0x39, 0x37, 0x6C, 0x63, 0x36, 0x77, 0x6A, 0x4F, 0x79, 0x30, 0x41, 0x76, 0x7A, 0x56, 0x56, 0x64, 0x41, 0x6C, 0x4A, 0x32, 0x45, 0x6C, 0x59, 0x47, 0x6E, 0x2B, 0x53, 0x4E, + 0x75, 0x5A, 0x52, 0x6B, 0x67, 0x37, 0x7A, 0x4A, 0x6E, 0x30, 0x63, 0x54, 0x52, 0x65, 0x38, 0x0A, 0x79, 0x65, 0x78, 0x44, 0x4A, 0x74, 0x43, 0x2F, 0x51, 0x56, 0x39, 0x41, 0x71, 0x55, 0x52, 0x45, 0x39, 0x4A, 0x6E, 0x6E, 0x56, 0x34, 0x65, 0x65, + 0x55, 0x42, 0x39, 0x58, 0x56, 0x4B, 0x67, 0x2B, 0x2F, 0x58, 0x52, 0x6A, 0x4C, 0x37, 0x46, 0x51, 0x5A, 0x51, 0x6E, 0x6D, 0x57, 0x45, 0x49, 0x75, 0x51, 0x78, 0x70, 0x4D, 0x74, 0x50, 0x41, 0x6C, 0x52, 0x31, 0x6E, 0x36, 0x42, 0x42, 0x36, 0x54, + 0x31, 0x43, 0x5A, 0x47, 0x53, 0x6C, 0x43, 0x42, 0x73, 0x74, 0x36, 0x2B, 0x0A, 0x65, 0x4C, 0x66, 0x38, 0x5A, 0x78, 0x58, 0x68, 0x79, 0x56, 0x65, 0x45, 0x48, 0x67, 0x39, 0x6A, 0x31, 0x75, 0x6C, 0x69, 0x75, 0x74, 0x5A, 0x66, 0x56, 0x53, 0x37, + 0x71, 0x58, 0x4D, 0x59, 0x6F, 0x43, 0x41, 0x51, 0x6C, 0x4F, 0x62, 0x67, 0x4F, 0x4B, 0x36, 0x6E, 0x79, 0x54, 0x4A, 0x63, 0x63, 0x42, 0x7A, 0x38, 0x4E, 0x55, 0x76, 0x58, 0x74, 0x37, 0x79, 0x2B, 0x43, 0x44, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, + 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x55, 0x33, 0x6D, 0x2F, 0x57, 0x71, 0x6F, 0x72, 0x53, 0x73, 0x39, 0x55, 0x67, 0x4F, 0x48, 0x59, 0x6D, 0x38, + 0x43, 0x64, 0x38, 0x72, 0x49, 0x44, 0x5A, 0x73, 0x73, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, + 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x0A, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, + 0x46, 0x7A, 0x55, 0x66, 0x41, 0x33, 0x50, 0x39, 0x77, 0x46, 0x39, 0x51, 0x5A, 0x6C, 0x6C, 0x44, 0x48, 0x50, 0x46, 0x55, 0x70, 0x2F, 0x4C, 0x2B, 0x4D, 0x2B, 0x5A, 0x42, 0x6E, 0x38, 0x62, 0x32, 0x6B, 0x4D, 0x56, 0x6E, 0x35, 0x34, 0x43, 0x56, + 0x56, 0x65, 0x57, 0x0A, 0x46, 0x50, 0x46, 0x53, 0x50, 0x43, 0x65, 0x48, 0x6C, 0x43, 0x6A, 0x74, 0x48, 0x7A, 0x6F, 0x42, 0x4E, 0x36, 0x4A, 0x32, 0x2F, 0x46, 0x4E, 0x51, 0x77, 0x49, 0x53, 0x62, 0x78, 0x6D, 0x74, 0x4F, 0x75, 0x6F, 0x77, 0x68, + 0x54, 0x36, 0x4B, 0x4F, 0x56, 0x57, 0x4B, 0x52, 0x38, 0x32, 0x6B, 0x56, 0x32, 0x4C, 0x79, 0x49, 0x34, 0x38, 0x53, 0x71, 0x43, 0x2F, 0x33, 0x76, 0x71, 0x4F, 0x6C, 0x4C, 0x56, 0x53, 0x6F, 0x47, 0x49, 0x47, 0x31, 0x56, 0x65, 0x43, 0x6B, 0x5A, + 0x0A, 0x37, 0x6C, 0x38, 0x77, 0x58, 0x45, 0x73, 0x6B, 0x45, 0x56, 0x58, 0x2F, 0x4A, 0x4A, 0x70, 0x75, 0x58, 0x69, 0x6F, 0x72, 0x37, 0x67, 0x74, 0x4E, 0x6E, 0x33, 0x2F, 0x33, 0x41, 0x54, 0x69, 0x55, 0x46, 0x4A, 0x56, 0x44, 0x42, 0x77, 0x6E, + 0x37, 0x59, 0x4B, 0x6E, 0x75, 0x48, 0x4B, 0x73, 0x53, 0x6A, 0x4B, 0x43, 0x61, 0x58, 0x71, 0x65, 0x59, 0x61, 0x6C, 0x6C, 0x74, 0x69, 0x7A, 0x38, 0x49, 0x2B, 0x38, 0x6A, 0x52, 0x52, 0x61, 0x38, 0x59, 0x46, 0x57, 0x53, 0x51, 0x0A, 0x45, 0x67, + 0x39, 0x7A, 0x4B, 0x43, 0x37, 0x46, 0x34, 0x69, 0x52, 0x4F, 0x2F, 0x46, 0x6A, 0x73, 0x38, 0x50, 0x52, 0x46, 0x2F, 0x69, 0x4B, 0x7A, 0x36, 0x79, 0x2B, 0x4F, 0x30, 0x74, 0x6C, 0x46, 0x59, 0x51, 0x58, 0x42, 0x6C, 0x32, 0x2B, 0x6F, 0x64, 0x6E, + 0x4B, 0x50, 0x69, 0x34, 0x77, 0x32, 0x72, 0x37, 0x38, 0x4E, 0x42, 0x63, 0x35, 0x78, 0x6A, 0x65, 0x61, 0x6D, 0x62, 0x78, 0x39, 0x73, 0x70, 0x6E, 0x46, 0x69, 0x78, 0x64, 0x6A, 0x51, 0x67, 0x33, 0x49, 0x4D, 0x0A, 0x38, 0x57, 0x63, 0x52, 0x69, + 0x51, 0x79, 0x63, 0x45, 0x30, 0x78, 0x79, 0x4E, 0x4E, 0x2B, 0x38, 0x31, 0x58, 0x48, 0x66, 0x71, 0x6E, 0x48, 0x64, 0x34, 0x62, 0x6C, 0x73, 0x6A, 0x44, 0x77, 0x53, 0x58, 0x57, 0x58, 0x61, 0x76, 0x56, 0x63, 0x53, 0x74, 0x6B, 0x4E, 0x72, 0x2F, + 0x2B, 0x58, 0x65, 0x54, 0x57, 0x59, 0x52, 0x55, 0x63, 0x2B, 0x5A, 0x72, 0x75, 0x77, 0x58, 0x74, 0x75, 0x68, 0x78, 0x6B, 0x59, 0x7A, 0x65, 0x53, 0x66, 0x37, 0x64, 0x4E, 0x58, 0x47, 0x69, 0x0A, 0x46, 0x53, 0x65, 0x55, 0x48, 0x4D, 0x39, 0x68, + 0x34, 0x79, 0x61, 0x37, 0x62, 0x36, 0x4E, 0x6E, 0x4A, 0x53, 0x46, 0x64, 0x35, 0x74, 0x30, 0x64, 0x43, 0x79, 0x35, 0x6F, 0x47, 0x7A, 0x75, 0x43, 0x72, 0x2B, 0x79, 0x44, 0x5A, 0x34, 0x58, 0x55, 0x6D, 0x46, 0x46, 0x30, 0x73, 0x62, 0x6D, 0x5A, + 0x67, 0x49, 0x6E, 0x2F, 0x66, 0x33, 0x67, 0x5A, 0x58, 0x48, 0x6C, 0x4B, 0x59, 0x43, 0x36, 0x53, 0x51, 0x4B, 0x35, 0x4D, 0x4E, 0x79, 0x6F, 0x73, 0x79, 0x63, 0x64, 0x69, 0x0A, 0x79, 0x41, 0x35, 0x64, 0x39, 0x7A, 0x5A, 0x62, 0x79, 0x75, 0x41, + 0x6C, 0x4A, 0x51, 0x47, 0x30, 0x33, 0x52, 0x6F, 0x48, 0x6E, 0x48, 0x63, 0x41, 0x50, 0x39, 0x44, 0x63, 0x31, 0x65, 0x77, 0x39, 0x31, 0x50, 0x71, 0x37, 0x50, 0x38, 0x79, 0x46, 0x31, 0x6D, 0x39, 0x2F, 0x71, 0x53, 0x33, 0x66, 0x75, 0x51, 0x4C, + 0x33, 0x39, 0x5A, 0x65, 0x61, 0x74, 0x54, 0x58, 0x61, 0x77, 0x32, 0x65, 0x77, 0x68, 0x30, 0x71, 0x70, 0x4B, 0x4A, 0x34, 0x6A, 0x6A, 0x76, 0x39, 0x63, 0x0A, 0x4A, 0x32, 0x76, 0x68, 0x73, 0x45, 0x2F, 0x7A, 0x42, 0x2B, 0x34, 0x41, 0x4C, 0x74, + 0x52, 0x5A, 0x68, 0x38, 0x74, 0x53, 0x51, 0x5A, 0x58, 0x71, 0x39, 0x45, 0x66, 0x58, 0x37, 0x6D, 0x52, 0x42, 0x56, 0x58, 0x79, 0x4E, 0x57, 0x51, 0x4B, 0x56, 0x33, 0x57, 0x4B, 0x64, 0x77, 0x72, 0x6E, 0x75, 0x57, 0x69, 0x68, 0x30, 0x68, 0x4B, + 0x57, 0x62, 0x74, 0x35, 0x44, 0x48, 0x44, 0x41, 0x66, 0x66, 0x39, 0x59, 0x6B, 0x32, 0x64, 0x44, 0x4C, 0x57, 0x4B, 0x4D, 0x47, 0x77, 0x0A, 0x73, 0x41, 0x76, 0x67, 0x6E, 0x45, 0x7A, 0x44, 0x48, 0x4E, 0x62, 0x38, 0x34, 0x32, 0x6D, 0x31, 0x52, + 0x30, 0x61, 0x42, 0x4C, 0x36, 0x4B, 0x43, 0x71, 0x39, 0x4E, 0x6A, 0x52, 0x48, 0x44, 0x45, 0x6A, 0x66, 0x38, 0x74, 0x4D, 0x37, 0x71, 0x74, 0x6A, 0x33, 0x75, 0x31, 0x63, 0x49, 0x69, 0x75, 0x50, 0x68, 0x6E, 0x50, 0x51, 0x43, 0x6A, 0x59, 0x2F, + 0x4D, 0x69, 0x51, 0x75, 0x31, 0x32, 0x5A, 0x49, 0x76, 0x56, 0x53, 0x35, 0x6C, 0x6A, 0x46, 0x48, 0x34, 0x67, 0x78, 0x0A, 0x51, 0x2B, 0x36, 0x49, 0x48, 0x64, 0x66, 0x47, 0x6A, 0x6A, 0x78, 0x44, 0x61, 0x68, 0x32, 0x6E, 0x47, 0x4E, 0x35, 0x39, + 0x50, 0x52, 0x62, 0x78, 0x59, 0x76, 0x6E, 0x4B, 0x6B, 0x4B, 0x6A, 0x39, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, + 0x55, 0x53, 0x45, 0x52, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x45, 0x43, 0x43, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x6A, 0x7A, 0x43, 0x43, 0x41, 0x68, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, + 0x41, 0x67, 0x49, 0x51, 0x58, 0x49, 0x75, 0x5A, 0x78, 0x56, 0x71, 0x55, 0x78, 0x64, 0x4A, 0x78, 0x56, 0x74, 0x37, 0x4E, 0x69, 0x59, 0x44, 0x4D, 0x4A, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, + 0x41, 0x7A, 0x43, 0x42, 0x69, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x0A, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x43, 0x6B, 0x35, + 0x6C, 0x64, 0x79, 0x42, 0x4B, 0x5A, 0x58, 0x4A, 0x7A, 0x5A, 0x58, 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x30, 0x70, 0x6C, 0x63, 0x6E, 0x4E, 0x6C, 0x65, 0x53, 0x42, 0x44, 0x61, 0x58, 0x52, + 0x35, 0x4D, 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x56, 0x55, 0x0A, 0x61, 0x47, 0x55, 0x67, 0x56, 0x56, 0x4E, 0x46, 0x55, 0x6C, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x35, 0x6C, 0x64, 0x48, + 0x64, 0x76, 0x63, 0x6D, 0x73, 0x78, 0x4C, 0x6A, 0x41, 0x73, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4A, 0x56, 0x56, 0x54, 0x52, 0x56, 0x4A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x51, 0x32, + 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x0A, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x41, 0x77, 0x4D, + 0x6A, 0x41, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x34, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x43, 0x42, 0x69, 0x44, 0x45, 0x4C, 0x4D, + 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x0A, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x43, 0x6B, 0x35, 0x6C, 0x64, 0x79, 0x42, 0x4B, 0x5A, 0x58, 0x4A, 0x7A, + 0x5A, 0x58, 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x30, 0x70, 0x6C, 0x63, 0x6E, 0x4E, 0x6C, 0x65, 0x53, 0x42, 0x44, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x52, 0x34, 0x77, 0x48, 0x41, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x56, 0x55, 0x0A, 0x61, 0x47, 0x55, 0x67, 0x56, 0x56, 0x4E, 0x46, 0x55, 0x6C, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x35, 0x6C, 0x64, 0x48, 0x64, 0x76, 0x63, 0x6D, 0x73, 0x78, 0x4C, 0x6A, 0x41, + 0x73, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4A, 0x56, 0x56, 0x54, 0x52, 0x56, 0x4A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, + 0x68, 0x64, 0x47, 0x6C, 0x76, 0x0A, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, + 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x51, 0x61, 0x72, 0x46, 0x52, 0x61, 0x71, 0x66, 0x6C, 0x6F, 0x49, 0x2B, 0x64, 0x36, 0x31, 0x53, 0x52, 0x76, 0x55, 0x38, 0x5A, 0x61, 0x32, 0x45, 0x75, 0x72, 0x78, 0x74, + 0x57, 0x32, 0x0A, 0x30, 0x65, 0x5A, 0x7A, 0x63, 0x61, 0x37, 0x64, 0x6E, 0x4E, 0x59, 0x4D, 0x59, 0x66, 0x33, 0x62, 0x6F, 0x49, 0x6B, 0x44, 0x75, 0x41, 0x55, 0x55, 0x37, 0x46, 0x66, 0x4F, 0x37, 0x6C, 0x30, 0x2F, 0x34, 0x69, 0x47, 0x7A, 0x7A, + 0x76, 0x66, 0x55, 0x69, 0x6E, 0x6E, 0x67, 0x6F, 0x34, 0x4E, 0x2B, 0x4C, 0x5A, 0x66, 0x51, 0x59, 0x63, 0x54, 0x78, 0x6D, 0x64, 0x77, 0x6C, 0x6B, 0x57, 0x4F, 0x72, 0x66, 0x7A, 0x43, 0x6A, 0x74, 0x48, 0x44, 0x69, 0x78, 0x36, 0x45, 0x7A, 0x0A, + 0x6E, 0x50, 0x4F, 0x2F, 0x4C, 0x6C, 0x78, 0x54, 0x73, 0x56, 0x2B, 0x7A, 0x66, 0x54, 0x4A, 0x2F, 0x69, 0x6A, 0x54, 0x6A, 0x65, 0x58, 0x6D, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, + 0x42, 0x42, 0x51, 0x36, 0x34, 0x51, 0x6D, 0x47, 0x31, 0x4D, 0x38, 0x5A, 0x77, 0x70, 0x5A, 0x32, 0x64, 0x45, 0x6C, 0x32, 0x33, 0x4F, 0x41, 0x31, 0x78, 0x6D, 0x4E, 0x6A, 0x6D, 0x6A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x38, + 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4B, 0x42, 0x67, 0x67, + 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x41, 0x44, 0x42, 0x6C, 0x41, 0x6A, 0x41, 0x32, 0x5A, 0x36, 0x45, 0x57, 0x43, 0x4E, 0x7A, 0x6B, 0x6C, 0x77, 0x42, 0x42, 0x0A, 0x48, 0x55, 0x36, 0x2B, 0x34, 0x57, + 0x4D, 0x42, 0x7A, 0x7A, 0x75, 0x71, 0x51, 0x68, 0x46, 0x6B, 0x6F, 0x4A, 0x32, 0x55, 0x4F, 0x51, 0x49, 0x52, 0x65, 0x56, 0x78, 0x37, 0x48, 0x66, 0x70, 0x6B, 0x75, 0x65, 0x34, 0x57, 0x51, 0x72, 0x4F, 0x2F, 0x69, 0x73, 0x49, 0x4A, 0x78, 0x4F, + 0x7A, 0x6B, 0x73, 0x55, 0x30, 0x43, 0x4D, 0x51, 0x44, 0x70, 0x4B, 0x6D, 0x46, 0x48, 0x6A, 0x46, 0x4A, 0x4B, 0x53, 0x30, 0x34, 0x59, 0x63, 0x50, 0x62, 0x57, 0x52, 0x4E, 0x5A, 0x75, 0x0A, 0x39, 0x59, 0x4F, 0x36, 0x62, 0x56, 0x69, 0x39, 0x4A, + 0x4E, 0x6C, 0x57, 0x53, 0x4F, 0x72, 0x76, 0x78, 0x4B, 0x4A, 0x47, 0x67, 0x59, 0x68, 0x71, 0x4F, 0x6B, 0x62, 0x52, 0x71, 0x5A, 0x74, 0x4E, 0x79, 0x57, 0x48, 0x61, 0x30, 0x56, 0x31, 0x58, 0x61, 0x68, 0x67, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x52, 0x35, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x48, 0x6A, 0x43, 0x43, 0x41, 0x61, 0x53, 0x67, 0x41, + 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x52, 0x59, 0x46, 0x6C, 0x4A, 0x34, 0x43, 0x59, 0x75, 0x75, 0x31, 0x58, 0x35, 0x43, 0x6E, 0x65, 0x4B, 0x63, 0x66, 0x6C, 0x4B, 0x32, 0x47, 0x77, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, + 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x77, 0x55, 0x44, 0x45, 0x6B, 0x4D, 0x43, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x62, 0x0A, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x46, + 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x46, 0x49, 0x31, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x70, 0x48, 0x62, 0x47, 0x39, 0x69, + 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x45, 0x77, 0x70, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x4D, 0x42, 0x34, + 0x58, 0x44, 0x54, 0x45, 0x79, 0x4D, 0x54, 0x45, 0x78, 0x4D, 0x7A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x34, 0x4D, 0x44, 0x45, 0x78, 0x4F, 0x54, 0x41, 0x7A, 0x4D, 0x54, 0x51, 0x77, 0x4E, 0x31, 0x6F, + 0x77, 0x55, 0x44, 0x45, 0x6B, 0x4D, 0x43, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x62, 0x0A, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, + 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x46, 0x49, 0x31, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x70, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, + 0x64, 0x75, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x45, 0x77, 0x70, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x4D, 0x48, 0x59, 0x77, 0x45, 0x41, 0x59, 0x48, 0x4B, + 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x43, 0x41, 0x51, 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x52, 0x30, 0x55, 0x4F, 0x6C, 0x76, 0x74, 0x39, 0x58, 0x62, 0x2F, 0x70, 0x4F, 0x64, 0x45, 0x68, 0x2B, + 0x4A, 0x38, 0x4C, 0x74, 0x74, 0x56, 0x37, 0x48, 0x70, 0x49, 0x36, 0x0A, 0x53, 0x46, 0x6B, 0x63, 0x38, 0x47, 0x49, 0x78, 0x4C, 0x63, 0x42, 0x36, 0x4B, 0x50, 0x34, 0x61, 0x70, 0x31, 0x79, 0x7A, 0x74, 0x73, 0x79, 0x58, 0x35, 0x30, 0x58, 0x55, + 0x57, 0x50, 0x72, 0x52, 0x64, 0x32, 0x31, 0x44, 0x6F, 0x73, 0x43, 0x48, 0x5A, 0x54, 0x51, 0x4B, 0x48, 0x33, 0x72, 0x64, 0x36, 0x7A, 0x77, 0x7A, 0x6F, 0x63, 0x57, 0x64, 0x54, 0x61, 0x52, 0x76, 0x51, 0x5A, 0x55, 0x34, 0x66, 0x38, 0x6B, 0x65, + 0x68, 0x4F, 0x76, 0x52, 0x6E, 0x6B, 0x6D, 0x53, 0x0A, 0x68, 0x35, 0x53, 0x48, 0x44, 0x44, 0x71, 0x46, 0x53, 0x6D, 0x61, 0x66, 0x6E, 0x56, 0x6D, 0x54, 0x54, 0x5A, 0x64, 0x68, 0x42, 0x6F, 0x5A, 0x4B, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, + 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, + 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x50, 0x65, 0x59, 0x70, 0x53, 0x4A, 0x76, 0x71, 0x42, 0x38, 0x6F, 0x68, 0x52, 0x45, 0x6F, 0x6D, 0x33, 0x6D, 0x37, 0x65, 0x30, 0x6F, + 0x50, 0x51, 0x6E, 0x31, 0x6B, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, 0x77, 0x5A, 0x51, 0x49, 0x78, 0x41, 0x4F, 0x56, 0x70, 0x45, 0x73, 0x6C, 0x75, 0x32, 0x38, + 0x59, 0x78, 0x0A, 0x75, 0x67, 0x6C, 0x42, 0x34, 0x5A, 0x66, 0x34, 0x2B, 0x2F, 0x32, 0x61, 0x34, 0x6E, 0x30, 0x53, 0x79, 0x65, 0x31, 0x38, 0x5A, 0x4E, 0x50, 0x4C, 0x42, 0x53, 0x57, 0x4C, 0x56, 0x74, 0x6D, 0x67, 0x35, 0x31, 0x35, 0x64, 0x54, + 0x67, 0x75, 0x44, 0x6E, 0x46, 0x74, 0x32, 0x4B, 0x61, 0x41, 0x4A, 0x4A, 0x69, 0x46, 0x71, 0x59, 0x67, 0x49, 0x77, 0x63, 0x64, 0x4B, 0x31, 0x6A, 0x31, 0x7A, 0x71, 0x4F, 0x2B, 0x46, 0x34, 0x43, 0x59, 0x57, 0x6F, 0x64, 0x5A, 0x49, 0x37, 0x0A, + 0x79, 0x46, 0x7A, 0x39, 0x53, 0x4F, 0x38, 0x4E, 0x64, 0x43, 0x4B, 0x6F, 0x43, 0x4F, 0x4A, 0x75, 0x78, 0x55, 0x6E, 0x4F, 0x78, 0x77, 0x79, 0x38, 0x70, 0x32, 0x46, 0x70, 0x38, 0x66, 0x63, 0x37, 0x34, 0x53, 0x72, 0x4C, 0x2B, 0x53, 0x76, 0x7A, + 0x5A, 0x70, 0x41, 0x33, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x49, 0x64, 0x65, 0x6E, 0x54, 0x72, 0x75, 0x73, + 0x74, 0x20, 0x43, 0x6F, 0x6D, 0x6D, 0x65, 0x72, 0x63, 0x69, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x59, 0x44, 0x43, 0x43, 0x41, 0x30, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x67, 0x46, 0x43, 0x67, 0x41, 0x41, 0x41, 0x41, 0x55, 0x55, 0x6A, 0x79, 0x45, 0x53, 0x31, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x4B, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, + 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x53, 0x57, 0x52, 0x6C, 0x62, 0x6C, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, + 0x51, 0x51, 0x44, 0x45, 0x78, 0x35, 0x4A, 0x5A, 0x47, 0x56, 0x75, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x51, 0x32, 0x39, 0x74, 0x62, 0x57, 0x56, 0x79, 0x59, 0x32, 0x6C, 0x68, 0x62, 0x43, 0x42, 0x53, 0x0A, 0x62, 0x32, 0x39, 0x30, + 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x51, 0x77, 0x4D, 0x54, 0x45, 0x32, 0x4D, 0x54, 0x67, 0x78, 0x4D, 0x6A, 0x49, 0x7A, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x51, 0x77, 0x4D, 0x54, 0x45, 0x32, + 0x4D, 0x54, 0x67, 0x78, 0x4D, 0x6A, 0x49, 0x7A, 0x57, 0x6A, 0x42, 0x4B, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x53, 0x0A, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x53, 0x57, 0x52, 0x6C, 0x62, 0x6C, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x35, 0x4A, 0x5A, 0x47, 0x56, 0x75, 0x56, 0x48, 0x4A, + 0x31, 0x63, 0x33, 0x51, 0x67, 0x51, 0x32, 0x39, 0x74, 0x62, 0x57, 0x56, 0x79, 0x59, 0x32, 0x6C, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x0A, 0x49, 0x44, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, + 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x6E, 0x55, 0x42, + 0x6E, 0x65, 0x50, 0x35, 0x6B, 0x39, 0x31, 0x44, 0x4E, 0x47, 0x38, 0x57, 0x39, 0x52, 0x59, 0x59, 0x4B, 0x79, 0x71, 0x55, 0x2B, 0x50, 0x5A, 0x34, 0x6C, 0x64, 0x0A, 0x68, 0x4E, 0x6C, 0x54, 0x33, 0x51, 0x77, 0x6F, 0x32, 0x64, 0x66, 0x77, 0x2F, + 0x36, 0x36, 0x56, 0x51, 0x33, 0x4B, 0x5A, 0x2B, 0x62, 0x56, 0x64, 0x66, 0x49, 0x72, 0x42, 0x51, 0x75, 0x45, 0x78, 0x55, 0x48, 0x54, 0x52, 0x67, 0x51, 0x31, 0x38, 0x7A, 0x5A, 0x73, 0x68, 0x71, 0x30, 0x50, 0x69, 0x72, 0x4B, 0x31, 0x65, 0x68, + 0x6D, 0x37, 0x7A, 0x43, 0x59, 0x6F, 0x66, 0x57, 0x6A, 0x4B, 0x39, 0x6F, 0x75, 0x75, 0x55, 0x2B, 0x65, 0x68, 0x63, 0x43, 0x75, 0x7A, 0x2F, 0x0A, 0x6D, 0x4E, 0x4B, 0x76, 0x63, 0x62, 0x4F, 0x30, 0x55, 0x35, 0x39, 0x4F, 0x68, 0x2B, 0x2B, 0x53, + 0x76, 0x4C, 0x33, 0x73, 0x54, 0x7A, 0x49, 0x77, 0x69, 0x45, 0x73, 0x58, 0x58, 0x6C, 0x66, 0x45, 0x55, 0x38, 0x4C, 0x32, 0x41, 0x70, 0x65, 0x4E, 0x32, 0x57, 0x49, 0x72, 0x76, 0x79, 0x51, 0x66, 0x59, 0x6F, 0x33, 0x66, 0x77, 0x37, 0x67, 0x70, + 0x53, 0x30, 0x6C, 0x34, 0x50, 0x4A, 0x4E, 0x67, 0x69, 0x43, 0x4C, 0x38, 0x6D, 0x64, 0x6F, 0x32, 0x79, 0x4D, 0x4B, 0x69, 0x0A, 0x31, 0x43, 0x78, 0x55, 0x41, 0x47, 0x63, 0x31, 0x62, 0x6E, 0x4F, 0x2F, 0x41, 0x6C, 0x6A, 0x77, 0x70, 0x4E, 0x33, + 0x6C, 0x73, 0x4B, 0x49, 0x6D, 0x65, 0x73, 0x72, 0x67, 0x4E, 0x71, 0x55, 0x5A, 0x46, 0x76, 0x58, 0x39, 0x74, 0x2B, 0x2B, 0x75, 0x50, 0x30, 0x44, 0x31, 0x62, 0x56, 0x6F, 0x45, 0x2F, 0x63, 0x34, 0x30, 0x79, 0x69, 0x54, 0x63, 0x64, 0x43, 0x4D, + 0x62, 0x58, 0x54, 0x4D, 0x54, 0x45, 0x6C, 0x33, 0x45, 0x41, 0x53, 0x58, 0x32, 0x4D, 0x4E, 0x30, 0x43, 0x0A, 0x58, 0x5A, 0x2F, 0x67, 0x31, 0x55, 0x65, 0x39, 0x74, 0x4F, 0x73, 0x62, 0x6F, 0x62, 0x74, 0x4A, 0x53, 0x64, 0x69, 0x66, 0x57, 0x77, + 0x4C, 0x7A, 0x69, 0x75, 0x51, 0x6B, 0x6B, 0x4F, 0x52, 0x69, 0x54, 0x30, 0x2F, 0x42, 0x72, 0x34, 0x73, 0x4F, 0x64, 0x42, 0x65, 0x6F, 0x30, 0x58, 0x4B, 0x49, 0x61, 0x6E, 0x6F, 0x42, 0x53, 0x63, 0x79, 0x30, 0x52, 0x6E, 0x6E, 0x47, 0x46, 0x37, + 0x48, 0x61, 0x6D, 0x42, 0x34, 0x48, 0x57, 0x66, 0x70, 0x31, 0x49, 0x59, 0x56, 0x6C, 0x0A, 0x33, 0x5A, 0x42, 0x57, 0x7A, 0x76, 0x75, 0x72, 0x70, 0x57, 0x43, 0x64, 0x78, 0x4A, 0x33, 0x35, 0x55, 0x72, 0x43, 0x4C, 0x76, 0x59, 0x66, 0x35, 0x6A, + 0x79, 0x73, 0x6A, 0x43, 0x69, 0x4E, 0x32, 0x4F, 0x2F, 0x63, 0x7A, 0x34, 0x63, 0x6B, 0x41, 0x38, 0x32, 0x6E, 0x35, 0x53, 0x36, 0x4C, 0x67, 0x54, 0x72, 0x78, 0x2B, 0x6B, 0x7A, 0x6D, 0x45, 0x42, 0x2F, 0x64, 0x45, 0x63, 0x48, 0x37, 0x2B, 0x42, + 0x31, 0x72, 0x6C, 0x73, 0x61, 0x7A, 0x52, 0x47, 0x4D, 0x7A, 0x79, 0x0A, 0x4E, 0x65, 0x56, 0x4A, 0x53, 0x51, 0x6A, 0x4B, 0x56, 0x73, 0x6B, 0x39, 0x2B, 0x77, 0x38, 0x59, 0x66, 0x59, 0x73, 0x37, 0x77, 0x52, 0x50, 0x43, 0x54, 0x59, 0x2F, 0x4A, + 0x54, 0x77, 0x34, 0x33, 0x36, 0x52, 0x2B, 0x68, 0x44, 0x6D, 0x72, 0x66, 0x59, 0x69, 0x37, 0x4C, 0x4E, 0x51, 0x5A, 0x52, 0x65, 0x53, 0x7A, 0x49, 0x4A, 0x54, 0x6A, 0x30, 0x2B, 0x6B, 0x75, 0x6E, 0x69, 0x56, 0x79, 0x63, 0x30, 0x75, 0x4D, 0x4E, + 0x4F, 0x59, 0x5A, 0x4B, 0x64, 0x48, 0x7A, 0x56, 0x0A, 0x57, 0x59, 0x66, 0x43, 0x50, 0x30, 0x34, 0x4D, 0x58, 0x46, 0x4C, 0x30, 0x50, 0x66, 0x64, 0x53, 0x67, 0x76, 0x48, 0x71, 0x6F, 0x36, 0x7A, 0x39, 0x53, 0x54, 0x51, 0x61, 0x4B, 0x50, 0x4E, + 0x42, 0x69, 0x44, 0x6F, 0x54, 0x37, 0x75, 0x6A, 0x65, 0x2F, 0x35, 0x6B, 0x64, 0x58, 0x37, 0x72, 0x4C, 0x36, 0x42, 0x37, 0x79, 0x75, 0x56, 0x42, 0x67, 0x77, 0x44, 0x48, 0x54, 0x63, 0x2B, 0x58, 0x76, 0x76, 0x71, 0x44, 0x74, 0x4D, 0x77, 0x74, + 0x30, 0x76, 0x69, 0x41, 0x67, 0x0A, 0x78, 0x47, 0x64, 0x73, 0x38, 0x41, 0x67, 0x44, 0x65, 0x6C, 0x57, 0x41, 0x66, 0x30, 0x5A, 0x4F, 0x6C, 0x71, 0x66, 0x30, 0x48, 0x6A, 0x37, 0x68, 0x39, 0x74, 0x67, 0x4A, 0x34, 0x54, 0x4E, 0x6B, 0x4B, 0x32, + 0x50, 0x58, 0x4D, 0x6C, 0x36, 0x66, 0x2B, 0x63, 0x42, 0x37, 0x44, 0x33, 0x68, 0x76, 0x6C, 0x37, 0x79, 0x54, 0x6D, 0x76, 0x6D, 0x63, 0x45, 0x70, 0x42, 0x34, 0x65, 0x6F, 0x43, 0x48, 0x46, 0x64, 0x64, 0x79, 0x64, 0x4A, 0x78, 0x56, 0x64, 0x48, + 0x69, 0x78, 0x0A, 0x75, 0x75, 0x46, 0x75, 0x63, 0x41, 0x53, 0x36, 0x54, 0x36, 0x43, 0x36, 0x61, 0x4D, 0x4E, 0x37, 0x2F, 0x7A, 0x48, 0x77, 0x63, 0x7A, 0x30, 0x39, 0x6C, 0x43, 0x71, 0x78, 0x43, 0x30, 0x45, 0x4F, 0x6F, 0x50, 0x35, 0x4E, 0x69, + 0x47, 0x56, 0x72, 0x65, 0x54, 0x4F, 0x30, 0x31, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x0A, + 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, + 0x37, 0x55, 0x51, 0x5A, 0x77, 0x4E, 0x50, 0x77, 0x42, 0x6F, 0x76, 0x75, 0x70, 0x48, 0x75, 0x2B, 0x51, 0x75, 0x63, 0x6D, 0x56, 0x4D, 0x69, 0x4F, 0x4E, 0x6E, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, + 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x41, 0x32, 0x75, 0x6B, 0x44, 0x4C, 0x32, 0x70, 0x6B, 0x74, 0x38, 0x52, 0x48, 0x59, 0x5A, 0x59, 0x52, 0x34, 0x6E, 0x4B, 0x4D, 0x31, 0x65, 0x56, 0x4F, 0x38, + 0x6C, 0x76, 0x4F, 0x4D, 0x49, 0x6B, 0x50, 0x6B, 0x70, 0x31, 0x36, 0x35, 0x6F, 0x43, 0x4F, 0x47, 0x55, 0x41, 0x46, 0x6A, 0x76, 0x4C, 0x69, 0x35, 0x2B, 0x55, 0x31, 0x4B, 0x4D, 0x74, 0x6C, 0x77, 0x48, 0x0A, 0x36, 0x6F, 0x69, 0x36, 0x6D, 0x59, + 0x74, 0x51, 0x6C, 0x4E, 0x65, 0x43, 0x67, 0x4E, 0x39, 0x68, 0x43, 0x51, 0x43, 0x54, 0x72, 0x51, 0x30, 0x55, 0x35, 0x73, 0x37, 0x42, 0x38, 0x6A, 0x65, 0x55, 0x65, 0x4C, 0x42, 0x66, 0x6E, 0x4C, 0x4F, 0x69, 0x63, 0x37, 0x69, 0x50, 0x42, 0x5A, + 0x4D, 0x34, 0x7A, 0x59, 0x30, 0x2B, 0x73, 0x4C, 0x6A, 0x37, 0x77, 0x4D, 0x2B, 0x78, 0x38, 0x75, 0x77, 0x74, 0x4C, 0x52, 0x76, 0x4D, 0x37, 0x4B, 0x71, 0x61, 0x73, 0x36, 0x70, 0x67, 0x0A, 0x67, 0x68, 0x73, 0x74, 0x4F, 0x38, 0x4F, 0x45, 0x50, + 0x56, 0x65, 0x4B, 0x6C, 0x68, 0x36, 0x63, 0x64, 0x62, 0x6A, 0x54, 0x4D, 0x4D, 0x31, 0x67, 0x43, 0x49, 0x4F, 0x51, 0x30, 0x34, 0x35, 0x55, 0x38, 0x55, 0x31, 0x6D, 0x77, 0x46, 0x31, 0x30, 0x41, 0x30, 0x43, 0x6A, 0x37, 0x6F, 0x56, 0x2B, 0x77, + 0x68, 0x39, 0x33, 0x6E, 0x41, 0x62, 0x6F, 0x77, 0x61, 0x63, 0x59, 0x58, 0x56, 0x4B, 0x56, 0x37, 0x63, 0x6E, 0x64, 0x4A, 0x5A, 0x35, 0x74, 0x2B, 0x71, 0x6E, 0x74, 0x0A, 0x6F, 0x7A, 0x6F, 0x30, 0x30, 0x46, 0x6C, 0x37, 0x32, 0x75, 0x31, 0x51, + 0x38, 0x7A, 0x57, 0x2F, 0x37, 0x65, 0x73, 0x55, 0x54, 0x54, 0x48, 0x48, 0x59, 0x50, 0x54, 0x61, 0x38, 0x59, 0x65, 0x63, 0x34, 0x6B, 0x6A, 0x69, 0x78, 0x73, 0x55, 0x33, 0x2B, 0x77, 0x59, 0x51, 0x2B, 0x6E, 0x56, 0x5A, 0x5A, 0x6A, 0x46, 0x48, + 0x4B, 0x64, 0x70, 0x32, 0x6D, 0x68, 0x7A, 0x70, 0x67, 0x71, 0x37, 0x76, 0x6D, 0x72, 0x6C, 0x52, 0x39, 0x34, 0x67, 0x6A, 0x6D, 0x6D, 0x6D, 0x56, 0x0A, 0x59, 0x6A, 0x7A, 0x6C, 0x56, 0x59, 0x41, 0x32, 0x31, 0x31, 0x51, 0x43, 0x2F, 0x2F, 0x47, + 0x35, 0x58, 0x63, 0x37, 0x55, 0x49, 0x32, 0x2F, 0x59, 0x52, 0x59, 0x52, 0x4B, 0x57, 0x32, 0x58, 0x76, 0x69, 0x51, 0x7A, 0x64, 0x46, 0x4B, 0x63, 0x67, 0x79, 0x78, 0x69, 0x6C, 0x4A, 0x62, 0x51, 0x4E, 0x2B, 0x51, 0x48, 0x77, 0x6F, 0x74, 0x4C, + 0x30, 0x41, 0x4D, 0x68, 0x30, 0x6A, 0x71, 0x45, 0x71, 0x53, 0x49, 0x35, 0x6C, 0x32, 0x78, 0x50, 0x45, 0x34, 0x69, 0x55, 0x58, 0x0A, 0x66, 0x65, 0x75, 0x2B, 0x68, 0x31, 0x73, 0x58, 0x49, 0x46, 0x52, 0x52, 0x6B, 0x30, 0x70, 0x54, 0x41, 0x77, + 0x76, 0x73, 0x58, 0x63, 0x6F, 0x7A, 0x37, 0x57, 0x4C, 0x39, 0x52, 0x63, 0x63, 0x76, 0x57, 0x39, 0x78, 0x59, 0x6F, 0x49, 0x41, 0x35, 0x35, 0x76, 0x72, 0x58, 0x2F, 0x68, 0x4D, 0x55, 0x70, 0x75, 0x30, 0x39, 0x6C, 0x45, 0x70, 0x43, 0x64, 0x4E, + 0x54, 0x44, 0x64, 0x31, 0x6C, 0x7A, 0x7A, 0x59, 0x39, 0x47, 0x76, 0x6C, 0x55, 0x34, 0x37, 0x2F, 0x72, 0x6F, 0x0A, 0x6B, 0x54, 0x4C, 0x71, 0x6C, 0x31, 0x67, 0x45, 0x49, 0x74, 0x34, 0x34, 0x77, 0x38, 0x79, 0x38, 0x62, 0x63, 0x6B, 0x7A, 0x4F, + 0x6D, 0x6F, 0x4B, 0x61, 0x54, 0x2B, 0x67, 0x79, 0x4F, 0x70, 0x79, 0x6A, 0x34, 0x78, 0x6A, 0x68, 0x69, 0x4F, 0x39, 0x62, 0x54, 0x79, 0x57, 0x6E, 0x70, 0x58, 0x67, 0x53, 0x55, 0x79, 0x71, 0x6F, 0x72, 0x6B, 0x71, 0x47, 0x35, 0x77, 0x32, 0x67, + 0x58, 0x6A, 0x74, 0x77, 0x2B, 0x68, 0x47, 0x34, 0x69, 0x5A, 0x5A, 0x52, 0x48, 0x55, 0x65, 0x0A, 0x32, 0x58, 0x57, 0x4A, 0x55, 0x63, 0x30, 0x51, 0x68, 0x4A, 0x31, 0x68, 0x59, 0x4D, 0x74, 0x64, 0x2B, 0x5A, 0x63, 0x69, 0x54, 0x59, 0x36, 0x59, + 0x35, 0x75, 0x4E, 0x2F, 0x39, 0x6C, 0x75, 0x37, 0x72, 0x73, 0x33, 0x4B, 0x53, 0x6F, 0x46, 0x72, 0x58, 0x67, 0x76, 0x7A, 0x55, 0x65, 0x46, 0x30, 0x4B, 0x2B, 0x6C, 0x2B, 0x4A, 0x36, 0x66, 0x5A, 0x6D, 0x55, 0x6C, 0x4F, 0x2B, 0x4B, 0x57, 0x41, + 0x32, 0x79, 0x55, 0x50, 0x48, 0x47, 0x4E, 0x69, 0x69, 0x73, 0x6B, 0x7A, 0x0A, 0x5A, 0x32, 0x73, 0x38, 0x45, 0x49, 0x50, 0x47, 0x72, 0x64, 0x36, 0x6F, 0x7A, 0x52, 0x61, 0x4F, 0x6A, 0x66, 0x41, 0x48, 0x4E, 0x33, 0x47, 0x66, 0x38, 0x71, 0x76, + 0x38, 0x51, 0x66, 0x58, 0x42, 0x69, 0x2B, 0x77, 0x41, 0x4E, 0x31, 0x30, 0x4A, 0x35, 0x55, 0x36, 0x41, 0x37, 0x2F, 0x71, 0x78, 0x58, 0x44, 0x67, 0x47, 0x70, 0x52, 0x74, 0x4B, 0x34, 0x64, 0x77, 0x34, 0x4C, 0x54, 0x7A, 0x63, 0x71, 0x78, 0x2B, + 0x51, 0x47, 0x74, 0x56, 0x4B, 0x6E, 0x4F, 0x37, 0x52, 0x0A, 0x63, 0x47, 0x7A, 0x4D, 0x37, 0x76, 0x52, 0x58, 0x2B, 0x42, 0x69, 0x36, 0x68, 0x47, 0x36, 0x48, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x49, 0x64, 0x65, 0x6E, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x50, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x53, 0x65, 0x63, 0x74, 0x6F, 0x72, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x5A, 0x6A, 0x43, 0x43, 0x41, 0x30, + 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x67, 0x46, 0x43, 0x67, 0x41, 0x41, 0x41, 0x41, 0x55, 0x55, 0x6A, 0x7A, 0x30, 0x5A, 0x38, 0x41, 0x41, 0x41, 0x41, 0x41, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, + 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x4E, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x53, 0x57, 0x52, 0x6C, 0x62, 0x6C, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x53, 0x6F, 0x77, 0x4B, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x46, 0x4A, 0x5A, 0x47, 0x56, 0x75, 0x56, + 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x48, 0x56, 0x69, 0x62, 0x47, 0x6C, 0x6A, 0x49, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x52, 0x76, 0x0A, 0x63, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x45, 0x77, + 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x51, 0x77, 0x4D, 0x54, 0x45, 0x32, 0x4D, 0x54, 0x63, 0x31, 0x4D, 0x7A, 0x4D, 0x79, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x51, 0x77, 0x4D, 0x54, 0x45, 0x32, 0x4D, 0x54, 0x63, 0x31, 0x4D, 0x7A, 0x4D, 0x79, + 0x57, 0x6A, 0x42, 0x4E, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x53, 0x57, 0x52, + 0x6C, 0x62, 0x6C, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4D, 0x53, 0x6F, 0x77, 0x4B, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x46, 0x4A, 0x5A, 0x47, 0x56, 0x75, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x48, 0x56, + 0x69, 0x62, 0x47, 0x6C, 0x6A, 0x49, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x52, 0x76, 0x63, 0x69, 0x42, 0x53, 0x0A, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, + 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x32, 0x49, 0x70, 0x54, 0x38, 0x70, 0x45, + 0x69, 0x76, 0x36, 0x45, 0x64, 0x72, 0x43, 0x76, 0x73, 0x6E, 0x64, 0x75, 0x54, 0x79, 0x0A, 0x50, 0x34, 0x6F, 0x37, 0x65, 0x6B, 0x6F, 0x73, 0x4D, 0x53, 0x71, 0x4D, 0x6A, 0x62, 0x43, 0x70, 0x77, 0x7A, 0x46, 0x72, 0x71, 0x48, 0x64, 0x32, 0x68, + 0x43, 0x61, 0x32, 0x72, 0x49, 0x46, 0x43, 0x44, 0x51, 0x6A, 0x72, 0x56, 0x56, 0x69, 0x37, 0x65, 0x76, 0x69, 0x38, 0x5A, 0x58, 0x33, 0x79, 0x6F, 0x47, 0x32, 0x4C, 0x71, 0x45, 0x66, 0x70, 0x59, 0x6E, 0x59, 0x65, 0x45, 0x65, 0x34, 0x49, 0x46, + 0x4E, 0x47, 0x79, 0x52, 0x42, 0x62, 0x30, 0x36, 0x74, 0x44, 0x36, 0x0A, 0x48, 0x69, 0x39, 0x65, 0x32, 0x38, 0x74, 0x7A, 0x51, 0x61, 0x36, 0x38, 0x41, 0x4C, 0x42, 0x4B, 0x4B, 0x30, 0x43, 0x79, 0x72, 0x4F, 0x45, 0x37, 0x53, 0x38, 0x49, 0x74, + 0x6E, 0x65, 0x53, 0x68, 0x6D, 0x2B, 0x77, 0x61, 0x4F, 0x68, 0x37, 0x77, 0x43, 0x4C, 0x50, 0x51, 0x35, 0x43, 0x51, 0x31, 0x42, 0x35, 0x2B, 0x63, 0x74, 0x4D, 0x6C, 0x53, 0x62, 0x64, 0x73, 0x48, 0x79, 0x6F, 0x2B, 0x31, 0x57, 0x2F, 0x43, 0x44, + 0x38, 0x30, 0x2F, 0x48, 0x4C, 0x61, 0x58, 0x49, 0x0A, 0x72, 0x63, 0x75, 0x56, 0x49, 0x4B, 0x51, 0x78, 0x4B, 0x46, 0x64, 0x59, 0x57, 0x75, 0x53, 0x4E, 0x47, 0x35, 0x71, 0x72, 0x6E, 0x67, 0x30, 0x4D, 0x38, 0x67, 0x6F, 0x7A, 0x4F, 0x53, 0x49, + 0x35, 0x43, 0x70, 0x63, 0x75, 0x38, 0x31, 0x4E, 0x33, 0x75, 0x55, 0x52, 0x46, 0x2F, 0x59, 0x54, 0x4C, 0x4E, 0x69, 0x43, 0x42, 0x57, 0x53, 0x32, 0x61, 0x62, 0x32, 0x31, 0x49, 0x53, 0x47, 0x48, 0x4B, 0x54, 0x4E, 0x39, 0x54, 0x30, 0x61, 0x39, + 0x53, 0x76, 0x45, 0x53, 0x66, 0x0A, 0x71, 0x79, 0x39, 0x72, 0x67, 0x33, 0x4C, 0x76, 0x64, 0x59, 0x44, 0x61, 0x42, 0x6A, 0x4D, 0x62, 0x58, 0x63, 0x6A, 0x61, 0x59, 0x38, 0x5A, 0x4E, 0x7A, 0x61, 0x78, 0x6D, 0x4D, 0x63, 0x33, 0x52, 0x33, 0x6A, + 0x36, 0x48, 0x45, 0x44, 0x62, 0x68, 0x75, 0x61, 0x52, 0x36, 0x37, 0x32, 0x42, 0x51, 0x73, 0x73, 0x76, 0x4B, 0x70, 0x6C, 0x62, 0x67, 0x4E, 0x36, 0x2B, 0x72, 0x4E, 0x42, 0x4D, 0x35, 0x4A, 0x65, 0x67, 0x35, 0x5A, 0x75, 0x53, 0x59, 0x65, 0x71, + 0x6F, 0x53, 0x0A, 0x6D, 0x4A, 0x78, 0x5A, 0x5A, 0x6F, 0x59, 0x2B, 0x72, 0x66, 0x47, 0x77, 0x79, 0x6A, 0x34, 0x47, 0x44, 0x33, 0x76, 0x77, 0x45, 0x55, 0x73, 0x33, 0x6F, 0x45, 0x52, 0x74, 0x65, 0x38, 0x75, 0x6F, 0x6A, 0x48, 0x48, 0x30, 0x31, + 0x62, 0x57, 0x52, 0x4E, 0x73, 0x7A, 0x77, 0x46, 0x63, 0x59, 0x72, 0x33, 0x6C, 0x45, 0x58, 0x73, 0x5A, 0x64, 0x4D, 0x55, 0x44, 0x32, 0x78, 0x6C, 0x56, 0x6C, 0x38, 0x42, 0x58, 0x30, 0x74, 0x49, 0x64, 0x55, 0x41, 0x76, 0x77, 0x46, 0x6E, 0x0A, + 0x6F, 0x6C, 0x35, 0x37, 0x70, 0x6C, 0x7A, 0x79, 0x39, 0x79, 0x4C, 0x78, 0x6B, 0x41, 0x32, 0x54, 0x32, 0x36, 0x70, 0x45, 0x55, 0x57, 0x62, 0x4D, 0x66, 0x58, 0x59, 0x44, 0x36, 0x32, 0x71, 0x6F, 0x4B, 0x6A, 0x67, 0x5A, 0x6C, 0x33, 0x59, 0x4E, + 0x61, 0x34, 0x70, 0x68, 0x2B, 0x62, 0x7A, 0x32, 0x37, 0x6E, 0x62, 0x39, 0x63, 0x43, 0x76, 0x64, 0x4B, 0x54, 0x7A, 0x34, 0x43, 0x68, 0x35, 0x62, 0x51, 0x68, 0x79, 0x4C, 0x56, 0x69, 0x39, 0x56, 0x47, 0x78, 0x79, 0x68, 0x0A, 0x4C, 0x72, 0x58, + 0x48, 0x46, 0x75, 0x62, 0x34, 0x71, 0x6A, 0x79, 0x53, 0x6A, 0x6D, 0x6D, 0x32, 0x41, 0x63, 0x47, 0x31, 0x68, 0x70, 0x32, 0x4A, 0x44, 0x77, 0x73, 0x34, 0x6C, 0x46, 0x54, 0x6F, 0x36, 0x74, 0x79, 0x65, 0x50, 0x53, 0x57, 0x38, 0x55, 0x79, 0x62, + 0x74, 0x31, 0x61, 0x73, 0x35, 0x71, 0x73, 0x56, 0x41, 0x54, 0x46, 0x53, 0x72, 0x73, 0x72, 0x54, 0x5A, 0x32, 0x66, 0x6A, 0x58, 0x63, 0x74, 0x73, 0x63, 0x76, 0x47, 0x32, 0x39, 0x5A, 0x56, 0x2F, 0x76, 0x0A, 0x69, 0x44, 0x55, 0x71, 0x5A, 0x69, + 0x2F, 0x75, 0x39, 0x72, 0x4E, 0x6C, 0x38, 0x44, 0x4F, 0x4E, 0x66, 0x4A, 0x68, 0x42, 0x61, 0x55, 0x59, 0x50, 0x51, 0x78, 0x78, 0x70, 0x2B, 0x70, 0x75, 0x31, 0x30, 0x47, 0x46, 0x71, 0x7A, 0x63, 0x70, 0x4C, 0x32, 0x55, 0x79, 0x51, 0x52, 0x71, + 0x73, 0x56, 0x57, 0x61, 0x46, 0x48, 0x56, 0x43, 0x6B, 0x75, 0x67, 0x79, 0x68, 0x66, 0x48, 0x4D, 0x4B, 0x69, 0x71, 0x33, 0x49, 0x58, 0x41, 0x41, 0x61, 0x4F, 0x52, 0x65, 0x79, 0x4C, 0x0A, 0x34, 0x6A, 0x4D, 0x39, 0x66, 0x39, 0x6F, 0x5A, 0x52, + 0x4F, 0x52, 0x69, 0x63, 0x73, 0x50, 0x66, 0x49, 0x73, 0x62, 0x79, 0x56, 0x74, 0x54, 0x64, 0x58, 0x35, 0x56, 0x79, 0x37, 0x57, 0x31, 0x66, 0x39, 0x30, 0x67, 0x44, 0x57, 0x2F, 0x33, 0x46, 0x4B, 0x71, 0x44, 0x32, 0x63, 0x79, 0x4F, 0x45, 0x45, + 0x42, 0x73, 0x42, 0x35, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x0A, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, + 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x34, 0x33, 0x48, 0x67, + 0x6E, 0x74, 0x69, 0x6E, 0x51, 0x74, 0x6E, 0x62, 0x63, 0x5A, 0x46, 0x72, 0x6C, 0x4A, 0x50, 0x72, 0x77, 0x36, 0x50, 0x52, 0x46, 0x4B, 0x4D, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, + 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x45, 0x66, 0x36, 0x33, 0x51, 0x71, 0x77, 0x45, 0x5A, 0x45, 0x34, 0x72, 0x55, 0x31, 0x64, 0x39, 0x2B, 0x55, 0x4F, 0x6C, 0x31, 0x51, 0x5A, 0x67, 0x6B, 0x69, 0x48, 0x56, 0x49, 0x79, + 0x71, 0x5A, 0x4A, 0x6E, 0x59, 0x57, 0x76, 0x36, 0x49, 0x41, 0x63, 0x56, 0x59, 0x70, 0x5A, 0x6D, 0x78, 0x49, 0x31, 0x51, 0x6A, 0x0A, 0x74, 0x32, 0x6F, 0x64, 0x49, 0x46, 0x66, 0x6C, 0x41, 0x57, 0x4A, 0x42, 0x46, 0x39, 0x4D, 0x4A, 0x32, 0x33, + 0x58, 0x4C, 0x62, 0x6C, 0x53, 0x51, 0x64, 0x66, 0x34, 0x61, 0x6E, 0x34, 0x45, 0x4B, 0x77, 0x74, 0x33, 0x58, 0x39, 0x77, 0x6E, 0x51, 0x57, 0x33, 0x49, 0x56, 0x35, 0x42, 0x34, 0x4A, 0x61, 0x6A, 0x30, 0x7A, 0x38, 0x79, 0x47, 0x61, 0x35, 0x68, + 0x56, 0x2B, 0x72, 0x56, 0x48, 0x56, 0x44, 0x52, 0x44, 0x74, 0x66, 0x55, 0x4C, 0x41, 0x6A, 0x2B, 0x37, 0x41, 0x0A, 0x6D, 0x67, 0x6A, 0x56, 0x51, 0x64, 0x5A, 0x63, 0x44, 0x69, 0x46, 0x70, 0x62, 0x6F, 0x42, 0x68, 0x44, 0x68, 0x58, 0x41, 0x75, + 0x4D, 0x2F, 0x46, 0x53, 0x52, 0x4A, 0x53, 0x7A, 0x4C, 0x34, 0x36, 0x7A, 0x4E, 0x51, 0x75, 0x4F, 0x41, 0x58, 0x65, 0x4E, 0x66, 0x30, 0x66, 0x62, 0x37, 0x69, 0x41, 0x61, 0x4A, 0x67, 0x39, 0x54, 0x61, 0x44, 0x4B, 0x51, 0x47, 0x58, 0x53, 0x63, + 0x33, 0x7A, 0x31, 0x69, 0x39, 0x6B, 0x4B, 0x6C, 0x54, 0x2F, 0x59, 0x50, 0x79, 0x4E, 0x74, 0x0A, 0x47, 0x74, 0x45, 0x71, 0x4A, 0x42, 0x6E, 0x5A, 0x68, 0x62, 0x4D, 0x58, 0x37, 0x33, 0x68, 0x75, 0x71, 0x56, 0x6A, 0x52, 0x49, 0x39, 0x50, 0x48, + 0x45, 0x2B, 0x31, 0x79, 0x4A, 0x58, 0x39, 0x64, 0x73, 0x58, 0x4E, 0x77, 0x30, 0x48, 0x38, 0x47, 0x6C, 0x77, 0x6D, 0x45, 0x4B, 0x59, 0x42, 0x68, 0x48, 0x66, 0x70, 0x65, 0x2F, 0x33, 0x4F, 0x73, 0x6F, 0x4F, 0x4F, 0x4A, 0x75, 0x42, 0x78, 0x78, + 0x46, 0x63, 0x62, 0x65, 0x4D, 0x58, 0x38, 0x53, 0x33, 0x4F, 0x46, 0x74, 0x0A, 0x6D, 0x36, 0x2F, 0x6E, 0x36, 0x4A, 0x39, 0x31, 0x65, 0x45, 0x79, 0x72, 0x52, 0x6A, 0x75, 0x61, 0x7A, 0x72, 0x38, 0x46, 0x47, 0x46, 0x31, 0x4E, 0x46, 0x54, 0x77, + 0x57, 0x6D, 0x68, 0x6C, 0x51, 0x42, 0x4A, 0x71, 0x79, 0x6D, 0x6D, 0x39, 0x6C, 0x69, 0x31, 0x4A, 0x66, 0x50, 0x46, 0x67, 0x45, 0x4B, 0x43, 0x58, 0x41, 0x5A, 0x6D, 0x45, 0x78, 0x66, 0x72, 0x6E, 0x67, 0x64, 0x62, 0x6B, 0x61, 0x71, 0x49, 0x48, + 0x57, 0x63, 0x68, 0x65, 0x7A, 0x78, 0x51, 0x4D, 0x78, 0x0A, 0x4E, 0x52, 0x46, 0x34, 0x65, 0x4B, 0x4C, 0x67, 0x36, 0x54, 0x43, 0x4D, 0x66, 0x34, 0x44, 0x66, 0x57, 0x4E, 0x38, 0x38, 0x75, 0x69, 0x65, 0x57, 0x34, 0x6F, 0x41, 0x30, 0x62, 0x65, + 0x4F, 0x59, 0x30, 0x32, 0x51, 0x6E, 0x72, 0x45, 0x68, 0x2B, 0x4B, 0x48, 0x64, 0x63, 0x78, 0x69, 0x56, 0x68, 0x4A, 0x66, 0x69, 0x46, 0x44, 0x47, 0x58, 0x36, 0x78, 0x44, 0x49, 0x76, 0x70, 0x5A, 0x67, 0x46, 0x35, 0x50, 0x67, 0x4C, 0x5A, 0x78, + 0x59, 0x57, 0x78, 0x6F, 0x4B, 0x34, 0x0A, 0x4D, 0x68, 0x6E, 0x35, 0x2B, 0x62, 0x6C, 0x35, 0x33, 0x42, 0x2F, 0x4E, 0x36, 0x36, 0x2B, 0x72, 0x44, 0x74, 0x30, 0x62, 0x32, 0x30, 0x58, 0x6B, 0x65, 0x75, 0x63, 0x43, 0x34, 0x70, 0x56, 0x64, 0x2F, + 0x47, 0x6E, 0x77, 0x55, 0x32, 0x6C, 0x68, 0x6C, 0x58, 0x56, 0x35, 0x43, 0x31, 0x35, 0x56, 0x35, 0x6A, 0x67, 0x63, 0x6C, 0x4B, 0x6C, 0x5A, 0x4D, 0x35, 0x37, 0x49, 0x63, 0x58, 0x52, 0x35, 0x66, 0x31, 0x47, 0x4A, 0x74, 0x73, 0x68, 0x71, 0x75, + 0x44, 0x44, 0x49, 0x0A, 0x61, 0x6A, 0x6A, 0x44, 0x62, 0x70, 0x37, 0x68, 0x4E, 0x78, 0x62, 0x71, 0x42, 0x57, 0x4A, 0x4D, 0x57, 0x78, 0x4A, 0x48, 0x37, 0x61, 0x65, 0x30, 0x73, 0x31, 0x68, 0x57, 0x78, 0x30, 0x6E, 0x7A, 0x66, 0x78, 0x4A, 0x6F, + 0x43, 0x54, 0x46, 0x78, 0x38, 0x47, 0x33, 0x34, 0x54, 0x6B, 0x66, 0x37, 0x31, 0x6F, 0x58, 0x75, 0x78, 0x56, 0x68, 0x41, 0x47, 0x61, 0x51, 0x64, 0x70, 0x2F, 0x6C, 0x4C, 0x51, 0x7A, 0x66, 0x63, 0x61, 0x46, 0x70, 0x50, 0x7A, 0x2B, 0x76, 0x43, + 0x0A, 0x5A, 0x48, 0x54, 0x65, 0x74, 0x42, 0x58, 0x5A, 0x39, 0x46, 0x52, 0x55, 0x47, 0x69, 0x38, 0x63, 0x31, 0x35, 0x64, 0x78, 0x56, 0x4A, 0x43, 0x4F, 0x32, 0x53, 0x43, 0x64, 0x55, 0x79, 0x74, 0x2F, 0x71, 0x34, 0x2F, 0x69, 0x36, 0x6A, 0x43, + 0x38, 0x55, 0x44, 0x66, 0x76, 0x38, 0x55, 0x65, 0x31, 0x66, 0x58, 0x77, 0x73, 0x42, 0x4F, 0x78, 0x6F, 0x6E, 0x62, 0x52, 0x4A, 0x52, 0x42, 0x44, 0x30, 0x63, 0x6B, 0x73, 0x63, 0x5A, 0x4F, 0x66, 0x38, 0x35, 0x6D, 0x75, 0x51, 0x0A, 0x33, 0x57, + 0x6C, 0x39, 0x61, 0x66, 0x30, 0x41, 0x56, 0x71, 0x57, 0x33, 0x72, 0x4C, 0x61, 0x74, 0x74, 0x38, 0x6F, 0x2B, 0x41, 0x65, 0x2B, 0x63, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x45, 0x6E, 0x74, 0x72, 0x75, 0x73, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, + 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2D, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x50, 0x6A, 0x43, 0x43, 0x41, 0x79, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, 0x53, 0x6C, 0x4F, 0x4D, 0x4B, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x76, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, + 0x41, 0x6F, 0x54, 0x44, 0x55, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4B, 0x44, 0x41, 0x6D, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x48, 0x31, 0x4E, 0x6C, 0x5A, + 0x53, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x35, 0x75, 0x5A, 0x58, 0x51, 0x76, 0x62, 0x47, 0x56, 0x6E, 0x59, 0x57, 0x77, 0x74, 0x64, 0x47, 0x56, 0x79, 0x0A, 0x62, 0x58, 0x4D, 0x78, + 0x4F, 0x54, 0x41, 0x33, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x4D, 0x43, 0x68, 0x6A, 0x4B, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x41, 0x35, 0x49, 0x45, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, + 0x59, 0x79, 0x34, 0x67, 0x4C, 0x53, 0x42, 0x6D, 0x62, 0x33, 0x49, 0x67, 0x59, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x70, 0x6C, 0x5A, 0x43, 0x42, 0x31, 0x63, 0x32, 0x55, 0x67, 0x0A, 0x62, 0x32, 0x35, 0x73, 0x65, 0x54, 0x45, + 0x79, 0x4D, 0x44, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x70, 0x52, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, + 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x43, 0x30, 0x67, 0x52, 0x7A, 0x49, 0x77, 0x0A, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x44, 0x6B, 0x77, 0x4E, 0x7A, + 0x41, 0x33, 0x4D, 0x54, 0x63, 0x79, 0x4E, 0x54, 0x55, 0x30, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x41, 0x78, 0x4D, 0x6A, 0x41, 0x33, 0x4D, 0x54, 0x63, 0x31, 0x4E, 0x54, 0x55, 0x30, 0x57, 0x6A, 0x43, 0x42, 0x76, 0x6A, 0x45, 0x4C, 0x4D, 0x41, + 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x0A, 0x44, 0x55, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, 0x49, + 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4B, 0x44, 0x41, 0x6D, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x48, 0x31, 0x4E, 0x6C, 0x5A, 0x53, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, + 0x43, 0x35, 0x75, 0x5A, 0x58, 0x51, 0x76, 0x62, 0x47, 0x56, 0x6E, 0x59, 0x57, 0x77, 0x74, 0x64, 0x47, 0x56, 0x79, 0x62, 0x58, 0x4D, 0x78, 0x0A, 0x4F, 0x54, 0x41, 0x33, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x4D, 0x43, 0x68, 0x6A, + 0x4B, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x41, 0x35, 0x49, 0x45, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x67, 0x4C, 0x53, 0x42, 0x6D, 0x62, 0x33, 0x49, 0x67, 0x59, 0x58, 0x56, 0x30, + 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x70, 0x6C, 0x5A, 0x43, 0x42, 0x31, 0x63, 0x32, 0x55, 0x67, 0x62, 0x32, 0x35, 0x73, 0x0A, 0x65, 0x54, 0x45, 0x79, 0x4D, 0x44, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x70, 0x52, 0x57, 0x35, + 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, + 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x43, 0x30, 0x67, 0x52, 0x7A, 0x49, 0x77, 0x67, 0x67, 0x45, 0x69, 0x0A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, + 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x36, 0x68, 0x4C, 0x5A, 0x79, 0x32, 0x35, 0x34, 0x4D, 0x61, 0x2B, 0x4B, 0x5A, 0x36, 0x54, 0x41, 0x42, 0x70, 0x33, 0x62, 0x71, 0x4D, 0x72, + 0x69, 0x56, 0x51, 0x52, 0x72, 0x4A, 0x32, 0x6D, 0x46, 0x4F, 0x57, 0x48, 0x4C, 0x50, 0x0A, 0x2F, 0x76, 0x61, 0x43, 0x65, 0x62, 0x39, 0x7A, 0x59, 0x51, 0x59, 0x4B, 0x70, 0x53, 0x66, 0x59, 0x73, 0x31, 0x2F, 0x54, 0x52, 0x55, 0x34, 0x63, 0x63, + 0x74, 0x5A, 0x4F, 0x4D, 0x76, 0x4A, 0x79, 0x69, 0x67, 0x2F, 0x33, 0x67, 0x78, 0x6E, 0x51, 0x61, 0x6F, 0x43, 0x41, 0x41, 0x45, 0x55, 0x65, 0x73, 0x4D, 0x66, 0x6E, 0x6D, 0x72, 0x38, 0x53, 0x56, 0x79, 0x63, 0x63, 0x6F, 0x32, 0x67, 0x76, 0x43, + 0x6F, 0x65, 0x39, 0x61, 0x6D, 0x73, 0x4F, 0x58, 0x6D, 0x58, 0x7A, 0x0A, 0x48, 0x48, 0x66, 0x56, 0x31, 0x49, 0x57, 0x4E, 0x63, 0x43, 0x47, 0x30, 0x73, 0x7A, 0x4C, 0x6E, 0x69, 0x36, 0x4C, 0x56, 0x68, 0x6A, 0x6B, 0x43, 0x73, 0x62, 0x6A, 0x53, + 0x52, 0x38, 0x37, 0x6B, 0x79, 0x55, 0x6E, 0x45, 0x4F, 0x36, 0x66, 0x65, 0x2B, 0x31, 0x52, 0x39, 0x56, 0x37, 0x37, 0x77, 0x36, 0x47, 0x37, 0x43, 0x65, 0x62, 0x49, 0x36, 0x43, 0x31, 0x58, 0x69, 0x55, 0x4A, 0x67, 0x57, 0x4D, 0x68, 0x4E, 0x63, + 0x4C, 0x33, 0x68, 0x57, 0x77, 0x63, 0x4B, 0x55, 0x0A, 0x73, 0x2F, 0x4A, 0x61, 0x35, 0x43, 0x65, 0x61, 0x6E, 0x79, 0x54, 0x58, 0x78, 0x75, 0x7A, 0x51, 0x6D, 0x79, 0x57, 0x43, 0x34, 0x38, 0x7A, 0x43, 0x78, 0x45, 0x58, 0x46, 0x6A, 0x4A, 0x64, + 0x36, 0x42, 0x6D, 0x73, 0x71, 0x45, 0x5A, 0x2B, 0x70, 0x43, 0x6D, 0x35, 0x49, 0x4F, 0x32, 0x2F, 0x62, 0x31, 0x42, 0x45, 0x5A, 0x51, 0x76, 0x65, 0x50, 0x42, 0x37, 0x2F, 0x31, 0x55, 0x31, 0x2B, 0x63, 0x50, 0x76, 0x51, 0x58, 0x4C, 0x4F, 0x5A, + 0x70, 0x72, 0x45, 0x34, 0x79, 0x0A, 0x54, 0x47, 0x4A, 0x33, 0x36, 0x72, 0x66, 0x6F, 0x35, 0x62, 0x73, 0x30, 0x76, 0x42, 0x6D, 0x4C, 0x72, 0x70, 0x78, 0x52, 0x35, 0x37, 0x64, 0x2B, 0x74, 0x56, 0x4F, 0x78, 0x4D, 0x79, 0x4C, 0x6C, 0x62, 0x63, + 0x39, 0x77, 0x50, 0x42, 0x72, 0x36, 0x34, 0x70, 0x74, 0x6E, 0x74, 0x6F, 0x50, 0x30, 0x6A, 0x61, 0x57, 0x76, 0x59, 0x6B, 0x78, 0x4E, 0x34, 0x46, 0x69, 0x73, 0x5A, 0x44, 0x51, 0x53, 0x41, 0x2F, 0x69, 0x32, 0x6A, 0x5A, 0x52, 0x6A, 0x4A, 0x4B, + 0x52, 0x78, 0x0A, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x50, 0x42, + 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x71, 0x63, 0x69, 0x5A, 0x36, 0x0A, + 0x30, 0x42, 0x37, 0x76, 0x66, 0x65, 0x63, 0x37, 0x61, 0x56, 0x48, 0x55, 0x62, 0x49, 0x32, 0x66, 0x6B, 0x42, 0x4A, 0x6D, 0x71, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, + 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x45, 0x41, 0x65, 0x5A, 0x38, 0x64, 0x6C, 0x73, 0x61, 0x32, 0x65, 0x54, 0x38, 0x69, 0x6A, 0x59, 0x66, 0x54, 0x68, 0x77, 0x4D, 0x45, 0x59, 0x47, 0x70, 0x72, 0x6D, 0x69, 0x35, 0x5A, 0x0A, 0x69, 0x58, 0x4D, + 0x52, 0x72, 0x45, 0x50, 0x52, 0x39, 0x52, 0x50, 0x2F, 0x6A, 0x54, 0x6B, 0x72, 0x77, 0x50, 0x4B, 0x39, 0x54, 0x33, 0x43, 0x4D, 0x71, 0x53, 0x2F, 0x71, 0x46, 0x38, 0x51, 0x4C, 0x56, 0x4A, 0x37, 0x55, 0x47, 0x35, 0x61, 0x59, 0x4D, 0x7A, 0x79, + 0x6F, 0x72, 0x57, 0x4B, 0x69, 0x41, 0x48, 0x61, 0x72, 0x57, 0x57, 0x6C, 0x75, 0x42, 0x68, 0x31, 0x2B, 0x78, 0x4C, 0x6C, 0x45, 0x6A, 0x5A, 0x69, 0x76, 0x45, 0x74, 0x52, 0x68, 0x32, 0x77, 0x6F, 0x5A, 0x0A, 0x52, 0x6B, 0x66, 0x7A, 0x36, 0x2F, + 0x64, 0x6A, 0x77, 0x55, 0x41, 0x46, 0x51, 0x4B, 0x58, 0x53, 0x74, 0x2F, 0x53, 0x31, 0x6D, 0x6A, 0x61, 0x2F, 0x71, 0x59, 0x68, 0x32, 0x69, 0x41, 0x52, 0x56, 0x42, 0x43, 0x75, 0x63, 0x68, 0x33, 0x38, 0x61, 0x4E, 0x7A, 0x78, 0x2B, 0x4C, 0x61, + 0x55, 0x61, 0x32, 0x4E, 0x53, 0x4A, 0x58, 0x73, 0x71, 0x39, 0x72, 0x44, 0x31, 0x73, 0x32, 0x47, 0x32, 0x76, 0x31, 0x66, 0x4E, 0x32, 0x44, 0x38, 0x30, 0x37, 0x69, 0x44, 0x67, 0x69, 0x0A, 0x6E, 0x57, 0x79, 0x54, 0x6D, 0x73, 0x51, 0x39, 0x76, + 0x34, 0x49, 0x62, 0x5A, 0x54, 0x2B, 0x6D, 0x44, 0x31, 0x32, 0x71, 0x2F, 0x4F, 0x57, 0x79, 0x46, 0x63, 0x71, 0x31, 0x72, 0x63, 0x61, 0x38, 0x50, 0x64, 0x43, 0x45, 0x36, 0x4F, 0x6F, 0x47, 0x63, 0x72, 0x42, 0x4E, 0x4F, 0x54, 0x4A, 0x34, 0x76, + 0x7A, 0x34, 0x52, 0x6E, 0x41, 0x75, 0x6B, 0x6E, 0x5A, 0x6F, 0x68, 0x38, 0x2F, 0x43, 0x62, 0x43, 0x7A, 0x42, 0x34, 0x32, 0x38, 0x48, 0x63, 0x68, 0x30, 0x50, 0x2B, 0x0A, 0x76, 0x47, 0x4F, 0x61, 0x79, 0x73, 0x58, 0x43, 0x48, 0x4D, 0x6E, 0x48, + 0x6A, 0x66, 0x38, 0x37, 0x45, 0x6C, 0x67, 0x49, 0x35, 0x72, 0x59, 0x39, 0x37, 0x48, 0x6F, 0x73, 0x54, 0x76, 0x75, 0x44, 0x6C, 0x73, 0x34, 0x4D, 0x50, 0x47, 0x6D, 0x48, 0x56, 0x48, 0x4F, 0x6B, 0x63, 0x38, 0x4B, 0x54, 0x2F, 0x31, 0x45, 0x51, + 0x72, 0x42, 0x56, 0x55, 0x41, 0x64, 0x6A, 0x38, 0x42, 0x62, 0x47, 0x4A, 0x6F, 0x58, 0x39, 0x30, 0x67, 0x35, 0x70, 0x4A, 0x31, 0x39, 0x78, 0x4F, 0x0A, 0x65, 0x34, 0x70, 0x49, 0x62, 0x34, 0x74, 0x46, 0x39, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x45, 0x6E, 0x74, 0x72, 0x75, 0x73, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x2D, 0x20, 0x45, 0x43, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, + 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x2B, 0x54, 0x43, 0x43, 0x41, 0x6F, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4E, 0x41, + 0x4B, 0x61, 0x4C, 0x65, 0x53, 0x6B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x55, 0x4E, 0x43, 0x52, 0x2B, 0x54, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x43, 0x42, 0x76, 0x7A, 0x45, 0x4C, 0x4D, + 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x0A, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x55, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, + 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4B, 0x44, 0x41, 0x6D, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x48, 0x31, 0x4E, 0x6C, 0x5A, 0x53, 0x42, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x57, 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, + 0x64, 0x43, 0x35, 0x75, 0x5A, 0x58, 0x51, 0x76, 0x62, 0x47, 0x56, 0x6E, 0x0A, 0x59, 0x57, 0x77, 0x74, 0x64, 0x47, 0x56, 0x79, 0x62, 0x58, 0x4D, 0x78, 0x4F, 0x54, 0x41, 0x33, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x4D, 0x43, 0x68, + 0x6A, 0x4B, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x45, 0x79, 0x49, 0x45, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x67, 0x4C, 0x53, 0x42, 0x6D, 0x62, 0x33, 0x49, 0x67, 0x59, 0x58, 0x56, + 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x70, 0x6C, 0x0A, 0x5A, 0x43, 0x42, 0x31, 0x63, 0x32, 0x55, 0x67, 0x62, 0x32, 0x35, 0x73, 0x65, 0x54, 0x45, 0x7A, 0x4D, 0x44, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x71, 0x52, 0x57, + 0x35, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, + 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x0A, 0x49, 0x43, 0x30, 0x67, 0x52, 0x55, 0x4D, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x79, 0x4D, 0x54, 0x49, 0x78, 0x4F, 0x44, 0x45, 0x31, 0x4D, 0x6A, 0x55, 0x7A, 0x4E, 0x6C, 0x6F, 0x58, 0x44, + 0x54, 0x4D, 0x33, 0x4D, 0x54, 0x49, 0x78, 0x4F, 0x44, 0x45, 0x31, 0x4E, 0x54, 0x55, 0x7A, 0x4E, 0x6C, 0x6F, 0x77, 0x67, 0x62, 0x38, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, + 0x52, 0x59, 0x77, 0x0A, 0x46, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x31, 0x46, 0x62, 0x6E, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x4D, 0x53, 0x67, 0x77, 0x4A, 0x67, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x39, 0x54, 0x5A, 0x57, 0x55, 0x67, 0x64, 0x33, 0x64, 0x33, 0x4C, 0x6D, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x75, 0x62, 0x6D, 0x56, 0x30, 0x4C, 0x32, 0x78, 0x6C, 0x5A, 0x32, 0x46, 0x73, + 0x0A, 0x4C, 0x58, 0x52, 0x6C, 0x63, 0x6D, 0x31, 0x7A, 0x4D, 0x54, 0x6B, 0x77, 0x4E, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x7A, 0x41, 0x6F, 0x59, 0x79, 0x6B, 0x67, 0x4D, 0x6A, 0x41, 0x78, 0x4D, 0x69, 0x42, 0x46, 0x62, 0x6E, 0x52, + 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x49, 0x43, 0x30, 0x67, 0x5A, 0x6D, 0x39, 0x79, 0x49, 0x47, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x36, 0x5A, 0x57, 0x51, 0x67, 0x0A, 0x64, 0x58, + 0x4E, 0x6C, 0x49, 0x47, 0x39, 0x75, 0x62, 0x48, 0x6B, 0x78, 0x4D, 0x7A, 0x41, 0x78, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4B, 0x6B, 0x56, 0x75, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, + 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x41, 0x74, 0x0A, 0x49, 0x45, 0x56, 0x44, 0x4D, + 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x49, 0x51, 0x54, 0x79, 0x64, 0x43, 0x36, 0x62, + 0x55, 0x46, 0x37, 0x34, 0x6D, 0x7A, 0x51, 0x36, 0x31, 0x56, 0x66, 0x5A, 0x67, 0x49, 0x61, 0x4A, 0x50, 0x52, 0x62, 0x69, 0x57, 0x6C, 0x48, 0x34, 0x37, 0x6A, 0x43, 0x66, 0x66, 0x48, 0x79, 0x0A, 0x41, 0x73, 0x57, 0x66, 0x6F, 0x50, 0x5A, 0x62, + 0x31, 0x59, 0x73, 0x47, 0x47, 0x59, 0x5A, 0x50, 0x55, 0x78, 0x42, 0x74, 0x42, 0x79, 0x51, 0x6E, 0x6F, 0x61, 0x44, 0x34, 0x31, 0x55, 0x63, 0x5A, 0x59, 0x55, 0x78, 0x39, 0x79, 0x70, 0x4D, 0x6E, 0x36, 0x6E, 0x51, 0x4D, 0x37, 0x32, 0x2B, 0x57, + 0x43, 0x66, 0x35, 0x6A, 0x37, 0x48, 0x42, 0x64, 0x4E, 0x71, 0x31, 0x6E, 0x64, 0x36, 0x37, 0x4A, 0x6E, 0x58, 0x78, 0x56, 0x52, 0x44, 0x71, 0x69, 0x59, 0x31, 0x45, 0x66, 0x0A, 0x39, 0x65, 0x4E, 0x69, 0x31, 0x4B, 0x6C, 0x48, 0x42, 0x7A, 0x37, + 0x4D, 0x49, 0x4B, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, + 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x0A, 0x46, 0x4C, 0x64, 0x6A, 0x35, 0x78, 0x72, 0x64, 0x6A, 0x65, 0x6B, 0x49, 0x70, 0x6C, + 0x57, 0x44, 0x70, 0x4F, 0x42, 0x71, 0x55, 0x45, 0x46, 0x6C, 0x45, 0x55, 0x4A, 0x4A, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x63, 0x41, 0x4D, 0x47, 0x51, 0x43, 0x4D, 0x47, + 0x46, 0x35, 0x32, 0x4F, 0x56, 0x43, 0x52, 0x39, 0x38, 0x63, 0x72, 0x6C, 0x4F, 0x5A, 0x46, 0x37, 0x5A, 0x76, 0x48, 0x48, 0x33, 0x68, 0x0A, 0x76, 0x78, 0x47, 0x55, 0x30, 0x51, 0x4F, 0x49, 0x64, 0x65, 0x53, 0x4E, 0x69, 0x61, 0x53, 0x4B, 0x64, + 0x30, 0x62, 0x65, 0x62, 0x57, 0x48, 0x76, 0x41, 0x76, 0x58, 0x37, 0x74, 0x64, 0x2F, 0x4D, 0x2F, 0x6B, 0x37, 0x2F, 0x2F, 0x71, 0x6E, 0x6D, 0x70, 0x77, 0x49, 0x77, 0x57, 0x35, 0x6E, 0x58, 0x68, 0x54, 0x63, 0x47, 0x74, 0x58, 0x73, 0x49, 0x2F, + 0x65, 0x73, 0x6E, 0x69, 0x30, 0x71, 0x55, 0x2B, 0x65, 0x48, 0x36, 0x70, 0x34, 0x34, 0x6D, 0x43, 0x4F, 0x68, 0x38, 0x0A, 0x6B, 0x6D, 0x68, 0x74, 0x63, 0x39, 0x68, 0x76, 0x4A, 0x71, 0x77, 0x68, 0x41, 0x72, 0x69, 0x5A, 0x74, 0x79, 0x5A, 0x42, + 0x57, 0x79, 0x56, 0x67, 0x72, 0x74, 0x42, 0x49, 0x47, 0x75, 0x34, 0x47, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, + 0x43, 0x46, 0x43, 0x41, 0x20, 0x45, 0x56, 0x20, 0x52, 0x4F, 0x4F, 0x54, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x6A, 0x54, 0x43, 0x43, 0x41, 0x33, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, 0x47, 0x45, 0x72, 0x4D, 0x31, 0x6A, + 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x57, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, + 0x45, 0x77, 0x4D, 0x43, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x67, 0x77, 0x6E, 0x51, 0x32, 0x68, 0x70, 0x62, 0x6D, 0x45, 0x67, 0x52, 0x6D, 0x6C, 0x75, 0x59, 0x57, 0x35, 0x6A, 0x61, 0x57, 0x46, 0x73, 0x49, 0x45, 0x4E, 0x6C, 0x63, + 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, + 0x41, 0x78, 0x44, 0x52, 0x6B, 0x4E, 0x42, 0x0A, 0x49, 0x45, 0x56, 0x57, 0x49, 0x46, 0x4A, 0x50, 0x54, 0x31, 0x51, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x49, 0x77, 0x4F, 0x44, 0x41, 0x34, 0x4D, 0x44, 0x4D, 0x77, 0x4E, 0x7A, 0x41, 0x78, + 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x6B, 0x78, 0x4D, 0x6A, 0x4D, 0x78, 0x4D, 0x44, 0x4D, 0x77, 0x4E, 0x7A, 0x41, 0x78, 0x57, 0x6A, 0x42, 0x57, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, + 0x54, 0x6A, 0x45, 0x77, 0x0A, 0x4D, 0x43, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x6E, 0x51, 0x32, 0x68, 0x70, 0x62, 0x6D, 0x45, 0x67, 0x52, 0x6D, 0x6C, 0x75, 0x59, 0x57, 0x35, 0x6A, 0x61, 0x57, 0x46, 0x73, 0x49, 0x45, 0x4E, + 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x44, 0x0A, 0x44, 0x41, 0x78, 0x44, 0x52, 0x6B, 0x4E, 0x42, 0x49, 0x45, 0x56, 0x57, 0x49, 0x46, 0x4A, 0x50, 0x54, 0x31, 0x51, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, + 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x58, 0x58, 0x57, 0x76, 0x4E, 0x45, 0x44, 0x38, 0x66, 0x42, 0x56, 0x6E, 0x56, 0x0A, 0x42, + 0x55, 0x30, 0x33, 0x73, 0x51, 0x37, 0x73, 0x6D, 0x43, 0x75, 0x4F, 0x46, 0x52, 0x33, 0x36, 0x6B, 0x30, 0x73, 0x58, 0x67, 0x69, 0x46, 0x78, 0x45, 0x46, 0x4C, 0x58, 0x55, 0x57, 0x52, 0x77, 0x46, 0x73, 0x4A, 0x56, 0x61, 0x55, 0x32, 0x4F, 0x46, + 0x57, 0x32, 0x66, 0x76, 0x77, 0x77, 0x62, 0x77, 0x75, 0x43, 0x6A, 0x5A, 0x39, 0x59, 0x4D, 0x72, 0x4D, 0x38, 0x69, 0x72, 0x71, 0x39, 0x33, 0x56, 0x43, 0x70, 0x4C, 0x54, 0x49, 0x70, 0x54, 0x55, 0x6E, 0x72, 0x44, 0x0A, 0x37, 0x69, 0x37, 0x65, + 0x73, 0x33, 0x45, 0x6C, 0x77, 0x65, 0x6C, 0x64, 0x50, 0x65, 0x36, 0x68, 0x4C, 0x36, 0x50, 0x33, 0x4B, 0x6A, 0x7A, 0x4A, 0x49, 0x78, 0x31, 0x71, 0x71, 0x78, 0x32, 0x68, 0x70, 0x2F, 0x48, 0x7A, 0x37, 0x4B, 0x44, 0x56, 0x52, 0x4D, 0x38, 0x56, + 0x7A, 0x33, 0x49, 0x76, 0x48, 0x57, 0x4F, 0x58, 0x36, 0x4A, 0x6E, 0x35, 0x2F, 0x5A, 0x4F, 0x6B, 0x56, 0x49, 0x42, 0x4D, 0x55, 0x74, 0x52, 0x53, 0x71, 0x79, 0x35, 0x4A, 0x33, 0x35, 0x44, 0x4E, 0x0A, 0x75, 0x46, 0x2B, 0x2B, 0x50, 0x39, 0x36, + 0x68, 0x79, 0x6B, 0x30, 0x67, 0x31, 0x43, 0x58, 0x6F, 0x68, 0x43, 0x6C, 0x54, 0x74, 0x37, 0x47, 0x49, 0x48, 0x2F, 0x2F, 0x36, 0x32, 0x70, 0x43, 0x66, 0x43, 0x71, 0x6B, 0x74, 0x51, 0x54, 0x2B, 0x78, 0x38, 0x52, 0x67, 0x70, 0x37, 0x68, 0x5A, + 0x5A, 0x4C, 0x44, 0x52, 0x4A, 0x47, 0x71, 0x67, 0x47, 0x31, 0x36, 0x69, 0x49, 0x30, 0x67, 0x4E, 0x79, 0x65, 0x6A, 0x4C, 0x69, 0x36, 0x6D, 0x68, 0x4E, 0x62, 0x69, 0x79, 0x57, 0x0A, 0x5A, 0x58, 0x76, 0x4B, 0x57, 0x66, 0x72, 0x79, 0x34, 0x74, + 0x33, 0x75, 0x4D, 0x43, 0x7A, 0x37, 0x7A, 0x45, 0x61, 0x73, 0x78, 0x47, 0x50, 0x72, 0x62, 0x33, 0x38, 0x32, 0x4B, 0x7A, 0x52, 0x7A, 0x45, 0x70, 0x52, 0x2F, 0x33, 0x38, 0x77, 0x6D, 0x6E, 0x76, 0x46, 0x79, 0x58, 0x56, 0x42, 0x6C, 0x57, 0x59, + 0x39, 0x70, 0x73, 0x34, 0x64, 0x65, 0x4D, 0x6D, 0x2F, 0x44, 0x47, 0x49, 0x71, 0x31, 0x6C, 0x59, 0x2B, 0x77, 0x65, 0x6A, 0x66, 0x65, 0x57, 0x6B, 0x55, 0x37, 0x0A, 0x78, 0x7A, 0x62, 0x68, 0x37, 0x32, 0x66, 0x52, 0x4F, 0x64, 0x4F, 0x58, 0x57, + 0x33, 0x4E, 0x69, 0x47, 0x55, 0x67, 0x74, 0x68, 0x78, 0x77, 0x47, 0x2B, 0x33, 0x53, 0x59, 0x49, 0x45, 0x6C, 0x7A, 0x38, 0x41, 0x58, 0x53, 0x47, 0x37, 0x47, 0x67, 0x6F, 0x37, 0x63, 0x62, 0x63, 0x4E, 0x4F, 0x49, 0x61, 0x62, 0x6C, 0x61, 0x31, + 0x6A, 0x6A, 0x30, 0x59, 0x74, 0x77, 0x6C, 0x69, 0x33, 0x69, 0x2F, 0x2B, 0x4F, 0x68, 0x2B, 0x75, 0x46, 0x7A, 0x4A, 0x6C, 0x55, 0x39, 0x66, 0x0A, 0x70, 0x79, 0x32, 0x35, 0x49, 0x47, 0x76, 0x50, 0x61, 0x39, 0x33, 0x31, 0x44, 0x66, 0x53, 0x43, + 0x74, 0x2F, 0x53, 0x79, 0x5A, 0x69, 0x34, 0x51, 0x4B, 0x50, 0x61, 0x58, 0x57, 0x6E, 0x75, 0x57, 0x46, 0x6F, 0x38, 0x42, 0x47, 0x53, 0x31, 0x73, 0x62, 0x6E, 0x38, 0x35, 0x57, 0x41, 0x5A, 0x6B, 0x67, 0x77, 0x47, 0x44, 0x67, 0x38, 0x4E, 0x4E, + 0x6B, 0x74, 0x30, 0x79, 0x78, 0x6F, 0x65, 0x6B, 0x4E, 0x2B, 0x6B, 0x57, 0x7A, 0x71, 0x6F, 0x74, 0x61, 0x4B, 0x38, 0x4B, 0x0A, 0x67, 0x57, 0x55, 0x36, 0x63, 0x4D, 0x47, 0x62, 0x72, 0x55, 0x31, 0x74, 0x56, 0x4D, 0x6F, 0x71, 0x4C, 0x55, 0x75, + 0x46, 0x47, 0x37, 0x4F, 0x41, 0x35, 0x6E, 0x42, 0x46, 0x44, 0x57, 0x74, 0x65, 0x4E, 0x66, 0x42, 0x2F, 0x4F, 0x37, 0x69, 0x63, 0x35, 0x41, 0x52, 0x77, 0x69, 0x52, 0x49, 0x6C, 0x6B, 0x39, 0x6F, 0x4B, 0x6D, 0x53, 0x4A, 0x67, 0x61, 0x6D, 0x4E, + 0x67, 0x54, 0x6E, 0x59, 0x47, 0x6D, 0x45, 0x36, 0x39, 0x67, 0x36, 0x30, 0x64, 0x57, 0x49, 0x6F, 0x6C, 0x0A, 0x68, 0x64, 0x4C, 0x48, 0x5A, 0x52, 0x34, 0x74, 0x6A, 0x73, 0x62, 0x66, 0x74, 0x73, 0x62, 0x68, 0x66, 0x34, 0x6F, 0x45, 0x49, 0x52, + 0x55, 0x70, 0x64, 0x50, 0x41, 0x2B, 0x6E, 0x4A, 0x43, 0x64, 0x44, 0x43, 0x37, 0x78, 0x69, 0x6A, 0x35, 0x61, 0x71, 0x67, 0x77, 0x4A, 0x48, 0x73, 0x66, 0x56, 0x50, 0x4B, 0x50, 0x74, 0x6C, 0x38, 0x4D, 0x65, 0x4E, 0x50, 0x6F, 0x34, 0x2B, 0x51, + 0x67, 0x4F, 0x34, 0x38, 0x42, 0x64, 0x4B, 0x34, 0x50, 0x52, 0x56, 0x6D, 0x72, 0x4A, 0x0A, 0x74, 0x71, 0x68, 0x55, 0x55, 0x79, 0x35, 0x34, 0x4D, 0x6D, 0x63, 0x39, 0x67, 0x6E, 0x39, 0x30, 0x30, 0x50, 0x76, 0x68, 0x74, 0x67, 0x56, 0x67, 0x75, + 0x58, 0x44, 0x62, 0x6A, 0x67, 0x76, 0x35, 0x45, 0x31, 0x68, 0x76, 0x63, 0x57, 0x41, 0x51, 0x55, 0x68, 0x43, 0x35, 0x77, 0x55, 0x45, 0x4A, 0x37, 0x33, 0x49, 0x66, 0x5A, 0x7A, 0x46, 0x34, 0x2F, 0x35, 0x59, 0x46, 0x6A, 0x51, 0x49, 0x44, 0x41, + 0x51, 0x41, 0x42, 0x6F, 0x32, 0x4D, 0x77, 0x59, 0x54, 0x41, 0x66, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x54, 0x6A, 0x2F, 0x69, 0x33, 0x39, 0x4B, 0x4E, 0x41, 0x4C, 0x74, 0x62, 0x71, 0x32, + 0x6F, 0x73, 0x53, 0x2F, 0x42, 0x71, 0x6F, 0x46, 0x6A, 0x4A, 0x50, 0x37, 0x4C, 0x7A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x34, 0x2F, 0x34, 0x74, 0x2F, 0x53, 0x6A, + 0x51, 0x43, 0x37, 0x57, 0x36, 0x74, 0x71, 0x4C, 0x45, 0x76, 0x77, 0x61, 0x71, 0x42, 0x59, 0x79, 0x54, 0x2B, 0x79, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, + 0x44, 0x67, 0x67, 0x49, 0x42, 0x0A, 0x41, 0x43, 0x58, 0x47, 0x75, 0x6D, 0x76, 0x72, 0x68, 0x38, 0x76, 0x65, 0x67, 0x6A, 0x6D, 0x57, 0x50, 0x66, 0x42, 0x45, 0x70, 0x32, 0x75, 0x45, 0x63, 0x77, 0x50, 0x65, 0x6E, 0x53, 0x74, 0x50, 0x75, 0x69, + 0x42, 0x2F, 0x76, 0x48, 0x69, 0x79, 0x7A, 0x35, 0x65, 0x77, 0x47, 0x35, 0x7A, 0x7A, 0x31, 0x33, 0x6B, 0x75, 0x39, 0x55, 0x69, 0x32, 0x30, 0x76, 0x73, 0x58, 0x69, 0x4F, 0x62, 0x54, 0x65, 0x6A, 0x2F, 0x74, 0x55, 0x78, 0x50, 0x51, 0x34, 0x69, + 0x39, 0x71, 0x0A, 0x65, 0x63, 0x73, 0x41, 0x49, 0x79, 0x6A, 0x6D, 0x48, 0x6A, 0x64, 0x58, 0x4E, 0x59, 0x6D, 0x45, 0x77, 0x6E, 0x5A, 0x50, 0x4E, 0x44, 0x61, 0x74, 0x5A, 0x38, 0x50, 0x4F, 0x51, 0x51, 0x61, 0x49, 0x78, 0x66, 0x66, 0x75, 0x32, + 0x42, 0x71, 0x34, 0x31, 0x67, 0x74, 0x2F, 0x55, 0x50, 0x2B, 0x54, 0x71, 0x68, 0x64, 0x4C, 0x6A, 0x4F, 0x7A, 0x74, 0x55, 0x6D, 0x43, 0x79, 0x70, 0x41, 0x62, 0x71, 0x54, 0x75, 0x76, 0x30, 0x61, 0x78, 0x6E, 0x39, 0x36, 0x2F, 0x55, 0x61, 0x0A, + 0x34, 0x43, 0x55, 0x71, 0x6D, 0x74, 0x7A, 0x48, 0x51, 0x54, 0x62, 0x33, 0x79, 0x48, 0x51, 0x46, 0x68, 0x44, 0x6D, 0x56, 0x4F, 0x64, 0x59, 0x4C, 0x4F, 0x36, 0x51, 0x6E, 0x2B, 0x67, 0x6A, 0x59, 0x58, 0x42, 0x37, 0x34, 0x42, 0x47, 0x42, 0x53, + 0x45, 0x53, 0x67, 0x6F, 0x41, 0x2F, 0x2F, 0x76, 0x55, 0x32, 0x59, 0x41, 0x70, 0x55, 0x6F, 0x30, 0x46, 0x6D, 0x5A, 0x38, 0x2F, 0x51, 0x6D, 0x6B, 0x72, 0x70, 0x35, 0x6E, 0x47, 0x6D, 0x39, 0x42, 0x43, 0x32, 0x73, 0x47, 0x0A, 0x45, 0x35, 0x75, + 0x50, 0x68, 0x6E, 0x45, 0x46, 0x74, 0x43, 0x2B, 0x4E, 0x69, 0x57, 0x59, 0x7A, 0x4B, 0x58, 0x5A, 0x55, 0x6D, 0x68, 0x48, 0x34, 0x4A, 0x2F, 0x71, 0x79, 0x50, 0x35, 0x48, 0x67, 0x7A, 0x67, 0x30, 0x62, 0x38, 0x7A, 0x41, 0x61, 0x72, 0x62, 0x38, + 0x69, 0x58, 0x52, 0x76, 0x54, 0x76, 0x79, 0x55, 0x46, 0x54, 0x65, 0x47, 0x53, 0x47, 0x6E, 0x2B, 0x5A, 0x6E, 0x7A, 0x78, 0x45, 0x6B, 0x38, 0x72, 0x55, 0x51, 0x45, 0x6C, 0x73, 0x67, 0x49, 0x66, 0x58, 0x0A, 0x42, 0x44, 0x72, 0x44, 0x4D, 0x6C, + 0x49, 0x31, 0x44, 0x6C, 0x62, 0x34, 0x70, 0x64, 0x31, 0x39, 0x78, 0x49, 0x73, 0x4E, 0x45, 0x52, 0x39, 0x54, 0x79, 0x78, 0x36, 0x79, 0x46, 0x37, 0x5A, 0x6F, 0x64, 0x31, 0x72, 0x67, 0x31, 0x4D, 0x76, 0x49, 0x42, 0x36, 0x37, 0x31, 0x4F, 0x69, + 0x36, 0x4F, 0x4E, 0x37, 0x66, 0x51, 0x41, 0x55, 0x74, 0x44, 0x4B, 0x58, 0x65, 0x4D, 0x4F, 0x5A, 0x65, 0x50, 0x67, 0x6C, 0x72, 0x34, 0x55, 0x65, 0x57, 0x4A, 0x6F, 0x42, 0x6A, 0x6E, 0x0A, 0x61, 0x48, 0x39, 0x64, 0x43, 0x69, 0x37, 0x37, 0x6F, + 0x30, 0x63, 0x4F, 0x50, 0x61, 0x59, 0x6A, 0x65, 0x73, 0x59, 0x42, 0x78, 0x34, 0x2F, 0x49, 0x58, 0x72, 0x39, 0x74, 0x67, 0x46, 0x61, 0x2B, 0x69, 0x69, 0x53, 0x36, 0x4D, 0x2B, 0x71, 0x66, 0x34, 0x54, 0x49, 0x52, 0x6E, 0x76, 0x48, 0x53, 0x54, + 0x34, 0x44, 0x32, 0x47, 0x30, 0x43, 0x76, 0x4F, 0x4A, 0x34, 0x52, 0x55, 0x48, 0x6C, 0x7A, 0x45, 0x68, 0x4C, 0x4E, 0x35, 0x6D, 0x79, 0x64, 0x4C, 0x49, 0x68, 0x79, 0x0A, 0x50, 0x44, 0x43, 0x42, 0x42, 0x70, 0x45, 0x69, 0x36, 0x6C, 0x6D, 0x74, + 0x32, 0x68, 0x6B, 0x75, 0x49, 0x73, 0x4B, 0x4E, 0x75, 0x59, 0x79, 0x48, 0x34, 0x47, 0x61, 0x38, 0x63, 0x79, 0x4E, 0x66, 0x49, 0x57, 0x52, 0x6A, 0x67, 0x45, 0x6A, 0x31, 0x6F, 0x44, 0x77, 0x59, 0x50, 0x5A, 0x54, 0x49, 0x53, 0x45, 0x45, 0x64, + 0x51, 0x4C, 0x70, 0x65, 0x2F, 0x76, 0x35, 0x57, 0x4F, 0x61, 0x48, 0x49, 0x7A, 0x31, 0x36, 0x65, 0x47, 0x57, 0x52, 0x47, 0x45, 0x4E, 0x6F, 0x58, 0x0A, 0x6B, 0x62, 0x63, 0x46, 0x67, 0x4B, 0x79, 0x4C, 0x6D, 0x5A, 0x4A, 0x39, 0x35, 0x36, 0x4C, + 0x59, 0x42, 0x77, 0x73, 0x32, 0x4A, 0x2B, 0x64, 0x49, 0x65, 0x57, 0x43, 0x4B, 0x77, 0x39, 0x63, 0x54, 0x58, 0x50, 0x68, 0x79, 0x51, 0x4E, 0x39, 0x4B, 0x79, 0x38, 0x2B, 0x5A, 0x41, 0x41, 0x6F, 0x41, 0x43, 0x78, 0x47, 0x56, 0x32, 0x6C, 0x5A, + 0x46, 0x41, 0x34, 0x67, 0x4B, 0x6E, 0x32, 0x66, 0x51, 0x31, 0x58, 0x6D, 0x78, 0x71, 0x49, 0x31, 0x41, 0x62, 0x51, 0x33, 0x43, 0x0A, 0x65, 0x6B, 0x44, 0x36, 0x38, 0x31, 0x39, 0x6B, 0x52, 0x35, 0x4C, 0x4C, 0x55, 0x37, 0x6D, 0x37, 0x57, 0x63, + 0x35, 0x50, 0x2F, 0x64, 0x41, 0x56, 0x55, 0x77, 0x48, 0x59, 0x33, 0x2B, 0x76, 0x5A, 0x35, 0x6E, 0x62, 0x76, 0x30, 0x43, 0x4F, 0x37, 0x4F, 0x36, 0x6C, 0x35, 0x73, 0x39, 0x55, 0x43, 0x4B, 0x63, 0x32, 0x4A, 0x6F, 0x35, 0x59, 0x50, 0x53, 0x6A, + 0x58, 0x6E, 0x54, 0x6B, 0x4C, 0x41, 0x64, 0x63, 0x30, 0x48, 0x7A, 0x2B, 0x59, 0x73, 0x36, 0x33, 0x73, 0x75, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4F, 0x49, 0x53, 0x54, 0x45, 0x20, 0x57, 0x49, 0x53, 0x65, 0x4B, 0x65, 0x79, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x42, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x74, 0x54, 0x43, 0x43, 0x41, 0x70, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x64, 0x72, + 0x45, 0x67, 0x55, 0x6E, 0x54, 0x77, 0x68, 0x59, 0x64, 0x47, 0x73, 0x2F, 0x67, 0x6A, 0x47, 0x76, 0x62, 0x43, 0x77, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, + 0x42, 0x74, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x44, 0x53, 0x44, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x48, 0x56, 0x30, 0x6C, 0x54, 0x5A, + 0x55, 0x74, 0x6C, 0x65, 0x54, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x5A, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, 0x55, 0x67, 0x52, 0x6D, 0x39, 0x31, 0x62, 0x6D, 0x52, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, + 0x69, 0x42, 0x46, 0x62, 0x6D, 0x52, 0x76, 0x63, 0x6E, 0x4E, 0x6C, 0x0A, 0x5A, 0x44, 0x45, 0x6F, 0x4D, 0x43, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x66, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, 0x55, 0x67, 0x56, 0x30, 0x6C, 0x54, + 0x5A, 0x55, 0x74, 0x6C, 0x65, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x51, 0x69, 0x42, 0x44, 0x51, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x44, 0x45, 0x79, + 0x4D, 0x44, 0x45, 0x78, 0x4E, 0x54, 0x41, 0x77, 0x0A, 0x4D, 0x7A, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4F, 0x54, 0x45, 0x79, 0x4D, 0x44, 0x45, 0x78, 0x4E, 0x54, 0x45, 0x77, 0x4D, 0x7A, 0x46, 0x61, 0x4D, 0x47, 0x30, 0x78, 0x43, 0x7A, 0x41, + 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x49, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x64, 0x58, 0x53, 0x56, 0x4E, 0x6C, 0x53, 0x32, 0x56, 0x35, 0x4D, 0x53, 0x49, + 0x77, 0x49, 0x41, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x6C, 0x50, 0x53, 0x56, 0x4E, 0x55, 0x52, 0x53, 0x42, 0x47, 0x62, 0x33, 0x56, 0x75, 0x5A, 0x47, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x56, 0x75, 0x5A, 0x47, + 0x39, 0x79, 0x63, 0x32, 0x56, 0x6B, 0x4D, 0x53, 0x67, 0x77, 0x4A, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x39, 0x50, 0x53, 0x56, 0x4E, 0x55, 0x52, 0x53, 0x42, 0x58, 0x53, 0x56, 0x4E, 0x6C, 0x53, 0x32, 0x56, 0x35, 0x49, 0x45, + 0x64, 0x73, 0x0A, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x64, 0x43, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, + 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x32, 0x42, 0x65, 0x33, 0x48, 0x45, 0x6F, 0x6B, 0x4B, 0x74, 0x61, 0x58, 0x0A, + 0x73, 0x63, 0x72, 0x69, 0x48, 0x76, 0x74, 0x39, 0x4F, 0x4F, 0x2B, 0x59, 0x39, 0x62, 0x49, 0x35, 0x6D, 0x45, 0x34, 0x6E, 0x75, 0x42, 0x46, 0x64, 0x65, 0x39, 0x49, 0x6C, 0x6C, 0x49, 0x69, 0x43, 0x46, 0x53, 0x5A, 0x71, 0x47, 0x7A, 0x47, 0x37, + 0x71, 0x46, 0x73, 0x68, 0x49, 0x53, 0x76, 0x59, 0x44, 0x30, 0x36, 0x66, 0x57, 0x76, 0x47, 0x78, 0x57, 0x75, 0x52, 0x35, 0x31, 0x6A, 0x49, 0x6A, 0x4B, 0x2B, 0x46, 0x54, 0x7A, 0x4A, 0x6C, 0x46, 0x58, 0x48, 0x74, 0x50, 0x0A, 0x72, 0x62, 0x79, + 0x2F, 0x68, 0x30, 0x6F, 0x4C, 0x53, 0x35, 0x64, 0x61, 0x71, 0x50, 0x5A, 0x49, 0x37, 0x48, 0x31, 0x37, 0x44, 0x63, 0x30, 0x68, 0x42, 0x74, 0x2B, 0x65, 0x46, 0x66, 0x31, 0x42, 0x69, 0x6B, 0x69, 0x33, 0x49, 0x50, 0x53, 0x68, 0x65, 0x68, 0x74, + 0x58, 0x31, 0x46, 0x31, 0x51, 0x2F, 0x37, 0x70, 0x6E, 0x32, 0x43, 0x4F, 0x5A, 0x48, 0x38, 0x67, 0x2F, 0x34, 0x39, 0x37, 0x2F, 0x62, 0x31, 0x74, 0x33, 0x73, 0x57, 0x74, 0x75, 0x75, 0x4D, 0x6C, 0x6B, 0x0A, 0x39, 0x2B, 0x48, 0x4B, 0x51, 0x55, + 0x59, 0x4F, 0x4B, 0x58, 0x48, 0x51, 0x75, 0x53, 0x50, 0x38, 0x79, 0x59, 0x46, 0x66, 0x54, 0x76, 0x64, 0x76, 0x33, 0x37, 0x2B, 0x45, 0x72, 0x58, 0x4E, 0x6B, 0x75, 0x37, 0x64, 0x43, 0x6A, 0x6D, 0x6E, 0x32, 0x31, 0x48, 0x59, 0x64, 0x66, 0x70, + 0x32, 0x6E, 0x75, 0x46, 0x65, 0x4B, 0x55, 0x57, 0x64, 0x79, 0x31, 0x39, 0x53, 0x6F, 0x75, 0x4A, 0x56, 0x55, 0x51, 0x48, 0x4D, 0x44, 0x39, 0x75, 0x72, 0x30, 0x36, 0x2F, 0x34, 0x6F, 0x0A, 0x51, 0x6E, 0x63, 0x2F, 0x6E, 0x53, 0x4D, 0x62, 0x73, + 0x72, 0x59, 0x39, 0x67, 0x42, 0x51, 0x48, 0x54, 0x43, 0x35, 0x50, 0x39, 0x39, 0x55, 0x4B, 0x46, 0x67, 0x32, 0x39, 0x5A, 0x6B, 0x4D, 0x33, 0x66, 0x69, 0x4E, 0x44, 0x65, 0x63, 0x4E, 0x41, 0x68, 0x76, 0x56, 0x4D, 0x4B, 0x64, 0x71, 0x4F, 0x6D, + 0x71, 0x30, 0x4E, 0x70, 0x51, 0x53, 0x48, 0x69, 0x42, 0x36, 0x46, 0x34, 0x2B, 0x6C, 0x54, 0x31, 0x5A, 0x76, 0x49, 0x69, 0x77, 0x4E, 0x6A, 0x65, 0x4F, 0x76, 0x67, 0x0A, 0x47, 0x55, 0x70, 0x75, 0x75, 0x79, 0x39, 0x72, 0x4D, 0x32, 0x52, 0x59, + 0x6B, 0x36, 0x31, 0x70, 0x76, 0x34, 0x38, 0x62, 0x37, 0x34, 0x4A, 0x49, 0x78, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x31, 0x45, 0x77, 0x54, 0x7A, 0x41, 0x4C, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, + 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, + 0x55, 0x4E, 0x51, 0x2F, 0x49, 0x4E, 0x6D, 0x4E, 0x65, 0x34, 0x71, 0x50, 0x73, 0x2B, 0x54, 0x74, 0x6D, 0x46, 0x63, 0x35, 0x52, 0x55, 0x75, 0x4F, 0x52, 0x6D, 0x6A, 0x30, 0x77, 0x45, 0x41, 0x59, 0x4A, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x41, 0x47, + 0x43, 0x4E, 0x78, 0x55, 0x42, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x41, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x45, + 0x42, 0x4D, 0x2B, 0x34, 0x65, 0x79, 0x6D, 0x59, 0x47, 0x51, 0x66, 0x70, 0x33, 0x46, 0x73, 0x4C, 0x41, 0x6D, 0x7A, 0x59, 0x68, 0x37, 0x4B, 0x7A, 0x4B, 0x4E, 0x62, 0x72, 0x67, 0x68, 0x63, 0x56, 0x69, 0x58, 0x66, 0x61, 0x34, 0x33, 0x46, 0x4B, + 0x38, 0x2B, 0x35, 0x2F, 0x65, 0x61, 0x34, 0x6E, 0x33, 0x32, 0x63, 0x5A, 0x69, 0x5A, 0x42, 0x4B, 0x70, 0x44, 0x0A, 0x64, 0x48, 0x69, 0x6A, 0x34, 0x30, 0x6C, 0x68, 0x50, 0x6E, 0x4F, 0x4D, 0x54, 0x5A, 0x54, 0x67, 0x2B, 0x58, 0x48, 0x45, 0x74, + 0x68, 0x59, 0x4F, 0x55, 0x33, 0x67, 0x66, 0x31, 0x71, 0x4B, 0x48, 0x4C, 0x77, 0x49, 0x35, 0x67, 0x53, 0x6B, 0x38, 0x72, 0x78, 0x57, 0x59, 0x49, 0x54, 0x44, 0x2B, 0x4B, 0x4A, 0x41, 0x41, 0x6A, 0x4E, 0x48, 0x68, 0x79, 0x2F, 0x70, 0x65, 0x79, + 0x50, 0x33, 0x34, 0x45, 0x45, 0x59, 0x37, 0x6F, 0x6E, 0x68, 0x43, 0x6B, 0x52, 0x64, 0x30, 0x0A, 0x56, 0x51, 0x72, 0x65, 0x55, 0x47, 0x64, 0x4E, 0x5A, 0x74, 0x47, 0x6E, 0x2F, 0x2F, 0x33, 0x5A, 0x77, 0x4C, 0x57, 0x6F, 0x6F, 0x34, 0x72, 0x4F, + 0x5A, 0x76, 0x55, 0x50, 0x51, 0x38, 0x32, 0x6E, 0x4B, 0x31, 0x64, 0x37, 0x59, 0x30, 0x5A, 0x71, 0x71, 0x69, 0x35, 0x53, 0x32, 0x50, 0x54, 0x74, 0x34, 0x57, 0x32, 0x74, 0x4B, 0x5A, 0x42, 0x34, 0x53, 0x4C, 0x72, 0x68, 0x49, 0x36, 0x71, 0x6A, + 0x69, 0x65, 0x79, 0x31, 0x71, 0x35, 0x62, 0x41, 0x74, 0x45, 0x75, 0x69, 0x0A, 0x48, 0x5A, 0x65, 0x65, 0x65, 0x76, 0x4A, 0x75, 0x51, 0x48, 0x48, 0x66, 0x61, 0x50, 0x46, 0x6C, 0x54, 0x63, 0x35, 0x38, 0x42, 0x64, 0x39, 0x54, 0x5A, 0x61, 0x6D, + 0x6C, 0x38, 0x4C, 0x47, 0x58, 0x42, 0x48, 0x41, 0x56, 0x52, 0x67, 0x4F, 0x59, 0x31, 0x4E, 0x4B, 0x2F, 0x56, 0x4C, 0x53, 0x67, 0x57, 0x48, 0x31, 0x53, 0x62, 0x39, 0x70, 0x57, 0x4A, 0x6D, 0x4C, 0x55, 0x32, 0x4E, 0x75, 0x4A, 0x4D, 0x57, 0x38, + 0x63, 0x38, 0x43, 0x4C, 0x43, 0x30, 0x32, 0x49, 0x63, 0x0A, 0x4E, 0x63, 0x31, 0x4D, 0x61, 0x52, 0x56, 0x55, 0x47, 0x70, 0x43, 0x59, 0x33, 0x75, 0x73, 0x65, 0x58, 0x38, 0x70, 0x33, 0x78, 0x38, 0x75, 0x4F, 0x50, 0x55, 0x4E, 0x70, 0x6E, 0x4A, + 0x70, 0x59, 0x30, 0x43, 0x51, 0x37, 0x33, 0x78, 0x74, 0x41, 0x6C, 0x6E, 0x34, 0x31, 0x72, 0x59, 0x48, 0x48, 0x54, 0x6E, 0x47, 0x36, 0x69, 0x42, 0x4D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x5A, 0x41, 0x46, 0x49, 0x52, 0x20, 0x52, 0x4F, 0x4F, 0x54, 0x20, 0x43, 0x41, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x63, 0x6A, + 0x43, 0x43, 0x41, 0x6C, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x50, 0x6F, 0x70, 0x64, 0x42, 0x2B, 0x78, 0x56, 0x30, 0x6A, 0x4C, 0x56, 0x74, 0x2B, 0x4F, 0x32, 0x58, 0x77, 0x48, 0x72, 0x4C, 0x64, 0x7A, 0x6B, 0x31, 0x75, + 0x51, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x55, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x55, + 0x45, 0x77, 0x78, 0x4B, 0x44, 0x41, 0x6D, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x30, 0x74, 0x79, 0x59, 0x57, 0x70, 0x76, 0x64, 0x32, 0x45, 0x67, 0x53, 0x58, 0x70, 0x69, 0x59, 0x53, 0x42, 0x53, 0x62, 0x33, 0x70, 0x73, 0x61, + 0x57, 0x4E, 0x36, 0x5A, 0x57, 0x35, 0x70, 0x62, 0x33, 0x64, 0x68, 0x49, 0x46, 0x4D, 0x75, 0x51, 0x53, 0x34, 0x78, 0x47, 0x44, 0x41, 0x57, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x4D, 0x4D, 0x44, 0x31, 0x4E, 0x61, 0x51, 0x55, 0x5A, 0x4A, + 0x55, 0x69, 0x42, 0x53, 0x54, 0x30, 0x39, 0x55, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x6A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x54, 0x45, 0x77, 0x4D, 0x54, 0x6B, 0x77, 0x4E, 0x7A, 0x51, 0x7A, 0x4D, 0x7A, 0x42, 0x61, 0x46, 0x77, 0x30, 0x7A, + 0x4E, 0x54, 0x45, 0x77, 0x4D, 0x54, 0x6B, 0x77, 0x4E, 0x7A, 0x51, 0x7A, 0x4D, 0x7A, 0x42, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x42, 0x4D, 0x4D, 0x53, 0x67, + 0x77, 0x4A, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x39, 0x4C, 0x63, 0x6D, 0x46, 0x71, 0x62, 0x33, 0x64, 0x68, 0x49, 0x45, 0x6C, 0x36, 0x59, 0x6D, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x36, 0x62, 0x47, 0x6C, 0x6A, 0x65, 0x6D, 0x56, + 0x75, 0x61, 0x57, 0x39, 0x33, 0x59, 0x53, 0x42, 0x54, 0x4C, 0x6B, 0x45, 0x75, 0x4D, 0x52, 0x67, 0x77, 0x46, 0x67, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x44, 0x44, 0x41, 0x39, 0x54, 0x57, 0x6B, 0x46, 0x47, 0x53, 0x56, 0x49, 0x67, 0x55, 0x6B, + 0x39, 0x50, 0x56, 0x43, 0x42, 0x44, 0x51, 0x54, 0x49, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, + 0x41, 0x77, 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x33, 0x76, 0x44, 0x35, 0x51, 0x0A, 0x71, 0x45, 0x76, 0x4E, 0x51, 0x4C, 0x58, 0x4F, 0x59, 0x65, 0x65, 0x57, 0x79, 0x72, 0x53, 0x68, 0x32, 0x67, 0x77, 0x69, 0x73, + 0x50, 0x71, 0x31, 0x65, 0x33, 0x59, 0x41, 0x64, 0x34, 0x77, 0x4C, 0x7A, 0x33, 0x32, 0x6F, 0x68, 0x73, 0x77, 0x6D, 0x55, 0x65, 0x51, 0x67, 0x50, 0x59, 0x55, 0x4D, 0x31, 0x6C, 0x6A, 0x6A, 0x35, 0x2F, 0x51, 0x71, 0x47, 0x4A, 0x33, 0x61, 0x30, + 0x61, 0x34, 0x6D, 0x37, 0x75, 0x74, 0x54, 0x33, 0x50, 0x53, 0x51, 0x31, 0x68, 0x4E, 0x4B, 0x0A, 0x44, 0x4A, 0x41, 0x38, 0x77, 0x2F, 0x54, 0x61, 0x30, 0x6F, 0x34, 0x4E, 0x6B, 0x6A, 0x72, 0x63, 0x73, 0x62, 0x48, 0x2F, 0x4F, 0x4E, 0x37, 0x44, + 0x75, 0x69, 0x31, 0x66, 0x67, 0x4C, 0x6B, 0x43, 0x76, 0x55, 0x71, 0x64, 0x47, 0x77, 0x2B, 0x30, 0x77, 0x38, 0x4C, 0x42, 0x5A, 0x77, 0x50, 0x64, 0x33, 0x42, 0x75, 0x63, 0x50, 0x62, 0x4F, 0x77, 0x33, 0x67, 0x41, 0x65, 0x71, 0x44, 0x52, 0x48, + 0x75, 0x35, 0x72, 0x72, 0x2F, 0x67, 0x73, 0x55, 0x76, 0x54, 0x61, 0x45, 0x0A, 0x32, 0x67, 0x30, 0x67, 0x76, 0x2F, 0x70, 0x62, 0x79, 0x36, 0x6B, 0x57, 0x49, 0x4B, 0x30, 0x35, 0x59, 0x4F, 0x34, 0x76, 0x64, 0x62, 0x62, 0x6E, 0x6C, 0x35, 0x7A, + 0x35, 0x50, 0x76, 0x31, 0x2B, 0x54, 0x57, 0x39, 0x4E, 0x4C, 0x2B, 0x2B, 0x49, 0x44, 0x57, 0x72, 0x36, 0x33, 0x66, 0x45, 0x39, 0x62, 0x69, 0x43, 0x6C, 0x6F, 0x42, 0x4B, 0x30, 0x54, 0x58, 0x43, 0x35, 0x7A, 0x74, 0x64, 0x79, 0x4F, 0x34, 0x6D, + 0x54, 0x70, 0x34, 0x43, 0x45, 0x48, 0x43, 0x64, 0x4A, 0x0A, 0x63, 0x6B, 0x6D, 0x31, 0x2F, 0x7A, 0x75, 0x56, 0x6E, 0x73, 0x48, 0x4D, 0x79, 0x41, 0x48, 0x73, 0x36, 0x41, 0x36, 0x4B, 0x43, 0x70, 0x62, 0x6E, 0x73, 0x36, 0x61, 0x48, 0x35, 0x64, + 0x62, 0x35, 0x42, 0x53, 0x73, 0x4E, 0x6C, 0x30, 0x42, 0x77, 0x50, 0x4C, 0x71, 0x73, 0x64, 0x56, 0x71, 0x63, 0x31, 0x55, 0x32, 0x64, 0x41, 0x67, 0x72, 0x53, 0x53, 0x35, 0x74, 0x6D, 0x53, 0x30, 0x59, 0x48, 0x46, 0x32, 0x57, 0x74, 0x6E, 0x32, + 0x79, 0x49, 0x41, 0x4E, 0x77, 0x69, 0x0A, 0x69, 0x65, 0x44, 0x68, 0x5A, 0x4E, 0x52, 0x6E, 0x76, 0x44, 0x46, 0x35, 0x59, 0x54, 0x79, 0x37, 0x79, 0x6B, 0x48, 0x4E, 0x58, 0x47, 0x6F, 0x41, 0x79, 0x44, 0x77, 0x34, 0x6A, 0x6C, 0x69, 0x76, 0x41, + 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, + 0x52, 0x30, 0x50, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x51, 0x75, 0x46, 0x71, 0x6C, 0x4B, 0x47, 0x4C, 0x58, 0x4C, + 0x7A, 0x50, 0x56, 0x76, 0x55, 0x50, 0x4D, 0x6A, 0x58, 0x2F, 0x68, 0x64, 0x35, 0x36, 0x7A, 0x77, 0x79, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, + 0x0A, 0x41, 0x51, 0x45, 0x41, 0x74, 0x58, 0x50, 0x34, 0x41, 0x39, 0x78, 0x5A, 0x57, 0x78, 0x31, 0x32, 0x36, 0x61, 0x4D, 0x71, 0x65, 0x35, 0x41, 0x6F, 0x73, 0x6B, 0x33, 0x41, 0x4D, 0x30, 0x2B, 0x71, 0x6D, 0x72, 0x48, 0x55, 0x75, 0x4F, 0x51, + 0x6E, 0x2F, 0x36, 0x6D, 0x57, 0x6D, 0x63, 0x35, 0x47, 0x34, 0x47, 0x31, 0x38, 0x54, 0x4B, 0x49, 0x34, 0x70, 0x41, 0x5A, 0x77, 0x38, 0x50, 0x52, 0x42, 0x45, 0x65, 0x77, 0x2F, 0x52, 0x34, 0x30, 0x2F, 0x63, 0x6F, 0x66, 0x35, 0x0A, 0x4F, 0x2F, + 0x32, 0x6B, 0x62, 0x79, 0x74, 0x54, 0x41, 0x4F, 0x44, 0x2F, 0x4F, 0x62, 0x6C, 0x71, 0x42, 0x77, 0x37, 0x72, 0x48, 0x52, 0x7A, 0x32, 0x6F, 0x6E, 0x4B, 0x51, 0x79, 0x34, 0x49, 0x39, 0x45, 0x59, 0x4B, 0x4C, 0x30, 0x72, 0x75, 0x66, 0x4B, 0x71, + 0x38, 0x68, 0x35, 0x6D, 0x4F, 0x47, 0x6E, 0x58, 0x6B, 0x5A, 0x37, 0x2F, 0x65, 0x37, 0x44, 0x44, 0x57, 0x51, 0x77, 0x34, 0x72, 0x74, 0x54, 0x77, 0x2F, 0x31, 0x7A, 0x42, 0x4C, 0x5A, 0x70, 0x44, 0x36, 0x37, 0x0A, 0x6F, 0x50, 0x77, 0x67, 0x6C, + 0x56, 0x39, 0x50, 0x4A, 0x69, 0x38, 0x52, 0x49, 0x34, 0x4E, 0x4F, 0x64, 0x51, 0x63, 0x50, 0x76, 0x35, 0x76, 0x52, 0x74, 0x42, 0x33, 0x70, 0x45, 0x41, 0x54, 0x2B, 0x79, 0x6D, 0x43, 0x50, 0x6F, 0x6B, 0x79, 0x34, 0x72, 0x63, 0x2F, 0x68, 0x6B, + 0x41, 0x2F, 0x4E, 0x72, 0x67, 0x72, 0x48, 0x58, 0x58, 0x75, 0x33, 0x55, 0x4E, 0x4C, 0x55, 0x59, 0x66, 0x72, 0x56, 0x46, 0x64, 0x76, 0x58, 0x6E, 0x34, 0x64, 0x52, 0x56, 0x4F, 0x75, 0x6C, 0x0A, 0x34, 0x2B, 0x76, 0x4A, 0x68, 0x61, 0x41, 0x6C, + 0x49, 0x44, 0x66, 0x37, 0x6A, 0x73, 0x34, 0x4D, 0x4E, 0x49, 0x54, 0x68, 0x50, 0x49, 0x47, 0x79, 0x64, 0x30, 0x35, 0x44, 0x70, 0x59, 0x68, 0x66, 0x68, 0x6D, 0x65, 0x68, 0x50, 0x65, 0x61, 0x30, 0x58, 0x47, 0x47, 0x32, 0x50, 0x74, 0x76, 0x2B, + 0x74, 0x79, 0x6A, 0x46, 0x6F, 0x67, 0x65, 0x75, 0x74, 0x63, 0x72, 0x4B, 0x6A, 0x53, 0x6F, 0x53, 0x37, 0x35, 0x66, 0x74, 0x77, 0x6A, 0x43, 0x6B, 0x79, 0x53, 0x70, 0x36, 0x0A, 0x2B, 0x2F, 0x4E, 0x4E, 0x49, 0x78, 0x75, 0x5A, 0x4D, 0x7A, 0x53, + 0x67, 0x4C, 0x76, 0x57, 0x70, 0x43, 0x7A, 0x2F, 0x55, 0x58, 0x65, 0x48, 0x50, 0x68, 0x4A, 0x2F, 0x69, 0x47, 0x63, 0x4A, 0x66, 0x69, 0x74, 0x59, 0x67, 0x48, 0x75, 0x4E, 0x7A, 0x74, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, + 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x75, 0x6D, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x20, 0x4E, 0x65, 0x74, 0x77, + 0x6F, 0x72, 0x6B, 0x20, 0x43, 0x41, 0x20, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x30, 0x6A, 0x43, 0x43, 0x41, 0x37, 0x71, 0x67, 0x41, 0x77, 0x49, + 0x42, 0x41, 0x67, 0x49, 0x51, 0x49, 0x64, 0x62, 0x51, 0x53, 0x6B, 0x38, 0x6C, 0x44, 0x38, 0x6B, 0x79, 0x4E, 0x2F, 0x79, 0x71, 0x58, 0x68, 0x4B, 0x4E, 0x36, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, + 0x42, 0x41, 0x51, 0x30, 0x46, 0x41, 0x44, 0x43, 0x42, 0x67, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x55, 0x45, 0x77, 0x78, 0x49, 0x6A, 0x41, 0x67, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, + 0x6F, 0x54, 0x47, 0x56, 0x56, 0x75, 0x61, 0x58, 0x70, 0x6C, 0x64, 0x47, 0x38, 0x67, 0x56, 0x47, 0x56, 0x6A, 0x61, 0x47, 0x35, 0x76, 0x62, 0x47, 0x39, 0x6E, 0x61, 0x57, 0x56, 0x7A, 0x49, 0x46, 0x4D, 0x75, 0x51, 0x53, 0x34, 0x78, 0x4A, 0x7A, + 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x48, 0x6B, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x31, 0x0A, 0x62, 0x53, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, + 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x45, 0x6B, 0x4D, 0x43, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x62, 0x51, 0x32, 0x56, 0x79, 0x64, 0x48, 0x56, 0x74, 0x49, 0x46, 0x52, 0x79, 0x64, + 0x58, 0x4E, 0x30, 0x5A, 0x57, 0x51, 0x67, 0x54, 0x6D, 0x56, 0x30, 0x64, 0x32, 0x39, 0x79, 0x0A, 0x61, 0x79, 0x42, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x43, 0x49, 0x59, 0x44, 0x7A, 0x49, 0x77, 0x4D, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x41, 0x32, + 0x4D, 0x44, 0x67, 0x7A, 0x4F, 0x54, 0x55, 0x32, 0x57, 0x68, 0x67, 0x50, 0x4D, 0x6A, 0x41, 0x30, 0x4E, 0x6A, 0x45, 0x77, 0x4D, 0x44, 0x59, 0x77, 0x4F, 0x44, 0x4D, 0x35, 0x4E, 0x54, 0x5A, 0x61, 0x4D, 0x49, 0x47, 0x41, 0x4D, 0x51, 0x73, 0x77, + 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x51, 0x0A, 0x54, 0x44, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, 0x56, 0x57, 0x35, 0x70, 0x65, 0x6D, 0x56, 0x30, 0x62, 0x79, 0x42, + 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x67, 0x55, 0x79, 0x35, 0x42, 0x4C, 0x6A, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x65, 0x51, 0x32, 0x56, + 0x79, 0x64, 0x48, 0x56, 0x74, 0x49, 0x45, 0x4E, 0x6C, 0x0A, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x53, + 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x74, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, 0x30, 0x67, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x6C, 0x5A, 0x43, 0x42, 0x4F, 0x5A, 0x58, 0x52, 0x33, 0x62, 0x33, + 0x4A, 0x72, 0x49, 0x45, 0x4E, 0x42, 0x0A, 0x49, 0x44, 0x49, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, + 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x39, 0x2B, 0x58, 0x6A, 0x34, 0x35, 0x74, 0x57, 0x41, 0x44, 0x47, 0x53, 0x64, 0x68, 0x68, 0x75, 0x57, 0x5A, 0x47, 0x63, 0x2F, 0x49, 0x6A, 0x6F, 0x65, 0x64, + 0x51, 0x46, 0x39, 0x0A, 0x37, 0x2F, 0x74, 0x63, 0x5A, 0x34, 0x7A, 0x4A, 0x7A, 0x46, 0x78, 0x72, 0x71, 0x5A, 0x48, 0x6D, 0x75, 0x55, 0x4C, 0x6C, 0x49, 0x45, 0x75, 0x62, 0x32, 0x70, 0x74, 0x37, 0x75, 0x5A, 0x6C, 0x64, 0x32, 0x5A, 0x75, 0x41, + 0x53, 0x39, 0x65, 0x45, 0x51, 0x43, 0x73, 0x6E, 0x30, 0x2B, 0x69, 0x36, 0x4D, 0x4C, 0x73, 0x2B, 0x43, 0x52, 0x71, 0x6E, 0x53, 0x5A, 0x58, 0x76, 0x4B, 0x30, 0x41, 0x6B, 0x77, 0x70, 0x66, 0x48, 0x70, 0x2B, 0x36, 0x62, 0x4A, 0x65, 0x2B, 0x6F, + 0x0A, 0x43, 0x67, 0x43, 0x58, 0x68, 0x56, 0x71, 0x71, 0x6E, 0x64, 0x77, 0x70, 0x79, 0x65, 0x49, 0x31, 0x42, 0x2B, 0x74, 0x77, 0x54, 0x55, 0x72, 0x57, 0x77, 0x62, 0x4E, 0x57, 0x75, 0x4B, 0x46, 0x42, 0x4F, 0x4A, 0x76, 0x52, 0x2B, 0x7A, 0x46, + 0x2F, 0x6A, 0x2B, 0x42, 0x66, 0x34, 0x62, 0x45, 0x2F, 0x44, 0x34, 0x34, 0x57, 0x53, 0x57, 0x44, 0x58, 0x42, 0x6F, 0x30, 0x59, 0x2B, 0x61, 0x6F, 0x6D, 0x45, 0x4B, 0x73, 0x71, 0x30, 0x39, 0x44, 0x52, 0x5A, 0x34, 0x30, 0x62, 0x0A, 0x52, 0x72, + 0x35, 0x48, 0x4D, 0x4E, 0x55, 0x75, 0x63, 0x74, 0x48, 0x46, 0x59, 0x39, 0x72, 0x6E, 0x59, 0x33, 0x6C, 0x45, 0x66, 0x6B, 0x74, 0x6A, 0x4A, 0x49, 0x6D, 0x47, 0x4C, 0x6A, 0x51, 0x2F, 0x4B, 0x55, 0x78, 0x53, 0x69, 0x79, 0x71, 0x6E, 0x77, 0x4F, + 0x4B, 0x52, 0x4B, 0x49, 0x6D, 0x35, 0x77, 0x46, 0x76, 0x35, 0x48, 0x64, 0x6E, 0x6E, 0x4A, 0x36, 0x33, 0x2F, 0x6D, 0x67, 0x4B, 0x58, 0x77, 0x63, 0x5A, 0x51, 0x6B, 0x70, 0x73, 0x43, 0x4C, 0x4C, 0x32, 0x70, 0x0A, 0x75, 0x54, 0x52, 0x5A, 0x43, + 0x72, 0x2B, 0x45, 0x53, 0x76, 0x2F, 0x66, 0x2F, 0x72, 0x4F, 0x66, 0x36, 0x39, 0x6D, 0x65, 0x34, 0x4A, 0x67, 0x6A, 0x37, 0x4B, 0x5A, 0x72, 0x64, 0x78, 0x59, 0x71, 0x32, 0x38, 0x79, 0x74, 0x4F, 0x78, 0x79, 0x6B, 0x68, 0x39, 0x78, 0x47, 0x63, + 0x31, 0x34, 0x5A, 0x59, 0x6D, 0x68, 0x46, 0x56, 0x2B, 0x53, 0x51, 0x67, 0x6B, 0x4B, 0x37, 0x51, 0x74, 0x62, 0x77, 0x59, 0x65, 0x44, 0x42, 0x6F, 0x7A, 0x31, 0x6D, 0x6F, 0x31, 0x33, 0x30, 0x0A, 0x47, 0x4F, 0x36, 0x49, 0x79, 0x59, 0x30, 0x58, + 0x52, 0x53, 0x6D, 0x5A, 0x4D, 0x6E, 0x55, 0x43, 0x4D, 0x65, 0x34, 0x70, 0x4A, 0x73, 0x68, 0x72, 0x41, 0x75, 0x61, 0x31, 0x59, 0x6B, 0x56, 0x2F, 0x4E, 0x78, 0x56, 0x61, 0x49, 0x32, 0x69, 0x4A, 0x31, 0x44, 0x37, 0x65, 0x54, 0x69, 0x65, 0x77, + 0x38, 0x45, 0x41, 0x4D, 0x76, 0x45, 0x30, 0x58, 0x79, 0x30, 0x32, 0x69, 0x73, 0x78, 0x37, 0x51, 0x42, 0x6C, 0x72, 0x64, 0x39, 0x70, 0x50, 0x50, 0x56, 0x33, 0x57, 0x5A, 0x0A, 0x39, 0x66, 0x71, 0x47, 0x47, 0x6D, 0x64, 0x34, 0x73, 0x37, 0x2B, + 0x57, 0x2F, 0x6A, 0x54, 0x63, 0x76, 0x65, 0x64, 0x53, 0x56, 0x75, 0x57, 0x7A, 0x35, 0x58, 0x56, 0x37, 0x31, 0x30, 0x47, 0x52, 0x42, 0x64, 0x78, 0x64, 0x61, 0x65, 0x4F, 0x56, 0x44, 0x55, 0x4F, 0x35, 0x2F, 0x49, 0x4F, 0x57, 0x4F, 0x5A, 0x56, + 0x37, 0x62, 0x49, 0x42, 0x61, 0x54, 0x78, 0x4E, 0x79, 0x78, 0x74, 0x64, 0x39, 0x4B, 0x58, 0x70, 0x45, 0x75, 0x6C, 0x4B, 0x6B, 0x4B, 0x74, 0x56, 0x42, 0x0A, 0x52, 0x67, 0x6B, 0x67, 0x2F, 0x69, 0x4B, 0x67, 0x74, 0x6C, 0x73, 0x77, 0x6A, 0x62, + 0x79, 0x4A, 0x44, 0x4E, 0x58, 0x58, 0x63, 0x50, 0x69, 0x48, 0x55, 0x76, 0x33, 0x61, 0x37, 0x36, 0x78, 0x52, 0x4C, 0x67, 0x65, 0x7A, 0x54, 0x76, 0x37, 0x51, 0x43, 0x64, 0x70, 0x77, 0x37, 0x35, 0x6A, 0x36, 0x56, 0x75, 0x5A, 0x74, 0x32, 0x37, + 0x56, 0x58, 0x53, 0x39, 0x7A, 0x6C, 0x4C, 0x43, 0x55, 0x56, 0x79, 0x4A, 0x34, 0x75, 0x65, 0x45, 0x37, 0x34, 0x32, 0x70, 0x79, 0x65, 0x0A, 0x68, 0x69, 0x7A, 0x4B, 0x56, 0x2F, 0x4D, 0x61, 0x35, 0x63, 0x69, 0x53, 0x69, 0x78, 0x71, 0x43, 0x6C, + 0x6E, 0x72, 0x44, 0x76, 0x46, 0x41, 0x53, 0x61, 0x64, 0x67, 0x4F, 0x57, 0x6B, 0x61, 0x4C, 0x4F, 0x75, 0x73, 0x6D, 0x2B, 0x69, 0x50, 0x4A, 0x74, 0x72, 0x43, 0x42, 0x76, 0x6B, 0x49, 0x41, 0x70, 0x50, 0x6A, 0x57, 0x2F, 0x6A, 0x41, 0x75, 0x78, + 0x39, 0x4A, 0x47, 0x39, 0x75, 0x57, 0x4F, 0x64, 0x66, 0x33, 0x79, 0x7A, 0x4C, 0x6E, 0x51, 0x68, 0x31, 0x76, 0x4D, 0x0A, 0x42, 0x68, 0x42, 0x67, 0x75, 0x34, 0x4D, 0x31, 0x74, 0x31, 0x35, 0x6E, 0x33, 0x6B, 0x66, 0x73, 0x6D, 0x55, 0x6A, 0x78, + 0x70, 0x4B, 0x45, 0x56, 0x2F, 0x71, 0x32, 0x4D, 0x59, 0x6F, 0x34, 0x35, 0x56, 0x55, 0x38, 0x35, 0x46, 0x72, 0x6D, 0x78, 0x59, 0x35, 0x33, 0x2F, 0x74, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, + 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x32, 0x6F, 0x56, 0x51, + 0x35, 0x41, 0x73, 0x4F, 0x67, 0x50, 0x34, 0x36, 0x4B, 0x76, 0x50, 0x72, 0x55, 0x2B, 0x42, 0x79, 0x6D, 0x30, 0x54, 0x6F, 0x4F, 0x2F, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, + 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4E, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x48, 0x47, 0x6C, 0x44, 0x73, 0x37, 0x6B, 0x36, 0x62, + 0x38, 0x2F, 0x4F, 0x4E, 0x57, 0x4A, 0x57, 0x73, 0x51, 0x43, 0x59, 0x66, 0x74, 0x4D, 0x78, 0x52, 0x51, 0x58, 0x4C, 0x59, 0x74, 0x50, 0x55, 0x32, 0x73, 0x51, 0x46, 0x2F, 0x78, 0x6C, 0x68, 0x4D, 0x63, 0x51, 0x53, 0x5A, 0x44, 0x65, 0x32, 0x38, + 0x63, 0x6D, 0x6B, 0x34, 0x67, 0x6D, 0x62, 0x33, 0x44, 0x57, 0x0A, 0x41, 0x6C, 0x34, 0x35, 0x6F, 0x50, 0x65, 0x50, 0x71, 0x35, 0x61, 0x31, 0x70, 0x52, 0x4E, 0x63, 0x67, 0x52, 0x52, 0x74, 0x44, 0x6F, 0x47, 0x43, 0x45, 0x52, 0x75, 0x4B, 0x54, + 0x73, 0x5A, 0x50, 0x70, 0x64, 0x31, 0x69, 0x48, 0x6B, 0x54, 0x66, 0x43, 0x56, 0x6E, 0x30, 0x57, 0x33, 0x63, 0x4C, 0x4E, 0x2B, 0x6D, 0x4C, 0x49, 0x4D, 0x62, 0x34, 0x43, 0x6B, 0x34, 0x75, 0x57, 0x42, 0x7A, 0x72, 0x4D, 0x39, 0x44, 0x50, 0x68, + 0x6D, 0x44, 0x4A, 0x32, 0x76, 0x75, 0x41, 0x0A, 0x4C, 0x35, 0x35, 0x4D, 0x59, 0x49, 0x52, 0x34, 0x50, 0x53, 0x46, 0x6B, 0x31, 0x76, 0x74, 0x42, 0x48, 0x78, 0x67, 0x50, 0x35, 0x38, 0x6C, 0x31, 0x63, 0x62, 0x32, 0x39, 0x58, 0x4E, 0x34, 0x30, + 0x68, 0x7A, 0x35, 0x42, 0x73, 0x41, 0x37, 0x32, 0x75, 0x64, 0x59, 0x2F, 0x43, 0x52, 0x4F, 0x57, 0x46, 0x43, 0x2F, 0x65, 0x6D, 0x68, 0x31, 0x61, 0x75, 0x56, 0x62, 0x4F, 0x4E, 0x54, 0x71, 0x77, 0x58, 0x33, 0x42, 0x4E, 0x58, 0x75, 0x4D, 0x70, + 0x38, 0x53, 0x4D, 0x6F, 0x0A, 0x63, 0x6C, 0x6D, 0x32, 0x71, 0x38, 0x4B, 0x4D, 0x5A, 0x69, 0x59, 0x63, 0x64, 0x79, 0x77, 0x6D, 0x64, 0x6A, 0x57, 0x4C, 0x4B, 0x4B, 0x64, 0x70, 0x6F, 0x50, 0x6B, 0x37, 0x39, 0x53, 0x50, 0x64, 0x68, 0x52, 0x42, + 0x30, 0x79, 0x5A, 0x41, 0x44, 0x56, 0x70, 0x48, 0x6E, 0x72, 0x37, 0x70, 0x48, 0x31, 0x42, 0x4B, 0x58, 0x45, 0x53, 0x4C, 0x6A, 0x6F, 0x6B, 0x6D, 0x55, 0x62, 0x4F, 0x65, 0x33, 0x6C, 0x45, 0x75, 0x36, 0x4C, 0x61, 0x54, 0x61, 0x4D, 0x34, 0x74, + 0x4D, 0x0A, 0x70, 0x6B, 0x54, 0x2F, 0x57, 0x6A, 0x7A, 0x47, 0x48, 0x57, 0x54, 0x59, 0x74, 0x54, 0x48, 0x6B, 0x70, 0x6A, 0x78, 0x36, 0x71, 0x46, 0x63, 0x4C, 0x32, 0x2B, 0x31, 0x68, 0x47, 0x73, 0x76, 0x78, 0x7A, 0x6E, 0x4E, 0x33, 0x59, 0x36, + 0x53, 0x48, 0x62, 0x30, 0x78, 0x52, 0x4F, 0x4E, 0x62, 0x6B, 0x58, 0x38, 0x65, 0x66, 0x74, 0x6F, 0x45, 0x71, 0x35, 0x49, 0x56, 0x49, 0x65, 0x56, 0x68, 0x65, 0x4F, 0x2F, 0x6A, 0x62, 0x41, 0x6F, 0x4A, 0x6E, 0x77, 0x54, 0x6E, 0x62, 0x0A, 0x77, + 0x33, 0x52, 0x4C, 0x50, 0x54, 0x59, 0x65, 0x2B, 0x53, 0x6D, 0x54, 0x69, 0x47, 0x68, 0x62, 0x71, 0x45, 0x51, 0x5A, 0x49, 0x66, 0x43, 0x6E, 0x36, 0x49, 0x45, 0x4E, 0x4C, 0x4F, 0x69, 0x54, 0x4E, 0x72, 0x51, 0x33, 0x73, 0x73, 0x71, 0x77, 0x47, + 0x79, 0x5A, 0x36, 0x6D, 0x69, 0x55, 0x66, 0x6D, 0x70, 0x71, 0x41, 0x6E, 0x6B, 0x73, 0x71, 0x50, 0x2F, 0x75, 0x6A, 0x6D, 0x76, 0x35, 0x7A, 0x4D, 0x6E, 0x48, 0x43, 0x6E, 0x73, 0x5A, 0x79, 0x34, 0x59, 0x70, 0x6F, 0x0A, 0x4A, 0x2F, 0x48, 0x6B, + 0x44, 0x37, 0x54, 0x45, 0x54, 0x4B, 0x56, 0x68, 0x6B, 0x2F, 0x69, 0x58, 0x45, 0x41, 0x63, 0x71, 0x4D, 0x43, 0x57, 0x70, 0x75, 0x63, 0x68, 0x78, 0x75, 0x4F, 0x39, 0x6F, 0x7A, 0x43, 0x31, 0x2B, 0x39, 0x65, 0x42, 0x2B, 0x44, 0x34, 0x4B, 0x6F, + 0x62, 0x37, 0x61, 0x36, 0x62, 0x49, 0x4E, 0x44, 0x64, 0x38, 0x32, 0x4B, 0x6B, 0x68, 0x65, 0x68, 0x6E, 0x6C, 0x74, 0x34, 0x46, 0x6A, 0x31, 0x46, 0x34, 0x6A, 0x4E, 0x79, 0x33, 0x65, 0x46, 0x6D, 0x0A, 0x79, 0x70, 0x6E, 0x54, 0x79, 0x63, 0x55, + 0x6D, 0x2F, 0x51, 0x31, 0x6F, 0x42, 0x45, 0x61, 0x75, 0x74, 0x74, 0x6D, 0x62, 0x6A, 0x4C, 0x34, 0x5A, 0x76, 0x72, 0x48, 0x47, 0x38, 0x68, 0x6E, 0x6A, 0x58, 0x41, 0x4C, 0x4B, 0x4C, 0x4E, 0x68, 0x76, 0x53, 0x67, 0x66, 0x5A, 0x79, 0x54, 0x58, + 0x61, 0x51, 0x48, 0x58, 0x79, 0x78, 0x4B, 0x63, 0x5A, 0x62, 0x35, 0x35, 0x43, 0x45, 0x4A, 0x68, 0x31, 0x35, 0x70, 0x57, 0x4C, 0x59, 0x4C, 0x7A, 0x74, 0x78, 0x52, 0x4C, 0x58, 0x0A, 0x69, 0x73, 0x37, 0x56, 0x6D, 0x46, 0x78, 0x57, 0x6C, 0x67, + 0x50, 0x46, 0x37, 0x6E, 0x63, 0x47, 0x4E, 0x66, 0x2F, 0x50, 0x35, 0x4F, 0x34, 0x2F, 0x45, 0x32, 0x48, 0x75, 0x32, 0x39, 0x6F, 0x74, 0x68, 0x66, 0x44, 0x4E, 0x72, 0x70, 0x32, 0x79, 0x47, 0x41, 0x6C, 0x46, 0x77, 0x35, 0x4B, 0x68, 0x63, 0x68, + 0x66, 0x38, 0x52, 0x37, 0x61, 0x67, 0x43, 0x79, 0x7A, 0x78, 0x78, 0x4E, 0x35, 0x44, 0x61, 0x41, 0x68, 0x71, 0x58, 0x7A, 0x76, 0x77, 0x64, 0x6D, 0x50, 0x37, 0x0A, 0x7A, 0x41, 0x59, 0x73, 0x70, 0x73, 0x62, 0x69, 0x44, 0x72, 0x57, 0x35, 0x76, + 0x69, 0x53, 0x50, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x48, 0x65, 0x6C, 0x6C, 0x65, 0x6E, 0x69, 0x63, 0x20, + 0x41, 0x63, 0x61, 0x64, 0x65, 0x6D, 0x69, 0x63, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x52, 0x65, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x20, 0x49, 0x6E, 0x73, 0x74, 0x69, 0x74, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x43, + 0x41, 0x20, 0x32, 0x30, 0x31, 0x35, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x47, 0x43, 0x7A, 0x43, 0x43, 0x41, 0x2F, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, + 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x70, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x31, 0x49, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x0A, 0x42, 0x6B, 0x46, 0x30, 0x61, 0x47, 0x56, 0x75, 0x63, 0x7A, 0x46, 0x45, 0x4D, 0x45, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x37, 0x53, 0x47, 0x56, 0x73, 0x62, 0x47, 0x56, 0x75, + 0x61, 0x57, 0x4D, 0x67, 0x51, 0x57, 0x4E, 0x68, 0x5A, 0x47, 0x56, 0x74, 0x61, 0x57, 0x4D, 0x67, 0x59, 0x57, 0x35, 0x6B, 0x49, 0x46, 0x4A, 0x6C, 0x63, 0x32, 0x56, 0x68, 0x63, 0x6D, 0x4E, 0x6F, 0x49, 0x45, 0x6C, 0x75, 0x63, 0x33, 0x52, 0x70, + 0x64, 0x48, 0x56, 0x30, 0x0A, 0x61, 0x57, 0x39, 0x75, 0x63, 0x79, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x4C, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x51, 0x44, 0x41, 0x2B, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4E, 0x30, 0x68, 0x6C, 0x62, 0x47, 0x78, 0x6C, 0x62, 0x6D, 0x6C, 0x6A, 0x49, 0x45, 0x46, 0x6A, 0x59, 0x57, 0x52, 0x6C, 0x62, 0x57, 0x6C, 0x6A, 0x49, 0x47, 0x46, 0x75, 0x5A, 0x43, 0x42, 0x53, 0x5A, 0x58, 0x4E, + 0x6C, 0x0A, 0x59, 0x58, 0x4A, 0x6A, 0x61, 0x43, 0x42, 0x4A, 0x62, 0x6E, 0x4E, 0x30, 0x61, 0x58, 0x52, 0x31, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6E, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x54, + 0x55, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x55, 0x77, 0x4E, 0x7A, 0x41, 0x33, 0x4D, 0x54, 0x41, 0x78, 0x4D, 0x54, 0x49, 0x78, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x41, 0x77, 0x4E, 0x6A, 0x4D, 0x77, 0x4D, 0x54, 0x41, 0x78, 0x0A, 0x4D, + 0x54, 0x49, 0x78, 0x57, 0x6A, 0x43, 0x42, 0x70, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x31, 0x49, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x42, + 0x6B, 0x46, 0x30, 0x61, 0x47, 0x56, 0x75, 0x63, 0x7A, 0x46, 0x45, 0x4D, 0x45, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x37, 0x53, 0x47, 0x56, 0x73, 0x62, 0x47, 0x56, 0x75, 0x61, 0x57, 0x4D, 0x67, 0x0A, 0x51, 0x57, 0x4E, 0x68, + 0x5A, 0x47, 0x56, 0x74, 0x61, 0x57, 0x4D, 0x67, 0x59, 0x57, 0x35, 0x6B, 0x49, 0x46, 0x4A, 0x6C, 0x63, 0x32, 0x56, 0x68, 0x63, 0x6D, 0x4E, 0x6F, 0x49, 0x45, 0x6C, 0x75, 0x63, 0x33, 0x52, 0x70, 0x64, 0x48, 0x56, 0x30, 0x61, 0x57, 0x39, 0x75, + 0x63, 0x79, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x4C, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x51, 0x44, 0x41, 0x2B, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x4D, 0x54, 0x4E, 0x30, 0x68, + 0x6C, 0x62, 0x47, 0x78, 0x6C, 0x62, 0x6D, 0x6C, 0x6A, 0x49, 0x45, 0x46, 0x6A, 0x59, 0x57, 0x52, 0x6C, 0x62, 0x57, 0x6C, 0x6A, 0x49, 0x47, 0x46, 0x75, 0x5A, 0x43, 0x42, 0x53, 0x5A, 0x58, 0x4E, 0x6C, 0x59, 0x58, 0x4A, 0x6A, 0x61, 0x43, 0x42, + 0x4A, 0x62, 0x6E, 0x4E, 0x30, 0x61, 0x58, 0x52, 0x31, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6E, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x77, 0x0A, 0x4D, 0x54, 0x55, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, + 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x43, 0x2B, 0x4B, + 0x6B, 0x2F, 0x47, 0x34, 0x6E, 0x38, 0x50, 0x44, 0x77, 0x45, 0x58, 0x54, 0x32, 0x51, 0x4E, 0x72, 0x43, 0x52, 0x4F, 0x6E, 0x6B, 0x38, 0x5A, 0x6C, 0x72, 0x76, 0x0A, 0x62, 0x54, 0x6B, 0x42, 0x53, 0x52, 0x71, 0x30, 0x74, 0x38, 0x39, 0x2F, 0x54, + 0x53, 0x4E, 0x54, 0x74, 0x35, 0x41, 0x41, 0x34, 0x78, 0x4D, 0x71, 0x4B, 0x4B, 0x59, 0x78, 0x38, 0x5A, 0x45, 0x41, 0x34, 0x79, 0x6A, 0x73, 0x72, 0x69, 0x46, 0x42, 0x7A, 0x68, 0x2F, 0x61, 0x2F, 0x58, 0x30, 0x53, 0x57, 0x77, 0x47, 0x44, 0x44, + 0x37, 0x6D, 0x77, 0x58, 0x35, 0x6E, 0x68, 0x38, 0x68, 0x4B, 0x44, 0x67, 0x45, 0x30, 0x47, 0x50, 0x74, 0x2B, 0x73, 0x72, 0x2B, 0x65, 0x68, 0x0A, 0x69, 0x47, 0x73, 0x78, 0x72, 0x2F, 0x43, 0x4C, 0x30, 0x42, 0x67, 0x7A, 0x75, 0x4E, 0x74, 0x46, + 0x61, 0x6A, 0x54, 0x30, 0x41, 0x6F, 0x41, 0x6B, 0x4B, 0x41, 0x6F, 0x43, 0x46, 0x5A, 0x56, 0x65, 0x64, 0x69, 0x6F, 0x4E, 0x6D, 0x54, 0x6F, 0x55, 0x57, 0x2F, 0x62, 0x4C, 0x79, 0x31, 0x4F, 0x38, 0x45, 0x30, 0x30, 0x42, 0x69, 0x44, 0x65, 0x55, + 0x4A, 0x52, 0x74, 0x43, 0x76, 0x43, 0x4C, 0x59, 0x6A, 0x71, 0x4F, 0x57, 0x58, 0x6A, 0x72, 0x5A, 0x4D, 0x74, 0x73, 0x2B, 0x0A, 0x36, 0x50, 0x41, 0x51, 0x5A, 0x65, 0x31, 0x30, 0x34, 0x53, 0x2B, 0x6E, 0x66, 0x4B, 0x38, 0x6E, 0x4E, 0x4C, 0x73, + 0x70, 0x66, 0x5A, 0x75, 0x32, 0x7A, 0x77, 0x6E, 0x49, 0x35, 0x64, 0x4D, 0x4B, 0x2F, 0x49, 0x68, 0x6C, 0x5A, 0x58, 0x51, 0x4B, 0x33, 0x48, 0x4D, 0x63, 0x58, 0x4D, 0x31, 0x41, 0x73, 0x52, 0x7A, 0x55, 0x74, 0x6F, 0x53, 0x4D, 0x54, 0x46, 0x44, + 0x50, 0x61, 0x49, 0x36, 0x6F, 0x57, 0x61, 0x37, 0x43, 0x4A, 0x30, 0x36, 0x43, 0x6F, 0x6A, 0x58, 0x64, 0x0A, 0x46, 0x50, 0x51, 0x66, 0x2F, 0x37, 0x4A, 0x33, 0x31, 0x59, 0x63, 0x76, 0x71, 0x6D, 0x35, 0x39, 0x4A, 0x43, 0x66, 0x6E, 0x78, 0x73, + 0x73, 0x6D, 0x35, 0x75, 0x58, 0x2B, 0x5A, 0x77, 0x64, 0x6A, 0x32, 0x45, 0x55, 0x4E, 0x33, 0x54, 0x70, 0x5A, 0x5A, 0x54, 0x6C, 0x59, 0x65, 0x70, 0x4B, 0x5A, 0x63, 0x6A, 0x32, 0x63, 0x68, 0x46, 0x36, 0x49, 0x49, 0x62, 0x6A, 0x56, 0x39, 0x43, + 0x7A, 0x38, 0x32, 0x58, 0x42, 0x53, 0x54, 0x33, 0x69, 0x34, 0x76, 0x54, 0x77, 0x72, 0x0A, 0x69, 0x35, 0x57, 0x59, 0x39, 0x62, 0x50, 0x52, 0x61, 0x4D, 0x38, 0x67, 0x46, 0x48, 0x35, 0x4D, 0x58, 0x46, 0x2F, 0x6E, 0x69, 0x2B, 0x58, 0x31, 0x4E, + 0x59, 0x45, 0x5A, 0x4E, 0x39, 0x63, 0x52, 0x43, 0x4C, 0x64, 0x6D, 0x76, 0x74, 0x4E, 0x4B, 0x7A, 0x6F, 0x4E, 0x58, 0x41, 0x44, 0x72, 0x44, 0x67, 0x66, 0x67, 0x58, 0x79, 0x35, 0x49, 0x32, 0x58, 0x64, 0x47, 0x6A, 0x32, 0x48, 0x55, 0x62, 0x34, + 0x59, 0x73, 0x6E, 0x36, 0x6E, 0x70, 0x49, 0x51, 0x66, 0x31, 0x46, 0x0A, 0x47, 0x51, 0x61, 0x74, 0x4A, 0x35, 0x6C, 0x4F, 0x77, 0x58, 0x42, 0x48, 0x33, 0x62, 0x57, 0x66, 0x67, 0x56, 0x4D, 0x53, 0x35, 0x62, 0x47, 0x4D, 0x53, 0x46, 0x30, 0x78, + 0x51, 0x78, 0x66, 0x6A, 0x6A, 0x4D, 0x5A, 0x36, 0x59, 0x35, 0x5A, 0x4C, 0x4B, 0x54, 0x42, 0x4F, 0x68, 0x45, 0x35, 0x69, 0x47, 0x56, 0x34, 0x38, 0x7A, 0x70, 0x65, 0x51, 0x70, 0x58, 0x38, 0x42, 0x36, 0x35, 0x33, 0x67, 0x2B, 0x49, 0x75, 0x4A, + 0x33, 0x53, 0x57, 0x59, 0x50, 0x5A, 0x4B, 0x32, 0x0A, 0x66, 0x75, 0x2F, 0x5A, 0x38, 0x56, 0x46, 0x52, 0x66, 0x53, 0x30, 0x6D, 0x79, 0x47, 0x6C, 0x5A, 0x59, 0x65, 0x43, 0x73, 0x61, 0x72, 0x67, 0x71, 0x4E, 0x68, 0x45, 0x45, 0x65, 0x6C, 0x43, + 0x39, 0x4D, 0x6F, 0x53, 0x2B, 0x4C, 0x39, 0x78, 0x79, 0x31, 0x64, 0x63, 0x64, 0x46, 0x6B, 0x66, 0x6B, 0x52, 0x32, 0x59, 0x67, 0x50, 0x2F, 0x53, 0x57, 0x78, 0x61, 0x2B, 0x4F, 0x41, 0x58, 0x71, 0x6C, 0x44, 0x33, 0x70, 0x6B, 0x39, 0x51, 0x30, + 0x59, 0x68, 0x39, 0x6D, 0x75, 0x0A, 0x69, 0x4E, 0x58, 0x36, 0x68, 0x4D, 0x45, 0x36, 0x77, 0x47, 0x6B, 0x6F, 0x4C, 0x66, 0x49, 0x4E, 0x61, 0x46, 0x47, 0x71, 0x34, 0x36, 0x56, 0x33, 0x78, 0x71, 0x53, 0x51, 0x44, 0x71, 0x45, 0x33, 0x69, 0x7A, + 0x45, 0x6A, 0x52, 0x38, 0x45, 0x4A, 0x43, 0x4F, 0x74, 0x75, 0x39, 0x33, 0x69, 0x62, 0x31, 0x34, 0x4C, 0x38, 0x68, 0x43, 0x43, 0x5A, 0x53, 0x52, 0x6D, 0x32, 0x45, 0x6B, 0x61, 0x78, 0x2B, 0x30, 0x56, 0x56, 0x46, 0x71, 0x6D, 0x6A, 0x5A, 0x61, + 0x79, 0x63, 0x0A, 0x42, 0x77, 0x2F, 0x71, 0x61, 0x39, 0x77, 0x66, 0x4C, 0x67, 0x5A, 0x79, 0x37, 0x49, 0x61, 0x49, 0x45, 0x75, 0x51, 0x74, 0x32, 0x31, 0x38, 0x46, 0x4C, 0x2B, 0x54, 0x77, 0x41, 0x39, 0x4D, 0x6D, 0x4D, 0x2B, 0x65, 0x41, 0x77, + 0x73, 0x31, 0x43, 0x6F, 0x52, 0x63, 0x30, 0x43, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x0A, + 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, + 0x63, 0x52, 0x56, 0x6E, 0x79, 0x4D, 0x6A, 0x4A, 0x76, 0x58, 0x56, 0x64, 0x63, 0x74, 0x41, 0x34, 0x47, 0x47, 0x71, 0x64, 0x38, 0x33, 0x45, 0x6B, 0x56, 0x41, 0x73, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, + 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x48, 0x57, 0x37, 0x62, 0x56, 0x52, 0x4C, 0x71, 0x68, 0x42, 0x59, 0x52, 0x6A, 0x54, 0x79, 0x59, 0x74, 0x63, 0x57, 0x4E, 0x6C, 0x30, 0x49, 0x58, 0x74, 0x56, + 0x73, 0x79, 0x49, 0x65, 0x39, 0x74, 0x43, 0x35, 0x47, 0x38, 0x6A, 0x48, 0x34, 0x66, 0x4F, 0x70, 0x43, 0x74, 0x5A, 0x4D, 0x57, 0x56, 0x64, 0x79, 0x68, 0x44, 0x42, 0x4B, 0x67, 0x32, 0x6D, 0x46, 0x2B, 0x0A, 0x44, 0x31, 0x68, 0x59, 0x63, 0x32, + 0x52, 0x79, 0x78, 0x2B, 0x68, 0x46, 0x6A, 0x74, 0x79, 0x70, 0x38, 0x69, 0x59, 0x2F, 0x78, 0x6E, 0x6D, 0x4D, 0x73, 0x56, 0x4D, 0x49, 0x4D, 0x34, 0x47, 0x77, 0x56, 0x68, 0x4F, 0x2B, 0x35, 0x6C, 0x46, 0x63, 0x32, 0x4A, 0x73, 0x4B, 0x54, 0x30, + 0x75, 0x63, 0x56, 0x6C, 0x4D, 0x43, 0x36, 0x55, 0x2F, 0x32, 0x44, 0x57, 0x44, 0x71, 0x54, 0x55, 0x4A, 0x56, 0x36, 0x48, 0x77, 0x62, 0x49, 0x53, 0x48, 0x54, 0x47, 0x7A, 0x72, 0x4D, 0x0A, 0x64, 0x2F, 0x4B, 0x34, 0x6B, 0x50, 0x46, 0x6F, 0x78, + 0x2F, 0x6C, 0x61, 0x2F, 0x76, 0x6F, 0x74, 0x39, 0x4C, 0x2F, 0x4A, 0x39, 0x55, 0x55, 0x62, 0x7A, 0x6A, 0x67, 0x51, 0x4B, 0x6A, 0x65, 0x4B, 0x65, 0x61, 0x4F, 0x30, 0x34, 0x77, 0x6C, 0x73, 0x68, 0x59, 0x61, 0x54, 0x2F, 0x34, 0x6D, 0x57, 0x4A, + 0x33, 0x69, 0x42, 0x6A, 0x32, 0x66, 0x6A, 0x52, 0x6E, 0x52, 0x55, 0x6A, 0x74, 0x6B, 0x4E, 0x61, 0x65, 0x4A, 0x4B, 0x39, 0x45, 0x31, 0x30, 0x41, 0x2F, 0x2B, 0x79, 0x0A, 0x64, 0x2B, 0x32, 0x56, 0x5A, 0x35, 0x66, 0x6B, 0x73, 0x63, 0x57, 0x72, + 0x76, 0x32, 0x6F, 0x6A, 0x36, 0x4E, 0x53, 0x55, 0x34, 0x6B, 0x51, 0x6F, 0x59, 0x73, 0x52, 0x4C, 0x34, 0x76, 0x44, 0x59, 0x34, 0x69, 0x6C, 0x72, 0x47, 0x6E, 0x42, 0x2B, 0x4A, 0x47, 0x47, 0x54, 0x65, 0x30, 0x38, 0x44, 0x4D, 0x69, 0x55, 0x4E, + 0x52, 0x53, 0x51, 0x72, 0x6C, 0x72, 0x52, 0x47, 0x61, 0x72, 0x39, 0x4B, 0x43, 0x2F, 0x65, 0x61, 0x6A, 0x38, 0x47, 0x73, 0x47, 0x73, 0x56, 0x6E, 0x0A, 0x38, 0x32, 0x38, 0x30, 0x30, 0x76, 0x70, 0x7A, 0x59, 0x34, 0x7A, 0x76, 0x46, 0x72, 0x43, + 0x6F, 0x70, 0x45, 0x59, 0x71, 0x2B, 0x4F, 0x73, 0x53, 0x37, 0x48, 0x4B, 0x30, 0x37, 0x2F, 0x67, 0x72, 0x66, 0x6F, 0x78, 0x53, 0x77, 0x49, 0x75, 0x45, 0x56, 0x50, 0x6B, 0x76, 0x50, 0x75, 0x4E, 0x56, 0x71, 0x4E, 0x78, 0x6D, 0x73, 0x64, 0x6E, + 0x68, 0x58, 0x39, 0x69, 0x7A, 0x6A, 0x46, 0x6B, 0x30, 0x57, 0x61, 0x53, 0x72, 0x54, 0x32, 0x79, 0x37, 0x48, 0x78, 0x6A, 0x62, 0x0A, 0x64, 0x61, 0x76, 0x59, 0x79, 0x35, 0x4C, 0x4E, 0x6C, 0x44, 0x68, 0x68, 0x44, 0x67, 0x63, 0x47, 0x48, 0x30, + 0x74, 0x47, 0x45, 0x50, 0x45, 0x56, 0x76, 0x6F, 0x32, 0x46, 0x58, 0x44, 0x74, 0x4B, 0x4B, 0x34, 0x46, 0x35, 0x44, 0x37, 0x52, 0x70, 0x6E, 0x30, 0x6C, 0x51, 0x6C, 0x30, 0x33, 0x33, 0x44, 0x6C, 0x5A, 0x64, 0x77, 0x4A, 0x56, 0x71, 0x77, 0x6A, + 0x62, 0x44, 0x47, 0x32, 0x6A, 0x4A, 0x39, 0x53, 0x72, 0x63, 0x52, 0x35, 0x71, 0x2B, 0x73, 0x73, 0x37, 0x46, 0x0A, 0x4A, 0x65, 0x6A, 0x36, 0x41, 0x37, 0x6E, 0x61, 0x2B, 0x52, 0x5A, 0x75, 0x6B, 0x59, 0x54, 0x31, 0x48, 0x43, 0x6A, 0x49, 0x2F, + 0x43, 0x62, 0x4D, 0x31, 0x78, 0x79, 0x51, 0x56, 0x71, 0x64, 0x66, 0x62, 0x7A, 0x6F, 0x45, 0x76, 0x4D, 0x31, 0x34, 0x69, 0x51, 0x75, 0x4F, 0x44, 0x79, 0x2B, 0x6A, 0x71, 0x6B, 0x2B, 0x69, 0x47, 0x78, 0x49, 0x39, 0x46, 0x67, 0x68, 0x41, 0x44, + 0x2F, 0x46, 0x47, 0x54, 0x4E, 0x65, 0x71, 0x65, 0x77, 0x6A, 0x42, 0x43, 0x76, 0x56, 0x74, 0x0A, 0x4A, 0x39, 0x34, 0x43, 0x6A, 0x38, 0x72, 0x44, 0x74, 0x53, 0x76, 0x4B, 0x36, 0x65, 0x76, 0x49, 0x49, 0x56, 0x4D, 0x34, 0x70, 0x63, 0x77, 0x37, + 0x32, 0x48, 0x63, 0x33, 0x4D, 0x4B, 0x4A, 0x50, 0x32, 0x57, 0x2F, 0x52, 0x38, 0x6B, 0x43, 0x74, 0x51, 0x58, 0x6F, 0x58, 0x78, 0x64, 0x5A, 0x4B, 0x4E, 0x59, 0x6D, 0x33, 0x51, 0x64, 0x56, 0x38, 0x68, 0x6E, 0x39, 0x56, 0x54, 0x59, 0x4E, 0x4B, + 0x70, 0x58, 0x4D, 0x67, 0x77, 0x44, 0x71, 0x76, 0x6B, 0x50, 0x47, 0x61, 0x0A, 0x4A, 0x49, 0x37, 0x5A, 0x6A, 0x6E, 0x48, 0x4B, 0x65, 0x37, 0x69, 0x47, 0x32, 0x72, 0x4B, 0x50, 0x6D, 0x54, 0x34, 0x64, 0x45, 0x77, 0x30, 0x53, 0x45, 0x65, 0x37, + 0x55, 0x71, 0x2F, 0x44, 0x70, 0x46, 0x58, 0x59, 0x43, 0x35, 0x4F, 0x44, 0x66, 0x71, 0x69, 0x41, 0x65, 0x57, 0x32, 0x47, 0x46, 0x5A, 0x45, 0x43, 0x70, 0x6B, 0x4A, 0x63, 0x4E, 0x72, 0x56, 0x50, 0x53, 0x57, 0x68, 0x32, 0x48, 0x61, 0x67, 0x43, + 0x58, 0x5A, 0x57, 0x4B, 0x30, 0x76, 0x6D, 0x39, 0x71, 0x0A, 0x70, 0x2F, 0x55, 0x73, 0x51, 0x75, 0x30, 0x79, 0x72, 0x62, 0x59, 0x68, 0x6E, 0x72, 0x36, 0x38, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x48, 0x65, 0x6C, 0x6C, 0x65, 0x6E, 0x69, 0x63, 0x20, 0x41, 0x63, 0x61, 0x64, 0x65, 0x6D, 0x69, 0x63, 0x20, 0x61, 0x6E, 0x64, 0x20, 0x52, 0x65, 0x73, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x20, 0x49, 0x6E, 0x73, 0x74, 0x69, 0x74, 0x75, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x43, 0x41, 0x20, 0x32, 0x30, 0x31, 0x35, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x77, 0x7A, 0x43, 0x43, 0x41, 0x6B, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x6A, + 0x43, 0x42, 0x71, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x31, 0x49, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x42, 0x6B, 0x46, 0x30, 0x0A, 0x61, + 0x47, 0x56, 0x75, 0x63, 0x7A, 0x46, 0x45, 0x4D, 0x45, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x37, 0x53, 0x47, 0x56, 0x73, 0x62, 0x47, 0x56, 0x75, 0x61, 0x57, 0x4D, 0x67, 0x51, 0x57, 0x4E, 0x68, 0x5A, 0x47, 0x56, 0x74, 0x61, + 0x57, 0x4D, 0x67, 0x59, 0x57, 0x35, 0x6B, 0x49, 0x46, 0x4A, 0x6C, 0x63, 0x32, 0x56, 0x68, 0x63, 0x6D, 0x4E, 0x6F, 0x49, 0x45, 0x6C, 0x75, 0x63, 0x33, 0x52, 0x70, 0x64, 0x48, 0x56, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, 0x63, 0x79, 0x42, 0x44, + 0x5A, 0x58, 0x4A, 0x30, 0x4C, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x52, 0x44, 0x42, 0x43, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4F, 0x30, 0x68, 0x6C, 0x62, 0x47, 0x78, 0x6C, + 0x62, 0x6D, 0x6C, 0x6A, 0x49, 0x45, 0x46, 0x6A, 0x59, 0x57, 0x52, 0x6C, 0x62, 0x57, 0x6C, 0x6A, 0x49, 0x47, 0x46, 0x75, 0x5A, 0x43, 0x42, 0x53, 0x5A, 0x58, 0x4E, 0x6C, 0x59, 0x58, 0x4A, 0x6A, 0x0A, 0x61, 0x43, 0x42, 0x4A, 0x62, 0x6E, 0x4E, + 0x30, 0x61, 0x58, 0x52, 0x31, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6E, 0x4D, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x52, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x45, 0x31, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, + 0x31, 0x4D, 0x44, 0x63, 0x77, 0x4E, 0x7A, 0x45, 0x77, 0x4D, 0x7A, 0x63, 0x78, 0x4D, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x44, 0x59, 0x7A, 0x4D, 0x44, 0x45, 0x77, 0x0A, 0x4D, 0x7A, 0x63, 0x78, 0x4D, 0x6C, 0x6F, 0x77, 0x67, 0x61, + 0x6F, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x64, 0x53, 0x4D, 0x51, 0x38, 0x77, 0x44, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x45, 0x77, 0x5A, 0x42, 0x64, 0x47, 0x68, 0x6C, 0x62, 0x6E, + 0x4D, 0x78, 0x52, 0x44, 0x42, 0x43, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x4F, 0x30, 0x68, 0x6C, 0x62, 0x47, 0x78, 0x6C, 0x62, 0x6D, 0x6C, 0x6A, 0x0A, 0x49, 0x45, 0x46, 0x6A, 0x59, 0x57, 0x52, 0x6C, 0x62, 0x57, 0x6C, 0x6A, 0x49, + 0x47, 0x46, 0x75, 0x5A, 0x43, 0x42, 0x53, 0x5A, 0x58, 0x4E, 0x6C, 0x59, 0x58, 0x4A, 0x6A, 0x61, 0x43, 0x42, 0x4A, 0x62, 0x6E, 0x4E, 0x30, 0x61, 0x58, 0x52, 0x31, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6E, 0x4D, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, + 0x43, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x4D, 0x55, 0x51, 0x77, 0x51, 0x67, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x44, 0x45, 0x7A, 0x74, 0x49, 0x5A, 0x57, 0x78, 0x73, 0x5A, 0x57, 0x35, 0x70, + 0x59, 0x79, 0x42, 0x42, 0x59, 0x32, 0x46, 0x6B, 0x5A, 0x57, 0x31, 0x70, 0x59, 0x79, 0x42, 0x68, 0x62, 0x6D, 0x51, 0x67, 0x55, 0x6D, 0x56, 0x7A, 0x5A, 0x57, 0x46, 0x79, 0x59, 0x32, 0x67, 0x67, 0x53, 0x57, 0x35, 0x7A, 0x64, 0x47, 0x6C, 0x30, + 0x64, 0x58, 0x52, 0x70, 0x62, 0x32, 0x35, 0x7A, 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x0A, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x78, 0x4E, 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, + 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4A, 0x4B, 0x67, 0x51, 0x65, 0x68, 0x4C, 0x67, 0x6F, 0x52, 0x63, 0x34, 0x76, 0x67, 0x78, 0x45, 0x5A, 0x6D, + 0x47, 0x5A, 0x45, 0x34, 0x4A, 0x4A, 0x53, 0x2B, 0x64, 0x51, 0x53, 0x38, 0x4B, 0x72, 0x6A, 0x56, 0x50, 0x0A, 0x64, 0x4A, 0x57, 0x79, 0x55, 0x57, 0x52, 0x72, 0x6A, 0x57, 0x76, 0x6D, 0x50, 0x33, 0x43, 0x56, 0x38, 0x41, 0x56, 0x45, 0x52, 0x36, + 0x5A, 0x79, 0x4F, 0x46, 0x42, 0x32, 0x6C, 0x51, 0x4A, 0x61, 0x6A, 0x71, 0x34, 0x6F, 0x6E, 0x76, 0x6B, 0x74, 0x54, 0x70, 0x6E, 0x76, 0x4C, 0x45, 0x68, 0x76, 0x54, 0x43, 0x55, 0x70, 0x36, 0x4E, 0x46, 0x78, 0x57, 0x39, 0x38, 0x64, 0x77, 0x58, + 0x55, 0x33, 0x74, 0x4E, 0x66, 0x36, 0x65, 0x33, 0x70, 0x43, 0x6E, 0x47, 0x6F, 0x4B, 0x0A, 0x56, 0x6C, 0x70, 0x38, 0x61, 0x51, 0x75, 0x71, 0x67, 0x41, 0x6B, 0x6B, 0x62, 0x48, 0x37, 0x42, 0x52, 0x71, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, + 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, + 0x51, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x0A, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4C, 0x51, 0x69, 0x43, 0x34, 0x4B, 0x5A, 0x4A, 0x41, 0x45, 0x4F, 0x6E, 0x4C, 0x76, 0x6B, 0x44, 0x76, 0x32, 0x2F, 0x2B, 0x35, 0x63, 0x67, + 0x6B, 0x35, 0x6B, 0x71, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x32, 0x63, 0x41, 0x4D, 0x47, 0x51, 0x43, 0x4D, 0x47, 0x66, 0x4F, 0x46, 0x6D, 0x49, 0x34, 0x6F, 0x71, 0x78, 0x69, + 0x52, 0x61, 0x65, 0x70, 0x6C, 0x53, 0x54, 0x41, 0x0A, 0x47, 0x69, 0x65, 0x63, 0x4D, 0x6A, 0x76, 0x41, 0x77, 0x4E, 0x57, 0x36, 0x71, 0x65, 0x66, 0x34, 0x42, 0x45, 0x4E, 0x54, 0x68, 0x65, 0x35, 0x53, 0x49, 0x64, 0x36, 0x64, 0x39, 0x53, 0x57, + 0x44, 0x50, 0x70, 0x35, 0x59, 0x53, 0x79, 0x2F, 0x58, 0x5A, 0x78, 0x4D, 0x4F, 0x49, 0x51, 0x49, 0x77, 0x42, 0x65, 0x46, 0x31, 0x41, 0x64, 0x35, 0x6F, 0x37, 0x53, 0x6F, 0x66, 0x54, 0x55, 0x77, 0x4A, 0x43, 0x41, 0x33, 0x73, 0x53, 0x36, 0x31, + 0x6B, 0x46, 0x79, 0x6A, 0x6E, 0x0A, 0x64, 0x63, 0x35, 0x46, 0x5A, 0x58, 0x49, 0x68, 0x46, 0x38, 0x73, 0x69, 0x51, 0x51, 0x36, 0x4D, 0x45, 0x35, 0x67, 0x34, 0x6D, 0x6C, 0x52, 0x74, 0x6D, 0x38, 0x72, 0x69, 0x66, 0x4F, 0x6F, 0x43, 0x57, 0x43, + 0x4B, 0x52, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x49, 0x53, 0x52, 0x47, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, + 0x58, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x61, 0x7A, 0x43, 0x43, 0x41, 0x31, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x52, 0x41, 0x49, 0x49, 0x51, 0x7A, 0x37, 0x44, 0x53, 0x51, 0x4F, 0x4E, 0x5A, 0x52, 0x47, 0x50, 0x67, + 0x75, 0x32, 0x4F, 0x43, 0x69, 0x77, 0x41, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x54, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x4B, 0x54, 0x41, 0x6E, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x49, 0x45, 0x6C, 0x75, 0x64, 0x47, 0x56, 0x79, 0x62, 0x6D, 0x56, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x56, + 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x46, 0x4A, 0x6C, 0x63, 0x32, 0x56, 0x68, 0x63, 0x6D, 0x4E, 0x6F, 0x49, 0x45, 0x64, 0x79, 0x62, 0x33, 0x56, 0x77, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x45, 0x77, + 0x78, 0x4A, 0x55, 0x31, 0x4A, 0x48, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x57, 0x44, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x55, 0x77, 0x4E, 0x6A, 0x41, 0x30, 0x4D, 0x54, 0x45, 0x77, 0x4E, 0x44, 0x4D, 0x34, 0x57, 0x68, + 0x63, 0x4E, 0x4D, 0x7A, 0x55, 0x77, 0x4E, 0x6A, 0x41, 0x30, 0x4D, 0x54, 0x45, 0x77, 0x4E, 0x44, 0x4D, 0x34, 0x57, 0x6A, 0x42, 0x50, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, + 0x7A, 0x45, 0x70, 0x4D, 0x43, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x67, 0x53, 0x57, 0x35, 0x30, 0x5A, 0x58, 0x4A, 0x75, 0x5A, 0x58, 0x51, 0x67, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x55, + 0x6D, 0x56, 0x7A, 0x5A, 0x57, 0x46, 0x79, 0x59, 0x32, 0x67, 0x67, 0x52, 0x33, 0x4A, 0x76, 0x64, 0x58, 0x41, 0x78, 0x46, 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x0A, 0x44, 0x45, 0x6C, 0x54, 0x55, 0x6B, 0x63, 0x67, + 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x59, 0x4D, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, + 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4B, 0x33, 0x6F, 0x4A, 0x48, 0x50, 0x30, 0x46, 0x44, 0x66, 0x7A, 0x6D, 0x35, 0x34, 0x72, 0x0A, 0x56, 0x79, 0x67, 0x63, 0x68, 0x37, 0x37, 0x63, 0x74, 0x39, 0x38, + 0x34, 0x6B, 0x49, 0x78, 0x75, 0x50, 0x4F, 0x5A, 0x58, 0x6F, 0x48, 0x6A, 0x33, 0x64, 0x63, 0x4B, 0x69, 0x2F, 0x76, 0x56, 0x71, 0x62, 0x76, 0x59, 0x41, 0x54, 0x79, 0x6A, 0x62, 0x33, 0x6D, 0x69, 0x47, 0x62, 0x45, 0x53, 0x54, 0x74, 0x72, 0x46, + 0x6A, 0x2F, 0x52, 0x51, 0x53, 0x61, 0x37, 0x38, 0x66, 0x30, 0x75, 0x6F, 0x78, 0x6D, 0x79, 0x46, 0x2B, 0x30, 0x54, 0x4D, 0x38, 0x75, 0x6B, 0x6A, 0x31, 0x0A, 0x33, 0x58, 0x6E, 0x66, 0x73, 0x37, 0x6A, 0x2F, 0x45, 0x76, 0x45, 0x68, 0x6D, 0x6B, + 0x76, 0x42, 0x69, 0x6F, 0x5A, 0x78, 0x61, 0x55, 0x70, 0x6D, 0x5A, 0x6D, 0x79, 0x50, 0x66, 0x6A, 0x78, 0x77, 0x76, 0x36, 0x30, 0x70, 0x49, 0x67, 0x62, 0x7A, 0x35, 0x4D, 0x44, 0x6D, 0x67, 0x4B, 0x37, 0x69, 0x53, 0x34, 0x2B, 0x33, 0x6D, 0x58, + 0x36, 0x55, 0x41, 0x35, 0x2F, 0x54, 0x52, 0x35, 0x64, 0x38, 0x6D, 0x55, 0x67, 0x6A, 0x55, 0x2B, 0x67, 0x34, 0x72, 0x6B, 0x38, 0x4B, 0x0A, 0x62, 0x34, 0x4D, 0x75, 0x30, 0x55, 0x6C, 0x58, 0x6A, 0x49, 0x42, 0x30, 0x74, 0x74, 0x6F, 0x76, 0x30, + 0x44, 0x69, 0x4E, 0x65, 0x77, 0x4E, 0x77, 0x49, 0x52, 0x74, 0x31, 0x38, 0x6A, 0x41, 0x38, 0x2B, 0x6F, 0x2B, 0x75, 0x33, 0x64, 0x70, 0x6A, 0x71, 0x2B, 0x73, 0x57, 0x54, 0x38, 0x4B, 0x4F, 0x45, 0x55, 0x74, 0x2B, 0x7A, 0x77, 0x76, 0x6F, 0x2F, + 0x37, 0x56, 0x33, 0x4C, 0x76, 0x53, 0x79, 0x65, 0x30, 0x72, 0x67, 0x54, 0x42, 0x49, 0x6C, 0x44, 0x48, 0x43, 0x4E, 0x0A, 0x41, 0x79, 0x6D, 0x67, 0x34, 0x56, 0x4D, 0x6B, 0x37, 0x42, 0x50, 0x5A, 0x37, 0x68, 0x6D, 0x2F, 0x45, 0x4C, 0x4E, 0x4B, + 0x6A, 0x44, 0x2B, 0x4A, 0x6F, 0x32, 0x46, 0x52, 0x33, 0x71, 0x79, 0x48, 0x42, 0x35, 0x54, 0x30, 0x59, 0x33, 0x48, 0x73, 0x4C, 0x75, 0x4A, 0x76, 0x57, 0x35, 0x69, 0x42, 0x34, 0x59, 0x6C, 0x63, 0x4E, 0x48, 0x6C, 0x73, 0x64, 0x75, 0x38, 0x37, + 0x6B, 0x47, 0x4A, 0x35, 0x35, 0x74, 0x75, 0x6B, 0x6D, 0x69, 0x38, 0x6D, 0x78, 0x64, 0x41, 0x51, 0x0A, 0x34, 0x51, 0x37, 0x65, 0x32, 0x52, 0x43, 0x4F, 0x46, 0x76, 0x75, 0x33, 0x39, 0x36, 0x6A, 0x33, 0x78, 0x2B, 0x55, 0x43, 0x42, 0x35, 0x69, + 0x50, 0x4E, 0x67, 0x69, 0x56, 0x35, 0x2B, 0x49, 0x33, 0x6C, 0x67, 0x30, 0x32, 0x64, 0x5A, 0x37, 0x37, 0x44, 0x6E, 0x4B, 0x78, 0x48, 0x5A, 0x75, 0x38, 0x41, 0x2F, 0x6C, 0x4A, 0x42, 0x64, 0x69, 0x42, 0x33, 0x51, 0x57, 0x30, 0x4B, 0x74, 0x5A, + 0x42, 0x36, 0x61, 0x77, 0x42, 0x64, 0x70, 0x55, 0x4B, 0x44, 0x39, 0x6A, 0x66, 0x0A, 0x31, 0x62, 0x30, 0x53, 0x48, 0x7A, 0x55, 0x76, 0x4B, 0x42, 0x64, 0x73, 0x30, 0x70, 0x6A, 0x42, 0x71, 0x41, 0x6C, 0x6B, 0x64, 0x32, 0x35, 0x48, 0x4E, 0x37, + 0x72, 0x4F, 0x72, 0x46, 0x6C, 0x65, 0x61, 0x4A, 0x31, 0x2F, 0x63, 0x74, 0x61, 0x4A, 0x78, 0x51, 0x5A, 0x42, 0x4B, 0x54, 0x35, 0x5A, 0x50, 0x74, 0x30, 0x6D, 0x39, 0x53, 0x54, 0x4A, 0x45, 0x61, 0x64, 0x61, 0x6F, 0x30, 0x78, 0x41, 0x48, 0x30, + 0x61, 0x68, 0x6D, 0x62, 0x57, 0x6E, 0x4F, 0x6C, 0x46, 0x75, 0x0A, 0x68, 0x6A, 0x75, 0x65, 0x66, 0x58, 0x4B, 0x6E, 0x45, 0x67, 0x56, 0x34, 0x57, 0x65, 0x30, 0x2B, 0x55, 0x58, 0x67, 0x56, 0x43, 0x77, 0x4F, 0x50, 0x6A, 0x64, 0x41, 0x76, 0x42, + 0x62, 0x49, 0x2B, 0x65, 0x30, 0x6F, 0x63, 0x53, 0x33, 0x4D, 0x46, 0x45, 0x76, 0x7A, 0x47, 0x36, 0x75, 0x42, 0x51, 0x45, 0x33, 0x78, 0x44, 0x6B, 0x33, 0x53, 0x7A, 0x79, 0x6E, 0x54, 0x6E, 0x6A, 0x68, 0x38, 0x42, 0x43, 0x4E, 0x41, 0x77, 0x31, + 0x46, 0x74, 0x78, 0x4E, 0x72, 0x51, 0x48, 0x0A, 0x75, 0x73, 0x45, 0x77, 0x4D, 0x46, 0x78, 0x49, 0x74, 0x34, 0x49, 0x37, 0x6D, 0x4B, 0x5A, 0x39, 0x59, 0x49, 0x71, 0x69, 0x6F, 0x79, 0x6D, 0x43, 0x7A, 0x4C, 0x71, 0x39, 0x67, 0x77, 0x51, 0x62, + 0x6F, 0x6F, 0x4D, 0x44, 0x51, 0x61, 0x48, 0x57, 0x42, 0x66, 0x45, 0x62, 0x77, 0x72, 0x62, 0x77, 0x71, 0x48, 0x79, 0x47, 0x4F, 0x30, 0x61, 0x6F, 0x53, 0x43, 0x71, 0x49, 0x33, 0x48, 0x61, 0x61, 0x64, 0x72, 0x38, 0x66, 0x61, 0x71, 0x55, 0x39, + 0x47, 0x59, 0x2F, 0x72, 0x0A, 0x4F, 0x50, 0x4E, 0x6B, 0x33, 0x73, 0x67, 0x72, 0x44, 0x51, 0x6F, 0x6F, 0x2F, 0x2F, 0x66, 0x62, 0x34, 0x68, 0x56, 0x43, 0x31, 0x43, 0x4C, 0x51, 0x4A, 0x31, 0x33, 0x68, 0x65, 0x66, 0x34, 0x59, 0x35, 0x33, 0x43, + 0x49, 0x72, 0x55, 0x37, 0x6D, 0x32, 0x59, 0x73, 0x36, 0x78, 0x74, 0x30, 0x6E, 0x55, 0x57, 0x37, 0x2F, 0x76, 0x47, 0x54, 0x31, 0x4D, 0x30, 0x4E, 0x50, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x34, + 0x47, 0x0A, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, + 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x35, 0x74, 0x46, 0x6E, 0x6D, 0x65, 0x37, 0x62, 0x6C, 0x35, 0x41, 0x46, 0x7A, 0x67, 0x41, 0x69, 0x49, 0x79, 0x42, 0x70, 0x59, 0x0A, 0x39, + 0x75, 0x6D, 0x62, 0x62, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x56, 0x52, 0x39, 0x59, 0x71, 0x62, 0x79, 0x79, 0x71, + 0x46, 0x44, 0x51, 0x44, 0x4C, 0x48, 0x59, 0x47, 0x6D, 0x6B, 0x67, 0x4A, 0x79, 0x6B, 0x49, 0x72, 0x47, 0x46, 0x31, 0x58, 0x49, 0x70, 0x75, 0x2B, 0x49, 0x4C, 0x6C, 0x61, 0x53, 0x2F, 0x56, 0x39, 0x6C, 0x5A, 0x4C, 0x0A, 0x75, 0x62, 0x68, 0x7A, + 0x45, 0x46, 0x6E, 0x54, 0x49, 0x5A, 0x64, 0x2B, 0x35, 0x30, 0x78, 0x78, 0x2B, 0x37, 0x4C, 0x53, 0x59, 0x4B, 0x30, 0x35, 0x71, 0x41, 0x76, 0x71, 0x46, 0x79, 0x46, 0x57, 0x68, 0x66, 0x46, 0x51, 0x44, 0x6C, 0x6E, 0x72, 0x7A, 0x75, 0x42, 0x5A, + 0x36, 0x62, 0x72, 0x4A, 0x46, 0x65, 0x2B, 0x47, 0x6E, 0x59, 0x2B, 0x45, 0x67, 0x50, 0x62, 0x6B, 0x36, 0x5A, 0x47, 0x51, 0x33, 0x42, 0x65, 0x62, 0x59, 0x68, 0x74, 0x46, 0x38, 0x47, 0x61, 0x56, 0x0A, 0x30, 0x6E, 0x78, 0x76, 0x77, 0x75, 0x6F, + 0x37, 0x37, 0x78, 0x2F, 0x50, 0x79, 0x39, 0x61, 0x75, 0x4A, 0x2F, 0x47, 0x70, 0x73, 0x4D, 0x69, 0x75, 0x2F, 0x58, 0x31, 0x2B, 0x6D, 0x76, 0x6F, 0x69, 0x42, 0x4F, 0x76, 0x2F, 0x32, 0x58, 0x2F, 0x71, 0x6B, 0x53, 0x73, 0x69, 0x73, 0x52, 0x63, + 0x4F, 0x6A, 0x2F, 0x4B, 0x4B, 0x4E, 0x46, 0x74, 0x59, 0x32, 0x50, 0x77, 0x42, 0x79, 0x56, 0x53, 0x35, 0x75, 0x43, 0x62, 0x4D, 0x69, 0x6F, 0x67, 0x7A, 0x69, 0x55, 0x77, 0x74, 0x0A, 0x68, 0x44, 0x79, 0x43, 0x33, 0x2B, 0x36, 0x57, 0x56, 0x77, + 0x57, 0x36, 0x4C, 0x4C, 0x76, 0x33, 0x78, 0x4C, 0x66, 0x48, 0x54, 0x6A, 0x75, 0x43, 0x76, 0x6A, 0x48, 0x49, 0x49, 0x6E, 0x4E, 0x7A, 0x6B, 0x74, 0x48, 0x43, 0x67, 0x4B, 0x51, 0x35, 0x4F, 0x52, 0x41, 0x7A, 0x49, 0x34, 0x4A, 0x4D, 0x50, 0x4A, + 0x2B, 0x47, 0x73, 0x6C, 0x57, 0x59, 0x48, 0x62, 0x34, 0x70, 0x68, 0x6F, 0x77, 0x69, 0x6D, 0x35, 0x37, 0x69, 0x61, 0x7A, 0x74, 0x58, 0x4F, 0x6F, 0x4A, 0x77, 0x0A, 0x54, 0x64, 0x77, 0x4A, 0x78, 0x34, 0x6E, 0x4C, 0x43, 0x67, 0x64, 0x4E, 0x62, + 0x4F, 0x68, 0x64, 0x6A, 0x73, 0x6E, 0x76, 0x7A, 0x71, 0x76, 0x48, 0x75, 0x37, 0x55, 0x72, 0x54, 0x6B, 0x58, 0x57, 0x53, 0x74, 0x41, 0x6D, 0x7A, 0x4F, 0x56, 0x79, 0x79, 0x67, 0x68, 0x71, 0x70, 0x5A, 0x58, 0x6A, 0x46, 0x61, 0x48, 0x33, 0x70, + 0x4F, 0x33, 0x4A, 0x4C, 0x46, 0x2B, 0x6C, 0x2B, 0x2F, 0x2B, 0x73, 0x4B, 0x41, 0x49, 0x75, 0x76, 0x74, 0x64, 0x37, 0x75, 0x2B, 0x4E, 0x78, 0x0A, 0x65, 0x35, 0x41, 0x57, 0x30, 0x77, 0x64, 0x65, 0x52, 0x6C, 0x4E, 0x38, 0x4E, 0x77, 0x64, 0x43, + 0x6A, 0x4E, 0x50, 0x45, 0x6C, 0x70, 0x7A, 0x56, 0x6D, 0x62, 0x55, 0x71, 0x34, 0x4A, 0x55, 0x61, 0x67, 0x45, 0x69, 0x75, 0x54, 0x44, 0x6B, 0x48, 0x7A, 0x73, 0x78, 0x48, 0x70, 0x46, 0x4B, 0x56, 0x4B, 0x37, 0x71, 0x34, 0x2B, 0x36, 0x33, 0x53, + 0x4D, 0x31, 0x4E, 0x39, 0x35, 0x52, 0x31, 0x4E, 0x62, 0x64, 0x57, 0x68, 0x73, 0x63, 0x64, 0x43, 0x62, 0x2B, 0x5A, 0x41, 0x0A, 0x4A, 0x7A, 0x56, 0x63, 0x6F, 0x79, 0x69, 0x33, 0x42, 0x34, 0x33, 0x6E, 0x6A, 0x54, 0x4F, 0x51, 0x35, 0x79, 0x4F, + 0x66, 0x2B, 0x31, 0x43, 0x63, 0x65, 0x57, 0x78, 0x47, 0x31, 0x62, 0x51, 0x56, 0x73, 0x35, 0x5A, 0x75, 0x66, 0x70, 0x73, 0x4D, 0x6C, 0x6A, 0x71, 0x34, 0x55, 0x69, 0x30, 0x2F, 0x31, 0x6C, 0x76, 0x68, 0x2B, 0x77, 0x6A, 0x43, 0x68, 0x50, 0x34, + 0x6B, 0x71, 0x4B, 0x4F, 0x4A, 0x32, 0x71, 0x78, 0x71, 0x34, 0x52, 0x67, 0x71, 0x73, 0x61, 0x68, 0x44, 0x0A, 0x59, 0x56, 0x76, 0x54, 0x48, 0x39, 0x77, 0x37, 0x6A, 0x58, 0x62, 0x79, 0x4C, 0x65, 0x69, 0x4E, 0x64, 0x64, 0x38, 0x58, 0x4D, 0x32, + 0x77, 0x39, 0x55, 0x2F, 0x74, 0x37, 0x79, 0x30, 0x46, 0x66, 0x2F, 0x39, 0x79, 0x69, 0x30, 0x47, 0x45, 0x34, 0x34, 0x5A, 0x61, 0x34, 0x72, 0x46, 0x32, 0x4C, 0x4E, 0x39, 0x64, 0x31, 0x31, 0x54, 0x50, 0x41, 0x6D, 0x52, 0x47, 0x75, 0x6E, 0x55, + 0x48, 0x42, 0x63, 0x6E, 0x57, 0x45, 0x76, 0x67, 0x4A, 0x42, 0x51, 0x6C, 0x39, 0x6E, 0x0A, 0x4A, 0x45, 0x69, 0x55, 0x30, 0x5A, 0x73, 0x6E, 0x76, 0x67, 0x63, 0x2F, 0x75, 0x62, 0x68, 0x50, 0x67, 0x58, 0x52, 0x52, 0x34, 0x58, 0x71, 0x33, 0x37, + 0x5A, 0x30, 0x6A, 0x34, 0x72, 0x37, 0x67, 0x31, 0x53, 0x67, 0x45, 0x45, 0x7A, 0x77, 0x78, 0x41, 0x35, 0x37, 0x64, 0x65, 0x6D, 0x79, 0x50, 0x78, 0x67, 0x63, 0x59, 0x78, 0x6E, 0x2F, 0x65, 0x52, 0x34, 0x34, 0x2F, 0x4B, 0x4A, 0x34, 0x45, 0x42, + 0x73, 0x2B, 0x6C, 0x56, 0x44, 0x52, 0x33, 0x76, 0x65, 0x79, 0x4A, 0x0A, 0x6D, 0x2B, 0x6B, 0x58, 0x51, 0x39, 0x39, 0x62, 0x32, 0x31, 0x2F, 0x2B, 0x6A, 0x68, 0x35, 0x58, 0x6F, 0x73, 0x31, 0x41, 0x6E, 0x58, 0x35, 0x69, 0x49, 0x74, 0x72, 0x65, + 0x47, 0x43, 0x63, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x43, 0x20, 0x52, 0x41, 0x49, 0x5A, 0x20, + 0x46, 0x4E, 0x4D, 0x54, 0x2D, 0x52, 0x43, 0x4D, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x67, 0x7A, 0x43, 0x43, 0x41, 0x32, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x50, 0x58, 0x5A, 0x4F, 0x4E, 0x4D, 0x47, + 0x63, 0x32, 0x79, 0x41, 0x59, 0x64, 0x47, 0x73, 0x64, 0x55, 0x68, 0x47, 0x6B, 0x48, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x44, 0x73, 0x78, 0x43, 0x7A, + 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x0A, 0x41, 0x6B, 0x56, 0x54, 0x4D, 0x52, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x68, 0x47, 0x54, 0x6B, 0x31, 0x55, 0x4C, 0x56, 0x4A, 0x44, 0x54, + 0x54, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x77, 0x77, 0x51, 0x51, 0x55, 0x4D, 0x67, 0x55, 0x6B, 0x46, 0x4A, 0x57, 0x69, 0x42, 0x47, 0x54, 0x6B, 0x31, 0x55, 0x4C, 0x56, 0x4A, 0x44, 0x54, 0x54, 0x41, 0x65, 0x46, + 0x77, 0x30, 0x77, 0x4F, 0x44, 0x45, 0x77, 0x0A, 0x4D, 0x6A, 0x6B, 0x78, 0x4E, 0x54, 0x55, 0x35, 0x4E, 0x54, 0x5A, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x44, 0x41, 0x78, 0x4D, 0x44, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, + 0x4D, 0x44, 0x73, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x56, 0x54, 0x4D, 0x52, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x68, 0x47, 0x54, 0x6B, 0x31, 0x55, + 0x4C, 0x56, 0x4A, 0x44, 0x0A, 0x54, 0x54, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x77, 0x77, 0x51, 0x51, 0x55, 0x4D, 0x67, 0x55, 0x6B, 0x46, 0x4A, 0x57, 0x69, 0x42, 0x47, 0x54, 0x6B, 0x31, 0x55, 0x4C, 0x56, 0x4A, + 0x44, 0x54, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, + 0x43, 0x0A, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4C, 0x70, 0x78, 0x67, 0x48, 0x70, 0x4D, 0x68, 0x6D, 0x35, 0x2F, 0x79, 0x42, 0x4E, 0x74, 0x77, 0x4D, 0x5A, 0x39, 0x48, 0x41, 0x43, 0x58, 0x6A, 0x79, 0x77, 0x4D, 0x49, 0x37, 0x73, 0x51, 0x6D, 0x6B, + 0x43, 0x70, 0x47, 0x72, 0x65, 0x48, 0x69, 0x50, 0x69, 0x62, 0x56, 0x6D, 0x72, 0x37, 0x35, 0x6E, 0x75, 0x4F, 0x69, 0x35, 0x4B, 0x4F, 0x70, 0x79, 0x56, 0x64, 0x57, 0x52, 0x48, 0x62, 0x4E, 0x69, 0x36, 0x33, 0x55, 0x52, 0x63, 0x66, 0x0A, 0x71, + 0x51, 0x67, 0x66, 0x42, 0x42, 0x63, 0x6B, 0x57, 0x4B, 0x6F, 0x33, 0x53, 0x68, 0x6A, 0x66, 0x35, 0x54, 0x6E, 0x55, 0x56, 0x2F, 0x33, 0x58, 0x77, 0x53, 0x79, 0x52, 0x41, 0x5A, 0x48, 0x69, 0x49, 0x74, 0x51, 0x44, 0x77, 0x46, 0x6A, 0x38, 0x64, + 0x30, 0x66, 0x73, 0x6A, 0x7A, 0x35, 0x30, 0x51, 0x37, 0x71, 0x73, 0x4E, 0x49, 0x31, 0x4E, 0x4F, 0x48, 0x5A, 0x6E, 0x6A, 0x72, 0x44, 0x49, 0x62, 0x7A, 0x41, 0x7A, 0x57, 0x48, 0x46, 0x63, 0x74, 0x50, 0x56, 0x72, 0x0A, 0x62, 0x74, 0x51, 0x42, + 0x55, 0x4C, 0x67, 0x54, 0x66, 0x6D, 0x78, 0x4B, 0x6F, 0x30, 0x6E, 0x52, 0x49, 0x42, 0x6E, 0x75, 0x76, 0x4D, 0x41, 0x70, 0x47, 0x47, 0x57, 0x6E, 0x33, 0x76, 0x37, 0x76, 0x33, 0x51, 0x71, 0x51, 0x49, 0x65, 0x63, 0x61, 0x5A, 0x35, 0x4A, 0x43, + 0x45, 0x4A, 0x68, 0x66, 0x54, 0x7A, 0x43, 0x38, 0x50, 0x68, 0x78, 0x46, 0x74, 0x42, 0x44, 0x58, 0x61, 0x45, 0x41, 0x55, 0x77, 0x45, 0x44, 0x36, 0x35, 0x33, 0x63, 0x58, 0x65, 0x75, 0x59, 0x4C, 0x0A, 0x6A, 0x32, 0x56, 0x62, 0x50, 0x4E, 0x6D, + 0x61, 0x55, 0x74, 0x75, 0x31, 0x76, 0x5A, 0x35, 0x47, 0x7A, 0x7A, 0x33, 0x72, 0x6B, 0x51, 0x55, 0x43, 0x77, 0x4A, 0x61, 0x79, 0x64, 0x6B, 0x78, 0x4E, 0x45, 0x4A, 0x59, 0x37, 0x6B, 0x76, 0x71, 0x63, 0x66, 0x77, 0x2B, 0x5A, 0x33, 0x37, 0x34, + 0x6A, 0x4E, 0x55, 0x55, 0x65, 0x41, 0x6C, 0x7A, 0x2B, 0x74, 0x61, 0x69, 0x62, 0x6D, 0x53, 0x58, 0x61, 0x58, 0x76, 0x4D, 0x69, 0x77, 0x7A, 0x6E, 0x31, 0x35, 0x43, 0x6F, 0x75, 0x0A, 0x30, 0x38, 0x59, 0x66, 0x78, 0x47, 0x79, 0x71, 0x78, 0x52, + 0x78, 0x71, 0x41, 0x51, 0x56, 0x4B, 0x4C, 0x39, 0x4C, 0x46, 0x77, 0x61, 0x67, 0x30, 0x4A, 0x6C, 0x31, 0x6D, 0x70, 0x64, 0x49, 0x43, 0x49, 0x66, 0x6B, 0x59, 0x74, 0x77, 0x62, 0x31, 0x54, 0x70, 0x6C, 0x76, 0x71, 0x4B, 0x74, 0x4D, 0x55, 0x65, + 0x6A, 0x50, 0x55, 0x42, 0x6A, 0x46, 0x64, 0x38, 0x67, 0x35, 0x43, 0x53, 0x78, 0x4A, 0x6B, 0x6A, 0x4B, 0x5A, 0x71, 0x4C, 0x73, 0x58, 0x46, 0x33, 0x6D, 0x77, 0x0A, 0x57, 0x73, 0x58, 0x6D, 0x6F, 0x38, 0x52, 0x5A, 0x5A, 0x55, 0x63, 0x31, 0x67, + 0x31, 0x36, 0x70, 0x36, 0x44, 0x55, 0x4C, 0x6D, 0x62, 0x76, 0x6B, 0x7A, 0x53, 0x44, 0x47, 0x6D, 0x30, 0x6F, 0x47, 0x4F, 0x62, 0x56, 0x6F, 0x2F, 0x43, 0x4B, 0x36, 0x37, 0x6C, 0x57, 0x4D, 0x4B, 0x30, 0x37, 0x71, 0x38, 0x37, 0x48, 0x6A, 0x2F, + 0x4C, 0x61, 0x5A, 0x6D, 0x74, 0x56, 0x43, 0x2B, 0x6E, 0x46, 0x4E, 0x43, 0x4D, 0x2B, 0x48, 0x48, 0x6D, 0x70, 0x78, 0x66, 0x66, 0x6E, 0x54, 0x0A, 0x74, 0x4F, 0x6D, 0x6C, 0x63, 0x59, 0x46, 0x37, 0x77, 0x6B, 0x35, 0x48, 0x6C, 0x71, 0x58, 0x32, + 0x64, 0x6F, 0x57, 0x6A, 0x4B, 0x49, 0x2F, 0x70, 0x67, 0x47, 0x36, 0x42, 0x55, 0x36, 0x56, 0x74, 0x58, 0x37, 0x68, 0x49, 0x2B, 0x63, 0x4C, 0x35, 0x4E, 0x71, 0x59, 0x75, 0x53, 0x66, 0x2B, 0x34, 0x6C, 0x73, 0x4B, 0x4D, 0x42, 0x37, 0x4F, 0x62, + 0x69, 0x46, 0x6A, 0x38, 0x36, 0x78, 0x73, 0x63, 0x33, 0x69, 0x31, 0x77, 0x34, 0x70, 0x65, 0x53, 0x4D, 0x4B, 0x47, 0x4A, 0x0A, 0x34, 0x37, 0x78, 0x56, 0x71, 0x43, 0x66, 0x57, 0x53, 0x2B, 0x32, 0x51, 0x72, 0x59, 0x76, 0x36, 0x59, 0x79, 0x56, + 0x5A, 0x4C, 0x61, 0x67, 0x31, 0x33, 0x63, 0x71, 0x58, 0x4D, 0x37, 0x7A, 0x6C, 0x7A, 0x63, 0x65, 0x64, 0x30, 0x65, 0x7A, 0x76, 0x58, 0x67, 0x35, 0x4B, 0x6B, 0x41, 0x59, 0x6D, 0x59, 0x36, 0x32, 0x35, 0x32, 0x54, 0x55, 0x74, 0x42, 0x37, 0x70, + 0x32, 0x5A, 0x53, 0x79, 0x73, 0x56, 0x34, 0x39, 0x39, 0x39, 0x41, 0x65, 0x55, 0x31, 0x34, 0x45, 0x43, 0x0A, 0x6C, 0x6C, 0x32, 0x6A, 0x42, 0x30, 0x6E, 0x56, 0x65, 0x74, 0x42, 0x58, 0x2B, 0x52, 0x76, 0x6E, 0x55, 0x30, 0x5A, 0x31, 0x71, 0x72, + 0x42, 0x35, 0x51, 0x73, 0x74, 0x6F, 0x63, 0x51, 0x6A, 0x70, 0x59, 0x4C, 0x30, 0x35, 0x61, 0x63, 0x37, 0x30, 0x72, 0x38, 0x4E, 0x57, 0x51, 0x4D, 0x65, 0x74, 0x55, 0x71, 0x49, 0x4A, 0x35, 0x47, 0x2B, 0x47, 0x52, 0x34, 0x6F, 0x66, 0x36, 0x79, + 0x67, 0x6E, 0x58, 0x59, 0x4D, 0x67, 0x72, 0x77, 0x54, 0x4A, 0x62, 0x46, 0x61, 0x61, 0x0A, 0x69, 0x30, 0x62, 0x31, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x67, 0x59, 0x4D, 0x77, 0x67, 0x59, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, + 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x48, + 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x0A, 0x46, 0x50, 0x64, 0x39, 0x78, 0x66, 0x33, 0x45, 0x36, 0x4A, 0x6F, 0x62, 0x64, 0x32, 0x53, 0x6E, 0x39, 0x52, 0x32, 0x67, 0x7A, 0x4C, 0x2B, 0x48, 0x59, 0x4A, 0x70, 0x74, + 0x4D, 0x44, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x41, 0x51, 0x33, 0x4D, 0x44, 0x55, 0x77, 0x4D, 0x77, 0x59, 0x45, 0x56, 0x52, 0x30, 0x67, 0x41, 0x44, 0x41, 0x72, 0x4D, 0x43, 0x6B, 0x47, 0x43, 0x43, 0x73, 0x47, 0x41, 0x51, 0x55, 0x46, + 0x42, 0x77, 0x49, 0x42, 0x46, 0x68, 0x31, 0x6F, 0x0A, 0x64, 0x48, 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x64, 0x33, 0x64, 0x33, 0x4C, 0x6D, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x75, 0x5A, 0x6D, 0x35, 0x74, 0x64, 0x43, 0x35, 0x6C, 0x63, 0x79, 0x39, + 0x6B, 0x63, 0x47, 0x4E, 0x7A, 0x4C, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x42, 0x35, 0x42, 0x4B, 0x33, 0x2F, 0x4D, + 0x6A, 0x54, 0x76, 0x44, 0x44, 0x0A, 0x6E, 0x46, 0x46, 0x6C, 0x6D, 0x35, 0x77, 0x69, 0x6F, 0x6F, 0x6F, 0x4D, 0x68, 0x66, 0x4E, 0x7A, 0x4B, 0x57, 0x74, 0x4E, 0x2F, 0x67, 0x48, 0x69, 0x71, 0x51, 0x78, 0x6A, 0x41, 0x62, 0x38, 0x45, 0x5A, 0x36, + 0x57, 0x64, 0x6D, 0x46, 0x2F, 0x39, 0x41, 0x52, 0x50, 0x36, 0x37, 0x4A, 0x70, 0x69, 0x36, 0x59, 0x62, 0x2B, 0x74, 0x6D, 0x4C, 0x53, 0x62, 0x6B, 0x79, 0x55, 0x2B, 0x38, 0x42, 0x31, 0x52, 0x58, 0x78, 0x6C, 0x44, 0x50, 0x69, 0x79, 0x4E, 0x38, + 0x2B, 0x73, 0x0A, 0x44, 0x38, 0x2B, 0x4E, 0x62, 0x2F, 0x6B, 0x5A, 0x39, 0x34, 0x2F, 0x73, 0x48, 0x76, 0x4A, 0x77, 0x6E, 0x76, 0x44, 0x4B, 0x75, 0x4F, 0x2B, 0x33, 0x2F, 0x33, 0x59, 0x33, 0x64, 0x6C, 0x76, 0x32, 0x62, 0x6F, 0x6A, 0x7A, 0x72, + 0x32, 0x49, 0x79, 0x49, 0x70, 0x4D, 0x4E, 0x4F, 0x6D, 0x71, 0x4F, 0x46, 0x47, 0x59, 0x4D, 0x4C, 0x56, 0x4E, 0x30, 0x56, 0x32, 0x55, 0x65, 0x31, 0x62, 0x4C, 0x64, 0x49, 0x34, 0x45, 0x37, 0x70, 0x57, 0x59, 0x6A, 0x4A, 0x32, 0x63, 0x4A, 0x0A, + 0x6A, 0x2B, 0x46, 0x33, 0x71, 0x6B, 0x50, 0x4E, 0x5A, 0x56, 0x45, 0x49, 0x37, 0x56, 0x46, 0x59, 0x2F, 0x75, 0x59, 0x35, 0x2B, 0x63, 0x74, 0x48, 0x68, 0x4B, 0x51, 0x56, 0x38, 0x58, 0x61, 0x37, 0x70, 0x4F, 0x36, 0x6B, 0x4F, 0x38, 0x52, 0x66, + 0x37, 0x37, 0x49, 0x7A, 0x6C, 0x68, 0x45, 0x59, 0x74, 0x38, 0x6C, 0x6C, 0x76, 0x68, 0x6A, 0x68, 0x6F, 0x36, 0x54, 0x63, 0x2B, 0x68, 0x6A, 0x35, 0x30, 0x37, 0x77, 0x54, 0x6D, 0x7A, 0x6C, 0x36, 0x4E, 0x4C, 0x72, 0x54, 0x0A, 0x51, 0x66, 0x76, + 0x36, 0x4D, 0x6F, 0x6F, 0x71, 0x74, 0x79, 0x75, 0x47, 0x43, 0x32, 0x6D, 0x44, 0x4F, 0x4C, 0x37, 0x4E, 0x69, 0x69, 0x34, 0x4C, 0x63, 0x4B, 0x32, 0x4E, 0x4A, 0x70, 0x4C, 0x75, 0x48, 0x76, 0x55, 0x42, 0x4B, 0x77, 0x72, 0x5A, 0x31, 0x70, 0x65, + 0x62, 0x62, 0x75, 0x43, 0x6F, 0x47, 0x52, 0x77, 0x36, 0x49, 0x59, 0x73, 0x4D, 0x48, 0x6B, 0x43, 0x74, 0x41, 0x2B, 0x66, 0x64, 0x5A, 0x6E, 0x37, 0x31, 0x75, 0x53, 0x41, 0x4E, 0x41, 0x2B, 0x69, 0x57, 0x0A, 0x2B, 0x59, 0x4A, 0x46, 0x31, 0x44, + 0x6E, 0x67, 0x6F, 0x41, 0x42, 0x64, 0x31, 0x35, 0x6A, 0x6D, 0x66, 0x5A, 0x35, 0x6E, 0x63, 0x38, 0x4F, 0x61, 0x4B, 0x76, 0x65, 0x72, 0x69, 0x36, 0x45, 0x36, 0x46, 0x4F, 0x38, 0x30, 0x76, 0x46, 0x49, 0x4F, 0x69, 0x5A, 0x69, 0x61, 0x42, 0x45, + 0x43, 0x45, 0x48, 0x58, 0x35, 0x46, 0x61, 0x5A, 0x4E, 0x58, 0x7A, 0x75, 0x76, 0x4F, 0x2B, 0x46, 0x42, 0x38, 0x54, 0x78, 0x78, 0x75, 0x42, 0x45, 0x4F, 0x62, 0x2B, 0x64, 0x59, 0x37, 0x0A, 0x49, 0x78, 0x6A, 0x70, 0x36, 0x6F, 0x37, 0x52, 0x54, + 0x55, 0x61, 0x4E, 0x38, 0x54, 0x76, 0x6B, 0x61, 0x73, 0x71, 0x36, 0x2B, 0x79, 0x4F, 0x33, 0x6D, 0x2F, 0x71, 0x5A, 0x41, 0x53, 0x6C, 0x61, 0x57, 0x46, 0x6F, 0x74, 0x34, 0x2F, 0x6E, 0x55, 0x62, 0x51, 0x34, 0x6D, 0x72, 0x63, 0x46, 0x75, 0x4E, + 0x4C, 0x77, 0x79, 0x2B, 0x41, 0x77, 0x46, 0x2B, 0x6D, 0x57, 0x6A, 0x32, 0x7A, 0x73, 0x33, 0x67, 0x79, 0x4C, 0x70, 0x31, 0x74, 0x78, 0x79, 0x4D, 0x2F, 0x31, 0x64, 0x0A, 0x38, 0x69, 0x43, 0x39, 0x64, 0x6A, 0x77, 0x6A, 0x32, 0x69, 0x6A, 0x33, + 0x2B, 0x52, 0x76, 0x72, 0x57, 0x57, 0x54, 0x56, 0x33, 0x46, 0x39, 0x79, 0x66, 0x69, 0x44, 0x38, 0x7A, 0x59, 0x6D, 0x31, 0x6B, 0x47, 0x64, 0x4E, 0x59, 0x6E, 0x6F, 0x2F, 0x54, 0x71, 0x30, 0x64, 0x77, 0x7A, 0x6E, 0x2B, 0x65, 0x76, 0x51, 0x6F, + 0x46, 0x74, 0x39, 0x42, 0x39, 0x6B, 0x69, 0x41, 0x42, 0x64, 0x63, 0x50, 0x55, 0x58, 0x6D, 0x73, 0x45, 0x4B, 0x76, 0x55, 0x37, 0x41, 0x4E, 0x6D, 0x0A, 0x35, 0x6D, 0x71, 0x77, 0x75, 0x6A, 0x47, 0x53, 0x51, 0x6B, 0x42, 0x71, 0x76, 0x6A, 0x72, + 0x54, 0x63, 0x75, 0x46, 0x71, 0x4E, 0x31, 0x57, 0x38, 0x72, 0x42, 0x32, 0x56, 0x74, 0x32, 0x6C, 0x68, 0x38, 0x6B, 0x4F, 0x52, 0x64, 0x4F, 0x61, 0x67, 0x30, 0x77, 0x6F, 0x6B, 0x52, 0x71, 0x45, 0x49, 0x72, 0x39, 0x62, 0x61, 0x52, 0x52, 0x6D, + 0x57, 0x31, 0x46, 0x4D, 0x64, 0x57, 0x34, 0x52, 0x35, 0x38, 0x4D, 0x44, 0x33, 0x52, 0x2B, 0x2B, 0x4C, 0x6A, 0x38, 0x55, 0x47, 0x0A, 0x72, 0x70, 0x31, 0x4D, 0x59, 0x70, 0x33, 0x2F, 0x52, 0x67, 0x54, 0x34, 0x30, 0x38, 0x6D, 0x32, 0x45, 0x43, + 0x56, 0x41, 0x64, 0x66, 0x34, 0x57, 0x71, 0x73, 0x6C, 0x4B, 0x59, 0x49, 0x59, 0x76, 0x75, 0x75, 0x38, 0x77, 0x64, 0x2B, 0x52, 0x55, 0x34, 0x72, 0x69, 0x45, 0x6D, 0x56, 0x69, 0x41, 0x71, 0x68, 0x4F, 0x4C, 0x55, 0x54, 0x70, 0x50, 0x53, 0x50, + 0x61, 0x4C, 0x74, 0x72, 0x4D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x6D, 0x61, 0x7A, 0x6F, 0x6E, + 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x54, 0x42, 0x6D, 0x79, 0x66, + 0x7A, 0x35, 0x6D, 0x2F, 0x6A, 0x41, 0x6F, 0x35, 0x34, 0x76, 0x42, 0x34, 0x69, 0x6B, 0x50, 0x6D, 0x6C, 0x6A, 0x5A, 0x62, 0x79, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, + 0x41, 0x44, 0x41, 0x35, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x50, 0x4D, 0x41, 0x30, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x47, 0x51, 0x57, 0x31, + 0x68, 0x65, 0x6D, 0x39, 0x75, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, 0x42, 0x62, 0x57, 0x46, 0x36, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, + 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x31, 0x0A, 0x4D, 0x44, 0x55, 0x79, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x34, 0x4D, 0x44, 0x45, 0x78, 0x4E, 0x7A, 0x41, 0x77, 0x4D, 0x44, + 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x4F, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x42, 0x6B, + 0x46, 0x74, 0x59, 0x58, 0x70, 0x76, 0x0A, 0x62, 0x6A, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x51, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, + 0x30, 0x45, 0x67, 0x4D, 0x54, 0x43, 0x43, 0x41, 0x53, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, + 0x51, 0x6F, 0x43, 0x0A, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4C, 0x4A, 0x34, 0x67, 0x48, 0x48, 0x4B, 0x65, 0x4E, 0x58, 0x6A, 0x63, 0x61, 0x39, 0x48, 0x67, 0x46, 0x42, 0x30, 0x66, 0x57, 0x37, 0x59, 0x31, 0x34, 0x68, 0x32, 0x39, 0x4A, 0x6C, 0x6F, + 0x39, 0x31, 0x67, 0x68, 0x59, 0x50, 0x6C, 0x30, 0x68, 0x41, 0x45, 0x76, 0x72, 0x41, 0x49, 0x74, 0x68, 0x74, 0x4F, 0x67, 0x51, 0x33, 0x70, 0x4F, 0x73, 0x71, 0x54, 0x51, 0x4E, 0x72, 0x6F, 0x42, 0x76, 0x6F, 0x33, 0x62, 0x53, 0x4D, 0x67, 0x48, + 0x0A, 0x46, 0x7A, 0x5A, 0x4D, 0x39, 0x4F, 0x36, 0x49, 0x49, 0x38, 0x63, 0x2B, 0x36, 0x7A, 0x66, 0x31, 0x74, 0x52, 0x6E, 0x34, 0x53, 0x57, 0x69, 0x77, 0x33, 0x74, 0x65, 0x35, 0x64, 0x6A, 0x67, 0x64, 0x59, 0x5A, 0x36, 0x6B, 0x2F, 0x6F, 0x49, + 0x32, 0x70, 0x65, 0x56, 0x4B, 0x56, 0x75, 0x52, 0x46, 0x34, 0x66, 0x6E, 0x39, 0x74, 0x42, 0x62, 0x36, 0x64, 0x4E, 0x71, 0x63, 0x6D, 0x7A, 0x55, 0x35, 0x4C, 0x2F, 0x71, 0x77, 0x49, 0x46, 0x41, 0x47, 0x62, 0x48, 0x72, 0x51, 0x0A, 0x67, 0x4C, + 0x4B, 0x6D, 0x2B, 0x61, 0x2F, 0x73, 0x52, 0x78, 0x6D, 0x50, 0x55, 0x44, 0x67, 0x48, 0x33, 0x4B, 0x4B, 0x48, 0x4F, 0x56, 0x6A, 0x34, 0x75, 0x74, 0x57, 0x70, 0x2B, 0x55, 0x68, 0x6E, 0x4D, 0x4A, 0x62, 0x75, 0x6C, 0x48, 0x68, 0x65, 0x62, 0x34, + 0x6D, 0x6A, 0x55, 0x63, 0x41, 0x77, 0x68, 0x6D, 0x61, 0x68, 0x52, 0x57, 0x61, 0x36, 0x56, 0x4F, 0x75, 0x6A, 0x77, 0x35, 0x48, 0x35, 0x53, 0x4E, 0x7A, 0x2F, 0x30, 0x65, 0x67, 0x77, 0x4C, 0x58, 0x30, 0x74, 0x0A, 0x64, 0x48, 0x41, 0x31, 0x31, + 0x34, 0x67, 0x6B, 0x39, 0x35, 0x37, 0x45, 0x57, 0x57, 0x36, 0x37, 0x63, 0x34, 0x63, 0x58, 0x38, 0x6A, 0x4A, 0x47, 0x4B, 0x4C, 0x68, 0x44, 0x2B, 0x72, 0x63, 0x64, 0x71, 0x73, 0x71, 0x30, 0x38, 0x70, 0x38, 0x6B, 0x44, 0x69, 0x31, 0x4C, 0x39, + 0x33, 0x46, 0x63, 0x58, 0x6D, 0x6E, 0x2F, 0x36, 0x70, 0x55, 0x43, 0x79, 0x7A, 0x69, 0x4B, 0x72, 0x6C, 0x41, 0x34, 0x62, 0x39, 0x76, 0x37, 0x4C, 0x57, 0x49, 0x62, 0x78, 0x63, 0x63, 0x65, 0x0A, 0x56, 0x4F, 0x46, 0x33, 0x34, 0x47, 0x66, 0x49, + 0x44, 0x35, 0x79, 0x48, 0x49, 0x39, 0x59, 0x2F, 0x51, 0x43, 0x42, 0x2F, 0x49, 0x49, 0x44, 0x45, 0x67, 0x45, 0x77, 0x2B, 0x4F, 0x79, 0x51, 0x6D, 0x6A, 0x67, 0x53, 0x75, 0x62, 0x4A, 0x72, 0x49, 0x71, 0x67, 0x30, 0x43, 0x41, 0x77, 0x45, 0x41, + 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, + 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x49, 0x51, 0x59, 0x7A, 0x49, 0x55, 0x30, 0x37, 0x4C, 0x77, 0x4D, 0x6C, 0x4A, 0x51, + 0x75, 0x43, 0x46, 0x6D, 0x63, 0x78, 0x37, 0x49, 0x51, 0x54, 0x67, 0x6F, 0x49, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0A, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, + 0x43, 0x59, 0x38, 0x6A, 0x64, 0x61, 0x51, 0x5A, 0x43, 0x68, 0x47, 0x73, 0x56, 0x32, 0x55, 0x53, 0x67, 0x67, 0x4E, 0x69, 0x4D, 0x4F, 0x72, 0x75, 0x59, 0x6F, 0x75, 0x36, 0x72, 0x34, 0x6C, 0x4B, 0x35, 0x49, 0x70, 0x44, 0x42, 0x2F, 0x47, 0x2F, + 0x77, 0x6B, 0x6A, 0x55, 0x75, 0x30, 0x79, 0x4B, 0x47, 0x58, 0x39, 0x72, 0x62, 0x78, 0x65, 0x6E, 0x44, 0x49, 0x55, 0x35, 0x50, 0x4D, 0x0A, 0x43, 0x43, 0x6A, 0x6A, 0x6D, 0x43, 0x58, 0x50, 0x49, 0x36, 0x54, 0x35, 0x33, 0x69, 0x48, 0x54, 0x66, + 0x49, 0x55, 0x4A, 0x72, 0x55, 0x36, 0x61, 0x64, 0x54, 0x72, 0x43, 0x43, 0x32, 0x71, 0x4A, 0x65, 0x48, 0x5A, 0x45, 0x52, 0x78, 0x68, 0x6C, 0x62, 0x49, 0x31, 0x42, 0x6A, 0x6A, 0x74, 0x2F, 0x6D, 0x73, 0x76, 0x30, 0x74, 0x61, 0x64, 0x51, 0x31, + 0x77, 0x55, 0x73, 0x4E, 0x2B, 0x67, 0x44, 0x53, 0x36, 0x33, 0x70, 0x59, 0x61, 0x41, 0x43, 0x62, 0x76, 0x58, 0x79, 0x0A, 0x38, 0x4D, 0x57, 0x79, 0x37, 0x56, 0x75, 0x33, 0x33, 0x50, 0x71, 0x55, 0x58, 0x48, 0x65, 0x65, 0x45, 0x36, 0x56, 0x2F, + 0x55, 0x71, 0x32, 0x56, 0x38, 0x76, 0x69, 0x54, 0x4F, 0x39, 0x36, 0x4C, 0x58, 0x46, 0x76, 0x4B, 0x57, 0x6C, 0x4A, 0x62, 0x59, 0x4B, 0x38, 0x55, 0x39, 0x30, 0x76, 0x76, 0x6F, 0x2F, 0x75, 0x66, 0x51, 0x4A, 0x56, 0x74, 0x4D, 0x56, 0x54, 0x38, + 0x51, 0x74, 0x50, 0x48, 0x52, 0x68, 0x38, 0x6A, 0x72, 0x64, 0x6B, 0x50, 0x53, 0x48, 0x43, 0x61, 0x0A, 0x32, 0x58, 0x56, 0x34, 0x63, 0x64, 0x46, 0x79, 0x51, 0x7A, 0x52, 0x31, 0x62, 0x6C, 0x64, 0x5A, 0x77, 0x67, 0x4A, 0x63, 0x4A, 0x6D, 0x41, + 0x70, 0x7A, 0x79, 0x4D, 0x5A, 0x46, 0x6F, 0x36, 0x49, 0x51, 0x36, 0x58, 0x55, 0x35, 0x4D, 0x73, 0x49, 0x2B, 0x79, 0x4D, 0x52, 0x51, 0x2B, 0x68, 0x44, 0x4B, 0x58, 0x4A, 0x69, 0x6F, 0x61, 0x6C, 0x64, 0x58, 0x67, 0x6A, 0x55, 0x6B, 0x4B, 0x36, + 0x34, 0x32, 0x4D, 0x34, 0x55, 0x77, 0x74, 0x42, 0x56, 0x38, 0x6F, 0x62, 0x32, 0x0A, 0x78, 0x4A, 0x4E, 0x44, 0x64, 0x32, 0x5A, 0x68, 0x77, 0x4C, 0x6E, 0x6F, 0x51, 0x64, 0x65, 0x58, 0x65, 0x47, 0x41, 0x44, 0x62, 0x6B, 0x70, 0x79, 0x72, 0x71, + 0x58, 0x52, 0x66, 0x62, 0x6F, 0x51, 0x6E, 0x6F, 0x5A, 0x73, 0x47, 0x34, 0x71, 0x35, 0x57, 0x54, 0x50, 0x34, 0x36, 0x38, 0x53, 0x51, 0x76, 0x76, 0x47, 0x35, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x6D, 0x61, 0x7A, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, + 0x51, 0x54, 0x43, 0x43, 0x41, 0x79, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x54, 0x42, 0x6D, 0x79, 0x66, 0x30, 0x70, 0x59, 0x31, 0x68, 0x70, 0x38, 0x4B, 0x44, 0x2B, 0x57, 0x47, 0x65, 0x50, 0x68, 0x62, 0x4A, 0x72, 0x75, 0x4B, + 0x4E, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x41, 0x35, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, + 0x56, 0x55, 0x7A, 0x45, 0x50, 0x4D, 0x41, 0x30, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x47, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, + 0x42, 0x62, 0x57, 0x46, 0x36, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x31, 0x0A, 0x4D, 0x44, 0x55, 0x79, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, + 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x44, 0x55, 0x79, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x4F, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, + 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x42, 0x6B, 0x46, 0x74, 0x59, 0x58, 0x70, 0x76, 0x0A, 0x62, 0x6A, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, + 0x78, 0x4D, 0x51, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, + 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x0A, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4B, 0x32, 0x57, 0x6E, 0x79, 0x32, 0x63, 0x53, 0x6B, 0x78, 0x4B, + 0x67, 0x58, 0x6C, 0x52, 0x6D, 0x65, 0x79, 0x4B, 0x79, 0x32, 0x74, 0x67, 0x55, 0x52, 0x4F, 0x38, 0x54, 0x57, 0x30, 0x47, 0x2F, 0x4C, 0x41, 0x49, 0x6A, 0x64, 0x30, 0x5A, 0x45, 0x47, 0x72, 0x48, 0x4A, 0x67, 0x77, 0x31, 0x32, 0x4D, 0x42, 0x76, + 0x49, 0x49, 0x54, 0x70, 0x6C, 0x4C, 0x47, 0x62, 0x68, 0x51, 0x50, 0x44, 0x57, 0x39, 0x74, 0x4B, 0x36, 0x4D, 0x6A, 0x34, 0x0A, 0x6B, 0x48, 0x62, 0x5A, 0x57, 0x30, 0x2F, 0x6A, 0x54, 0x4F, 0x67, 0x47, 0x4E, 0x6B, 0x33, 0x4D, 0x6D, 0x71, 0x77, + 0x39, 0x44, 0x4A, 0x41, 0x72, 0x6B, 0x74, 0x51, 0x47, 0x47, 0x57, 0x43, 0x73, 0x4E, 0x30, 0x52, 0x35, 0x68, 0x59, 0x47, 0x43, 0x72, 0x56, 0x6F, 0x33, 0x34, 0x41, 0x33, 0x4D, 0x6E, 0x61, 0x5A, 0x4D, 0x55, 0x6E, 0x62, 0x71, 0x51, 0x35, 0x32, + 0x33, 0x42, 0x4E, 0x46, 0x51, 0x39, 0x6C, 0x58, 0x67, 0x31, 0x64, 0x4B, 0x6D, 0x53, 0x59, 0x58, 0x70, 0x0A, 0x4E, 0x2B, 0x6E, 0x4B, 0x66, 0x71, 0x35, 0x63, 0x6C, 0x55, 0x31, 0x49, 0x6D, 0x6A, 0x2B, 0x75, 0x49, 0x46, 0x70, 0x74, 0x69, 0x4A, + 0x58, 0x5A, 0x4E, 0x4C, 0x68, 0x53, 0x47, 0x6B, 0x4F, 0x51, 0x73, 0x4C, 0x39, 0x73, 0x42, 0x62, 0x6D, 0x32, 0x65, 0x4C, 0x66, 0x71, 0x30, 0x4F, 0x51, 0x36, 0x50, 0x42, 0x4A, 0x54, 0x59, 0x76, 0x39, 0x4B, 0x38, 0x6E, 0x75, 0x2B, 0x4E, 0x51, + 0x57, 0x70, 0x45, 0x6A, 0x54, 0x6A, 0x38, 0x32, 0x52, 0x30, 0x59, 0x69, 0x77, 0x39, 0x0A, 0x41, 0x45, 0x6C, 0x61, 0x4B, 0x50, 0x34, 0x79, 0x52, 0x4C, 0x75, 0x48, 0x33, 0x57, 0x55, 0x6E, 0x41, 0x6E, 0x45, 0x37, 0x32, 0x6B, 0x72, 0x33, 0x48, + 0x39, 0x72, 0x4E, 0x39, 0x79, 0x46, 0x56, 0x6B, 0x45, 0x38, 0x50, 0x37, 0x4B, 0x36, 0x43, 0x34, 0x5A, 0x39, 0x72, 0x32, 0x55, 0x58, 0x54, 0x75, 0x2F, 0x42, 0x66, 0x68, 0x2B, 0x30, 0x38, 0x4C, 0x44, 0x6D, 0x47, 0x32, 0x6A, 0x2F, 0x65, 0x37, + 0x48, 0x4A, 0x56, 0x36, 0x33, 0x6D, 0x6A, 0x72, 0x64, 0x76, 0x64, 0x0A, 0x66, 0x4C, 0x43, 0x36, 0x48, 0x4D, 0x37, 0x38, 0x33, 0x6B, 0x38, 0x31, 0x64, 0x73, 0x38, 0x50, 0x2B, 0x48, 0x67, 0x66, 0x61, 0x6A, 0x5A, 0x52, 0x52, 0x69, 0x64, 0x68, + 0x57, 0x2B, 0x6D, 0x65, 0x7A, 0x2F, 0x43, 0x69, 0x56, 0x58, 0x31, 0x38, 0x4A, 0x59, 0x70, 0x76, 0x4C, 0x37, 0x54, 0x46, 0x7A, 0x34, 0x51, 0x75, 0x4B, 0x2F, 0x30, 0x4E, 0x55, 0x52, 0x42, 0x73, 0x2B, 0x31, 0x38, 0x62, 0x76, 0x42, 0x74, 0x2B, + 0x78, 0x61, 0x34, 0x37, 0x6D, 0x41, 0x45, 0x78, 0x0A, 0x6B, 0x76, 0x38, 0x4C, 0x56, 0x2F, 0x53, 0x61, 0x73, 0x72, 0x6C, 0x58, 0x36, 0x61, 0x76, 0x76, 0x44, 0x58, 0x62, 0x52, 0x38, 0x4F, 0x37, 0x30, 0x7A, 0x6F, 0x61, 0x6E, 0x34, 0x47, 0x37, + 0x70, 0x74, 0x47, 0x6D, 0x68, 0x33, 0x32, 0x6E, 0x32, 0x4D, 0x38, 0x5A, 0x70, 0x4C, 0x70, 0x63, 0x54, 0x6E, 0x71, 0x57, 0x48, 0x73, 0x46, 0x63, 0x51, 0x67, 0x54, 0x66, 0x4A, 0x55, 0x37, 0x4F, 0x37, 0x66, 0x2F, 0x61, 0x53, 0x30, 0x5A, 0x7A, + 0x51, 0x47, 0x50, 0x53, 0x53, 0x0A, 0x62, 0x74, 0x71, 0x44, 0x54, 0x36, 0x5A, 0x6A, 0x6D, 0x55, 0x79, 0x6C, 0x2B, 0x31, 0x37, 0x76, 0x49, 0x57, 0x52, 0x36, 0x49, 0x46, 0x39, 0x73, 0x5A, 0x49, 0x55, 0x56, 0x79, 0x7A, 0x66, 0x70, 0x59, 0x67, + 0x77, 0x4C, 0x4B, 0x68, 0x62, 0x63, 0x41, 0x53, 0x34, 0x79, 0x32, 0x6A, 0x35, 0x4C, 0x39, 0x5A, 0x34, 0x36, 0x39, 0x68, 0x64, 0x41, 0x6C, 0x4F, 0x2B, 0x65, 0x6B, 0x51, 0x69, 0x47, 0x2B, 0x72, 0x35, 0x6A, 0x71, 0x46, 0x6F, 0x7A, 0x37, 0x4D, + 0x74, 0x30, 0x0A, 0x51, 0x35, 0x58, 0x35, 0x62, 0x47, 0x6C, 0x53, 0x4E, 0x73, 0x63, 0x70, 0x62, 0x2F, 0x78, 0x56, 0x41, 0x31, 0x77, 0x66, 0x2B, 0x35, 0x2B, 0x39, 0x52, 0x2B, 0x76, 0x6E, 0x53, 0x55, 0x65, 0x56, 0x43, 0x30, 0x36, 0x4A, 0x49, + 0x67, 0x6C, 0x4A, 0x34, 0x50, 0x56, 0x68, 0x48, 0x76, 0x47, 0x2F, 0x4C, 0x6F, 0x70, 0x79, 0x62, 0x6F, 0x42, 0x5A, 0x2F, 0x31, 0x63, 0x36, 0x2B, 0x58, 0x55, 0x79, 0x6F, 0x30, 0x35, 0x66, 0x37, 0x4F, 0x30, 0x6F, 0x59, 0x74, 0x6C, 0x4E, 0x0A, + 0x63, 0x2F, 0x4C, 0x4D, 0x67, 0x52, 0x64, 0x67, 0x37, 0x63, 0x33, 0x72, 0x33, 0x4E, 0x75, 0x6E, 0x79, 0x73, 0x56, 0x2B, 0x41, 0x72, 0x33, 0x79, 0x56, 0x41, 0x68, 0x55, 0x2F, 0x62, 0x51, 0x74, 0x43, 0x53, 0x77, 0x58, 0x56, 0x45, 0x71, 0x59, + 0x30, 0x56, 0x54, 0x68, 0x55, 0x57, 0x63, 0x49, 0x30, 0x75, 0x31, 0x75, 0x66, 0x6D, 0x38, 0x2F, 0x30, 0x69, 0x32, 0x42, 0x57, 0x53, 0x6C, 0x6D, 0x79, 0x35, 0x41, 0x35, 0x6C, 0x52, 0x45, 0x65, 0x64, 0x43, 0x66, 0x2B, 0x0A, 0x33, 0x65, 0x75, + 0x76, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, + 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x77, 0x0A, 0x44, 0x50, 0x42, 0x4D, 0x4D, 0x50, + 0x51, 0x46, 0x57, 0x41, 0x4A, 0x49, 0x2F, 0x54, 0x50, 0x6C, 0x55, 0x71, 0x39, 0x4C, 0x68, 0x4F, 0x4E, 0x6D, 0x55, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x41, + 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x71, 0x71, 0x69, 0x41, 0x6A, 0x77, 0x35, 0x34, 0x6F, 0x2B, 0x43, 0x69, 0x31, 0x4D, 0x33, 0x6D, 0x39, 0x5A, 0x68, 0x36, 0x4F, 0x2B, 0x6F, 0x41, 0x0A, 0x41, 0x37, 0x43, 0x58, 0x44, 0x70, 0x4F, 0x38, 0x57, + 0x71, 0x6A, 0x32, 0x4C, 0x49, 0x78, 0x79, 0x68, 0x36, 0x6D, 0x78, 0x2F, 0x48, 0x39, 0x7A, 0x2F, 0x57, 0x4E, 0x78, 0x65, 0x4B, 0x57, 0x48, 0x57, 0x63, 0x38, 0x77, 0x34, 0x51, 0x30, 0x51, 0x73, 0x68, 0x4E, 0x61, 0x62, 0x59, 0x4C, 0x31, 0x61, + 0x75, 0x61, 0x41, 0x6E, 0x36, 0x41, 0x46, 0x43, 0x32, 0x6A, 0x6B, 0x52, 0x32, 0x76, 0x48, 0x61, 0x74, 0x2B, 0x32, 0x2F, 0x58, 0x63, 0x79, 0x63, 0x75, 0x55, 0x59, 0x0A, 0x2B, 0x67, 0x6E, 0x30, 0x6F, 0x4A, 0x4D, 0x73, 0x58, 0x64, 0x4B, 0x4D, + 0x64, 0x59, 0x56, 0x32, 0x5A, 0x5A, 0x41, 0x4D, 0x41, 0x33, 0x6D, 0x33, 0x4D, 0x53, 0x4E, 0x6A, 0x72, 0x58, 0x69, 0x44, 0x43, 0x59, 0x5A, 0x6F, 0x68, 0x4D, 0x72, 0x2F, 0x2B, 0x63, 0x38, 0x6D, 0x6D, 0x70, 0x4A, 0x35, 0x35, 0x38, 0x31, 0x4C, + 0x78, 0x65, 0x64, 0x68, 0x70, 0x78, 0x66, 0x4C, 0x38, 0x36, 0x6B, 0x53, 0x6B, 0x35, 0x4E, 0x72, 0x70, 0x2B, 0x67, 0x76, 0x55, 0x35, 0x4C, 0x45, 0x0A, 0x59, 0x46, 0x69, 0x77, 0x7A, 0x41, 0x4A, 0x52, 0x47, 0x46, 0x75, 0x46, 0x6A, 0x57, 0x4A, + 0x5A, 0x59, 0x37, 0x61, 0x74, 0x74, 0x4E, 0x36, 0x61, 0x2B, 0x79, 0x62, 0x33, 0x41, 0x43, 0x66, 0x41, 0x58, 0x56, 0x55, 0x33, 0x64, 0x4A, 0x6E, 0x4A, 0x55, 0x48, 0x2F, 0x6A, 0x57, 0x53, 0x35, 0x45, 0x34, 0x79, 0x77, 0x6C, 0x37, 0x75, 0x78, + 0x4D, 0x4D, 0x6E, 0x65, 0x30, 0x6E, 0x78, 0x72, 0x70, 0x53, 0x31, 0x30, 0x67, 0x78, 0x64, 0x72, 0x39, 0x48, 0x49, 0x63, 0x57, 0x0A, 0x78, 0x6B, 0x50, 0x6F, 0x31, 0x4C, 0x73, 0x6D, 0x6D, 0x6B, 0x56, 0x77, 0x58, 0x71, 0x6B, 0x4C, 0x4E, 0x31, + 0x50, 0x69, 0x52, 0x6E, 0x73, 0x6E, 0x2F, 0x65, 0x42, 0x47, 0x38, 0x6F, 0x6D, 0x33, 0x7A, 0x45, 0x4B, 0x32, 0x79, 0x79, 0x67, 0x6D, 0x62, 0x74, 0x6D, 0x6C, 0x79, 0x54, 0x72, 0x49, 0x51, 0x52, 0x4E, 0x67, 0x39, 0x31, 0x43, 0x4D, 0x46, 0x61, + 0x36, 0x79, 0x62, 0x52, 0x6F, 0x56, 0x47, 0x6C, 0x64, 0x34, 0x35, 0x70, 0x49, 0x71, 0x32, 0x57, 0x57, 0x51, 0x0A, 0x67, 0x6A, 0x39, 0x73, 0x41, 0x71, 0x2B, 0x75, 0x45, 0x6A, 0x6F, 0x6E, 0x6C, 0x6A, 0x59, 0x45, 0x31, 0x78, 0x32, 0x69, 0x67, + 0x47, 0x4F, 0x70, 0x6D, 0x2F, 0x48, 0x6C, 0x75, 0x72, 0x52, 0x38, 0x46, 0x4C, 0x42, 0x4F, 0x79, 0x62, 0x45, 0x66, 0x64, 0x46, 0x38, 0x34, 0x39, 0x6C, 0x48, 0x71, 0x6D, 0x2F, 0x6F, 0x73, 0x6F, 0x68, 0x48, 0x55, 0x71, 0x53, 0x30, 0x6E, 0x47, + 0x6B, 0x57, 0x78, 0x72, 0x37, 0x4A, 0x4F, 0x63, 0x51, 0x33, 0x41, 0x57, 0x45, 0x62, 0x57, 0x0A, 0x61, 0x51, 0x62, 0x4C, 0x55, 0x38, 0x75, 0x7A, 0x2F, 0x6D, 0x74, 0x42, 0x7A, 0x55, 0x46, 0x2B, 0x66, 0x55, 0x77, 0x50, 0x66, 0x48, 0x4A, 0x35, + 0x65, 0x6C, 0x6E, 0x4E, 0x58, 0x6B, 0x6F, 0x4F, 0x72, 0x4A, 0x75, 0x70, 0x6D, 0x48, 0x4E, 0x35, 0x66, 0x4C, 0x54, 0x30, 0x7A, 0x4C, 0x6D, 0x34, 0x42, 0x77, 0x79, 0x79, 0x64, 0x46, 0x79, 0x34, 0x78, 0x32, 0x2B, 0x49, 0x6F, 0x5A, 0x43, 0x6E, + 0x39, 0x4B, 0x72, 0x35, 0x76, 0x32, 0x63, 0x36, 0x39, 0x42, 0x6F, 0x56, 0x0A, 0x59, 0x68, 0x36, 0x33, 0x6E, 0x37, 0x34, 0x39, 0x73, 0x53, 0x6D, 0x76, 0x5A, 0x36, 0x45, 0x53, 0x38, 0x6C, 0x67, 0x51, 0x47, 0x56, 0x4D, 0x44, 0x4D, 0x42, 0x75, + 0x34, 0x47, 0x6F, 0x6E, 0x32, 0x6E, 0x4C, 0x32, 0x58, 0x41, 0x34, 0x36, 0x6A, 0x43, 0x66, 0x4D, 0x64, 0x69, 0x79, 0x48, 0x78, 0x74, 0x4E, 0x2F, 0x6B, 0x48, 0x4E, 0x47, 0x66, 0x5A, 0x51, 0x49, 0x47, 0x36, 0x6C, 0x7A, 0x57, 0x45, 0x37, 0x4F, + 0x45, 0x37, 0x36, 0x4B, 0x6C, 0x58, 0x49, 0x78, 0x33, 0x0A, 0x4B, 0x61, 0x64, 0x6F, 0x77, 0x47, 0x75, 0x75, 0x51, 0x4E, 0x4B, 0x6F, 0x74, 0x4F, 0x72, 0x4E, 0x38, 0x49, 0x31, 0x4C, 0x4F, 0x4A, 0x77, 0x5A, 0x6D, 0x68, 0x73, 0x6F, 0x56, 0x4C, + 0x69, 0x4A, 0x6B, 0x4F, 0x2F, 0x4B, 0x64, 0x59, 0x45, 0x2B, 0x48, 0x76, 0x4A, 0x6B, 0x4A, 0x4D, 0x63, 0x59, 0x72, 0x30, 0x37, 0x2F, 0x52, 0x35, 0x34, 0x48, 0x39, 0x6A, 0x56, 0x6C, 0x70, 0x4E, 0x4D, 0x4B, 0x56, 0x76, 0x2F, 0x31, 0x46, 0x32, + 0x52, 0x73, 0x37, 0x36, 0x67, 0x69, 0x0A, 0x4A, 0x55, 0x6D, 0x54, 0x74, 0x74, 0x38, 0x41, 0x46, 0x39, 0x70, 0x59, 0x66, 0x6C, 0x33, 0x75, 0x78, 0x52, 0x75, 0x77, 0x30, 0x64, 0x46, 0x66, 0x49, 0x52, 0x44, 0x48, 0x2B, 0x66, 0x4F, 0x36, 0x41, + 0x67, 0x6F, 0x6E, 0x42, 0x38, 0x58, 0x78, 0x31, 0x73, 0x66, 0x54, 0x34, 0x50, 0x73, 0x4A, 0x59, 0x47, 0x77, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x6D, 0x61, 0x7A, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x42, 0x74, 0x6A, 0x43, 0x43, 0x41, 0x56, 0x75, + 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x54, 0x42, 0x6D, 0x79, 0x66, 0x31, 0x58, 0x53, 0x58, 0x4E, 0x6D, 0x59, 0x2F, 0x4F, 0x77, 0x75, 0x61, 0x32, 0x65, 0x69, 0x65, 0x64, 0x67, 0x50, 0x79, 0x53, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, + 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x6A, 0x41, 0x35, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x50, 0x4D, 0x41, 0x30, 0x47, 0x41, 0x31, + 0x55, 0x45, 0x43, 0x68, 0x4D, 0x47, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, 0x42, 0x62, 0x57, 0x46, 0x36, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, + 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x7A, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x55, 0x79, 0x0A, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, + 0x44, 0x55, 0x79, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x4F, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x42, 0x6B, 0x46, 0x74, 0x59, 0x58, 0x70, 0x76, 0x62, 0x6A, 0x45, 0x5A, 0x0A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x51, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, + 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x7A, 0x42, 0x5A, 0x4D, 0x42, 0x4D, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, + 0x41, 0x77, 0x45, 0x48, 0x41, 0x30, 0x49, 0x41, 0x42, 0x43, 0x6D, 0x58, 0x70, 0x38, 0x5A, 0x42, 0x0A, 0x66, 0x38, 0x41, 0x4E, 0x6D, 0x2B, 0x67, 0x42, 0x47, 0x31, 0x62, 0x47, 0x38, 0x6C, 0x4B, 0x6C, 0x75, 0x69, 0x32, 0x79, 0x45, 0x75, 0x6A, + 0x53, 0x4C, 0x74, 0x66, 0x36, 0x79, 0x63, 0x58, 0x59, 0x71, 0x6D, 0x30, 0x66, 0x63, 0x34, 0x45, 0x37, 0x4F, 0x35, 0x68, 0x72, 0x4F, 0x58, 0x77, 0x7A, 0x70, 0x63, 0x56, 0x4F, 0x68, 0x6F, 0x36, 0x41, 0x46, 0x32, 0x68, 0x69, 0x52, 0x56, 0x64, + 0x39, 0x52, 0x46, 0x67, 0x64, 0x73, 0x7A, 0x66, 0x6C, 0x5A, 0x77, 0x6A, 0x72, 0x0A, 0x5A, 0x74, 0x36, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, + 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, + 0x53, 0x72, 0x74, 0x74, 0x76, 0x58, 0x42, 0x70, 0x34, 0x33, 0x0A, 0x72, 0x44, 0x43, 0x47, 0x42, 0x35, 0x46, 0x77, 0x78, 0x35, 0x7A, 0x45, 0x47, 0x62, 0x46, 0x34, 0x77, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, + 0x51, 0x51, 0x44, 0x41, 0x67, 0x4E, 0x4A, 0x41, 0x44, 0x42, 0x47, 0x41, 0x69, 0x45, 0x41, 0x34, 0x49, 0x57, 0x53, 0x6F, 0x78, 0x65, 0x33, 0x6A, 0x66, 0x6B, 0x72, 0x42, 0x71, 0x57, 0x54, 0x72, 0x42, 0x71, 0x59, 0x61, 0x47, 0x46, 0x79, 0x2B, + 0x75, 0x47, 0x68, 0x30, 0x50, 0x73, 0x63, 0x0A, 0x65, 0x47, 0x43, 0x6D, 0x51, 0x35, 0x6E, 0x46, 0x75, 0x4D, 0x51, 0x43, 0x49, 0x51, 0x43, 0x63, 0x41, 0x75, 0x2F, 0x78, 0x6C, 0x4A, 0x79, 0x7A, 0x6C, 0x76, 0x6E, 0x72, 0x78, 0x69, 0x72, 0x34, + 0x74, 0x69, 0x7A, 0x2B, 0x4F, 0x70, 0x41, 0x55, 0x46, 0x74, 0x65, 0x4D, 0x59, 0x79, 0x52, 0x49, 0x48, 0x4E, 0x38, 0x77, 0x66, 0x64, 0x56, 0x6F, 0x4F, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x6D, 0x61, 0x7A, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x34, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, + 0x49, 0x42, 0x38, 0x6A, 0x43, 0x43, 0x41, 0x58, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x54, 0x42, 0x6D, 0x79, 0x66, 0x31, 0x38, 0x47, 0x37, 0x45, 0x45, 0x77, 0x70, 0x51, 0x2B, 0x56, 0x78, 0x65, 0x33, 0x73, 0x73, 0x79, 0x42, + 0x72, 0x42, 0x44, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x41, 0x35, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, + 0x7A, 0x45, 0x50, 0x4D, 0x41, 0x30, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x47, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, 0x42, 0x62, + 0x57, 0x46, 0x36, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x30, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x55, 0x79, 0x0A, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, + 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x44, 0x55, 0x79, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x4F, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, + 0x56, 0x56, 0x4D, 0x78, 0x44, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x42, 0x6B, 0x46, 0x74, 0x59, 0x58, 0x70, 0x76, 0x62, 0x6A, 0x45, 0x5A, 0x0A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, + 0x51, 0x51, 0x57, 0x31, 0x68, 0x65, 0x6D, 0x39, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4E, 0x44, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, + 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4E, 0x4B, 0x72, 0x69, 0x6A, 0x64, 0x50, 0x6F, 0x31, 0x4D, 0x4E, 0x0A, 0x2F, 0x73, 0x47, 0x4B, 0x65, 0x30, 0x75, 0x6F, 0x65, 0x30, 0x5A, 0x4C, 0x59, 0x37, + 0x42, 0x69, 0x39, 0x69, 0x30, 0x62, 0x32, 0x77, 0x68, 0x78, 0x49, 0x64, 0x49, 0x41, 0x36, 0x47, 0x4F, 0x39, 0x6D, 0x69, 0x66, 0x37, 0x38, 0x44, 0x6C, 0x75, 0x58, 0x65, 0x6F, 0x39, 0x70, 0x63, 0x6D, 0x42, 0x71, 0x71, 0x4E, 0x62, 0x49, 0x4A, + 0x68, 0x46, 0x58, 0x52, 0x62, 0x62, 0x2F, 0x65, 0x67, 0x51, 0x62, 0x65, 0x4F, 0x63, 0x34, 0x4F, 0x4F, 0x39, 0x58, 0x34, 0x52, 0x69, 0x0A, 0x38, 0x33, 0x42, 0x6B, 0x4D, 0x36, 0x44, 0x4C, 0x4A, 0x43, 0x39, 0x77, 0x75, 0x6F, 0x69, 0x68, 0x4B, + 0x71, 0x42, 0x31, 0x2B, 0x49, 0x47, 0x75, 0x59, 0x67, 0x62, 0x45, 0x67, 0x64, 0x73, 0x35, 0x62, 0x69, 0x6D, 0x77, 0x48, 0x76, 0x6F, 0x75, 0x58, 0x4B, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, + 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, + 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4E, 0x50, 0x73, 0x78, 0x7A, 0x70, 0x6C, 0x62, 0x73, 0x7A, 0x68, 0x32, 0x6E, 0x61, 0x61, 0x56, 0x76, 0x75, 0x63, 0x38, 0x34, 0x5A, 0x74, 0x56, 0x2B, 0x57, 0x42, 0x4D, 0x41, 0x6F, 0x47, + 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x67, 0x41, 0x0A, 0x4D, 0x47, 0x55, 0x43, 0x4D, 0x44, 0x71, 0x4C, 0x49, 0x66, 0x47, 0x39, 0x66, 0x68, 0x47, 0x74, 0x30, 0x4F, 0x39, 0x59, 0x6C, 0x69, 0x2F, + 0x57, 0x36, 0x35, 0x31, 0x2B, 0x6B, 0x49, 0x30, 0x72, 0x7A, 0x32, 0x5A, 0x56, 0x77, 0x79, 0x7A, 0x6A, 0x4B, 0x4B, 0x6C, 0x77, 0x43, 0x6B, 0x63, 0x4F, 0x38, 0x44, 0x64, 0x5A, 0x45, 0x76, 0x38, 0x74, 0x6D, 0x5A, 0x51, 0x6F, 0x54, 0x69, 0x70, + 0x50, 0x4E, 0x55, 0x30, 0x7A, 0x57, 0x67, 0x49, 0x78, 0x41, 0x4F, 0x70, 0x31, 0x0A, 0x41, 0x45, 0x34, 0x37, 0x78, 0x44, 0x71, 0x55, 0x45, 0x70, 0x48, 0x4A, 0x57, 0x45, 0x61, 0x64, 0x49, 0x52, 0x4E, 0x79, 0x70, 0x34, 0x69, 0x63, 0x69, 0x75, + 0x52, 0x4D, 0x53, 0x74, 0x75, 0x57, 0x31, 0x4B, 0x79, 0x4C, 0x61, 0x32, 0x74, 0x4A, 0x45, 0x6C, 0x4D, 0x7A, 0x72, 0x64, 0x66, 0x6B, 0x76, 0x69, 0x54, 0x38, 0x74, 0x51, 0x70, 0x32, 0x31, 0x4B, 0x57, 0x38, 0x45, 0x41, 0x3D, 0x3D, 0x0A, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x55, 0x42, 0x49, 0x54, 0x41, 0x4B, 0x20, 0x4B, 0x61, 0x6D, 0x75, 0x20, 0x53, + 0x4D, 0x20, 0x53, 0x53, 0x4C, 0x20, 0x4B, 0x6F, 0x6B, 0x20, 0x53, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x6B, 0x61, 0x73, 0x69, 0x20, 0x2D, 0x20, 0x53, 0x75, 0x72, 0x75, 0x6D, 0x20, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x45, 0x59, 0x7A, 0x43, 0x43, 0x41, 0x30, 0x75, 0x67, 0x41, 0x77, + 0x49, 0x42, 0x41, 0x67, 0x49, 0x42, 0x41, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x43, 0x42, 0x30, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, + 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x46, 0x49, 0x78, 0x47, 0x44, 0x41, 0x57, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x0A, 0x44, 0x30, 0x64, 0x6C, 0x59, 0x6E, 0x70, 0x6C, 0x49, 0x43, 0x30, 0x67, 0x53, 0x32, 0x39, 0x6A, 0x59, + 0x57, 0x56, 0x73, 0x61, 0x54, 0x46, 0x43, 0x4D, 0x45, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x35, 0x56, 0x48, 0x56, 0x79, 0x61, 0x32, 0x6C, 0x35, 0x5A, 0x53, 0x42, 0x43, 0x61, 0x57, 0x78, 0x70, 0x62, 0x58, 0x4E, 0x6C, 0x62, + 0x43, 0x42, 0x32, 0x5A, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x74, 0x75, 0x62, 0x32, 0x78, 0x76, 0x61, 0x6D, 0x6C, 0x72, 0x0A, 0x49, 0x45, 0x46, 0x79, 0x59, 0x58, 0x4E, 0x30, 0x61, 0x58, 0x4A, 0x74, 0x59, 0x53, 0x42, 0x4C, 0x64, 0x58, 0x4A, 0x31, + 0x62, 0x58, 0x55, 0x67, 0x4C, 0x53, 0x42, 0x55, 0x56, 0x55, 0x4A, 0x4A, 0x56, 0x45, 0x46, 0x4C, 0x4D, 0x53, 0x30, 0x77, 0x4B, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x79, 0x52, 0x4C, 0x59, 0x57, 0x31, 0x31, 0x49, 0x46, 0x4E, 0x6C, + 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x72, 0x59, 0x58, 0x4E, 0x35, 0x62, 0x32, 0x34, 0x67, 0x0A, 0x54, 0x57, 0x56, 0x79, 0x61, 0x32, 0x56, 0x36, 0x61, 0x53, 0x41, 0x74, 0x49, 0x45, 0x74, 0x68, 0x62, 0x58, 0x55, 0x67, 0x55, 0x30, 0x30, + 0x78, 0x4E, 0x6A, 0x41, 0x30, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x4C, 0x56, 0x52, 0x56, 0x51, 0x6B, 0x6C, 0x55, 0x51, 0x55, 0x73, 0x67, 0x53, 0x32, 0x46, 0x74, 0x64, 0x53, 0x42, 0x54, 0x54, 0x53, 0x42, 0x54, 0x55, 0x30, 0x77, + 0x67, 0x53, 0x32, 0x39, 0x72, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x0A, 0x5A, 0x6D, 0x6C, 0x72, 0x59, 0x58, 0x4E, 0x70, 0x49, 0x43, 0x30, 0x67, 0x55, 0x33, 0x56, 0x79, 0x64, 0x57, 0x30, 0x67, 0x4D, 0x54, 0x41, 0x65, 0x46, 0x77, + 0x30, 0x78, 0x4D, 0x7A, 0x45, 0x78, 0x4D, 0x6A, 0x55, 0x77, 0x4F, 0x44, 0x49, 0x31, 0x4E, 0x54, 0x56, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x7A, 0x45, 0x77, 0x4D, 0x6A, 0x55, 0x77, 0x4F, 0x44, 0x49, 0x31, 0x4E, 0x54, 0x56, 0x61, 0x4D, 0x49, + 0x48, 0x53, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x55, 0x55, 0x6A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x50, 0x52, 0x32, 0x56, 0x69, 0x65, + 0x6D, 0x55, 0x67, 0x4C, 0x53, 0x42, 0x4C, 0x62, 0x32, 0x4E, 0x68, 0x5A, 0x57, 0x78, 0x70, 0x4D, 0x55, 0x49, 0x77, 0x51, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x7A, 0x6C, 0x55, 0x64, 0x58, 0x4A, 0x72, 0x61, 0x58, 0x6C, 0x6C, 0x49, + 0x45, 0x4A, 0x70, 0x62, 0x47, 0x6C, 0x74, 0x0A, 0x63, 0x32, 0x56, 0x73, 0x49, 0x48, 0x5A, 0x6C, 0x49, 0x46, 0x52, 0x6C, 0x61, 0x32, 0x35, 0x76, 0x62, 0x47, 0x39, 0x71, 0x61, 0x57, 0x73, 0x67, 0x51, 0x58, 0x4A, 0x68, 0x63, 0x33, 0x52, 0x70, + 0x63, 0x6D, 0x31, 0x68, 0x49, 0x45, 0x74, 0x31, 0x63, 0x6E, 0x56, 0x74, 0x64, 0x53, 0x41, 0x74, 0x49, 0x46, 0x52, 0x56, 0x51, 0x6B, 0x6C, 0x55, 0x51, 0x55, 0x73, 0x78, 0x4C, 0x54, 0x41, 0x72, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, + 0x4A, 0x45, 0x74, 0x68, 0x0A, 0x62, 0x58, 0x55, 0x67, 0x55, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x74, 0x68, 0x63, 0x33, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x4E, 0x5A, 0x58, 0x4A, 0x72, 0x5A, 0x58, 0x70, 0x70, 0x49, 0x43, 0x30, + 0x67, 0x53, 0x32, 0x46, 0x74, 0x64, 0x53, 0x42, 0x54, 0x54, 0x54, 0x45, 0x32, 0x4D, 0x44, 0x51, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x74, 0x56, 0x46, 0x56, 0x43, 0x53, 0x56, 0x52, 0x42, 0x53, 0x79, 0x42, 0x4C, 0x59, 0x57, 0x31, + 0x31, 0x0A, 0x49, 0x46, 0x4E, 0x4E, 0x49, 0x46, 0x4E, 0x54, 0x54, 0x43, 0x42, 0x4C, 0x62, 0x32, 0x73, 0x67, 0x55, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x74, 0x68, 0x63, 0x32, 0x6B, 0x67, 0x4C, 0x53, 0x42, 0x54, 0x64, 0x58, + 0x4A, 0x31, 0x62, 0x53, 0x41, 0x78, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x0A, 0x4D, + 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x72, 0x33, 0x55, 0x77, 0x4D, 0x36, 0x71, 0x37, 0x61, 0x39, 0x4F, 0x5A, 0x4C, 0x42, 0x49, 0x33, 0x68, 0x4E, 0x6D, 0x4E, 0x65, 0x35, 0x65, 0x41, 0x30, 0x32, 0x37, 0x6E, 0x2F, + 0x35, 0x74, 0x51, 0x6C, 0x54, 0x36, 0x51, 0x6C, 0x56, 0x5A, 0x43, 0x31, 0x78, 0x6C, 0x38, 0x4A, 0x6F, 0x53, 0x4E, 0x6B, 0x76, 0x6F, 0x42, 0x48, 0x54, 0x6F, 0x50, 0x34, 0x6D, 0x51, 0x34, 0x74, 0x34, 0x79, 0x38, 0x0A, 0x36, 0x49, 0x6A, 0x35, + 0x69, 0x79, 0x53, 0x72, 0x4C, 0x71, 0x50, 0x31, 0x4E, 0x2B, 0x52, 0x41, 0x6A, 0x68, 0x67, 0x6C, 0x65, 0x59, 0x4E, 0x31, 0x48, 0x7A, 0x76, 0x2F, 0x62, 0x4B, 0x6A, 0x46, 0x78, 0x6C, 0x62, 0x34, 0x74, 0x4F, 0x32, 0x4B, 0x52, 0x4B, 0x4F, 0x72, + 0x62, 0x45, 0x7A, 0x38, 0x48, 0x64, 0x44, 0x63, 0x37, 0x32, 0x69, 0x39, 0x7A, 0x2B, 0x53, 0x71, 0x7A, 0x76, 0x42, 0x56, 0x39, 0x36, 0x49, 0x30, 0x31, 0x49, 0x4E, 0x72, 0x4E, 0x33, 0x77, 0x63, 0x0A, 0x77, 0x76, 0x36, 0x31, 0x41, 0x2B, 0x78, + 0x58, 0x7A, 0x72, 0x79, 0x30, 0x74, 0x63, 0x58, 0x74, 0x41, 0x41, 0x39, 0x54, 0x4E, 0x79, 0x70, 0x4E, 0x39, 0x45, 0x38, 0x4D, 0x67, 0x2F, 0x75, 0x47, 0x7A, 0x38, 0x76, 0x2B, 0x6A, 0x45, 0x36, 0x39, 0x68, 0x2F, 0x6D, 0x6E, 0x69, 0x79, 0x46, + 0x58, 0x6E, 0x48, 0x72, 0x66, 0x41, 0x32, 0x65, 0x4A, 0x4C, 0x4A, 0x32, 0x58, 0x59, 0x61, 0x63, 0x51, 0x75, 0x46, 0x57, 0x51, 0x66, 0x77, 0x34, 0x74, 0x4A, 0x7A, 0x68, 0x30, 0x0A, 0x33, 0x2B, 0x66, 0x39, 0x32, 0x6B, 0x34, 0x53, 0x34, 0x30, + 0x30, 0x56, 0x49, 0x67, 0x4C, 0x49, 0x34, 0x4F, 0x44, 0x38, 0x44, 0x36, 0x32, 0x4B, 0x31, 0x38, 0x6C, 0x55, 0x55, 0x4D, 0x77, 0x37, 0x44, 0x38, 0x6F, 0x57, 0x67, 0x49, 0x54, 0x51, 0x55, 0x56, 0x62, 0x44, 0x6A, 0x6C, 0x5A, 0x2F, 0x69, 0x53, + 0x49, 0x7A, 0x4C, 0x2B, 0x61, 0x46, 0x43, 0x72, 0x32, 0x6C, 0x71, 0x42, 0x73, 0x32, 0x33, 0x74, 0x50, 0x63, 0x4C, 0x47, 0x30, 0x37, 0x78, 0x78, 0x4F, 0x39, 0x0A, 0x57, 0x53, 0x4D, 0x73, 0x35, 0x75, 0x57, 0x6B, 0x39, 0x39, 0x67, 0x4C, 0x37, + 0x65, 0x71, 0x51, 0x51, 0x45, 0x53, 0x6F, 0x6C, 0x62, 0x75, 0x54, 0x31, 0x64, 0x43, 0x41, 0x4E, 0x4C, 0x5A, 0x47, 0x65, 0x41, 0x34, 0x66, 0x41, 0x4A, 0x4E, 0x47, 0x34, 0x65, 0x37, 0x70, 0x2B, 0x65, 0x78, 0x50, 0x46, 0x77, 0x49, 0x44, 0x41, + 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x0A, 0x5A, 0x54, 0x2F, 0x48, 0x69, 0x6F, 0x62, 0x47, 0x50, 0x4E, 0x30, 0x38, 0x56, 0x46, 0x77, 0x31, + 0x2B, 0x44, 0x72, 0x74, 0x55, 0x67, 0x78, 0x48, 0x56, 0x38, 0x67, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, + 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x0A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, + 0x42, 0x41, 0x43, 0x6F, 0x2F, 0x34, 0x66, 0x45, 0x79, 0x6A, 0x71, 0x37, 0x68, 0x6D, 0x46, 0x78, 0x4C, 0x58, 0x73, 0x39, 0x72, 0x48, 0x6D, 0x6F, 0x4A, 0x30, 0x69, 0x4B, 0x70, 0x45, 0x73, 0x64, 0x65, 0x56, 0x33, 0x31, 0x7A, 0x56, 0x6D, 0x53, + 0x41, 0x68, 0x48, 0x71, 0x54, 0x35, 0x41, 0x6D, 0x35, 0x45, 0x4D, 0x32, 0x66, 0x4B, 0x69, 0x66, 0x68, 0x0A, 0x41, 0x48, 0x65, 0x2B, 0x53, 0x4D, 0x67, 0x31, 0x71, 0x49, 0x47, 0x66, 0x35, 0x4C, 0x67, 0x73, 0x79, 0x58, 0x38, 0x4F, 0x73, 0x4E, + 0x4A, 0x4C, 0x4E, 0x31, 0x33, 0x71, 0x75, 0x64, 0x55, 0x4C, 0x58, 0x6A, 0x53, 0x39, 0x39, 0x48, 0x4D, 0x70, 0x77, 0x2B, 0x30, 0x6D, 0x46, 0x5A, 0x78, 0x2B, 0x43, 0x46, 0x4F, 0x4B, 0x57, 0x49, 0x33, 0x51, 0x53, 0x79, 0x6A, 0x66, 0x77, 0x62, + 0x50, 0x66, 0x49, 0x50, 0x50, 0x35, 0x34, 0x2B, 0x4D, 0x36, 0x33, 0x38, 0x79, 0x63, 0x0A, 0x6C, 0x4E, 0x68, 0x4F, 0x54, 0x38, 0x4E, 0x72, 0x46, 0x37, 0x66, 0x33, 0x63, 0x75, 0x69, 0x74, 0x5A, 0x6A, 0x4F, 0x31, 0x4A, 0x56, 0x4F, 0x72, 0x34, + 0x50, 0x68, 0x4D, 0x71, 0x5A, 0x33, 0x39, 0x38, 0x67, 0x32, 0x36, 0x72, 0x72, 0x6E, 0x5A, 0x71, 0x73, 0x5A, 0x72, 0x2B, 0x5A, 0x4F, 0x37, 0x72, 0x71, 0x75, 0x34, 0x6C, 0x7A, 0x77, 0x44, 0x47, 0x72, 0x70, 0x44, 0x78, 0x70, 0x61, 0x35, 0x52, + 0x58, 0x49, 0x34, 0x73, 0x36, 0x65, 0x68, 0x6C, 0x6A, 0x32, 0x52, 0x0A, 0x65, 0x33, 0x37, 0x41, 0x49, 0x56, 0x4E, 0x4D, 0x68, 0x2B, 0x33, 0x79, 0x43, 0x31, 0x53, 0x56, 0x55, 0x5A, 0x50, 0x56, 0x49, 0x71, 0x55, 0x4E, 0x69, 0x76, 0x47, 0x54, + 0x44, 0x6A, 0x35, 0x55, 0x44, 0x72, 0x44, 0x59, 0x79, 0x55, 0x37, 0x63, 0x38, 0x6A, 0x45, 0x79, 0x56, 0x75, 0x70, 0x6B, 0x2B, 0x65, 0x71, 0x31, 0x6E, 0x52, 0x5A, 0x6D, 0x51, 0x6E, 0x4C, 0x7A, 0x66, 0x39, 0x4F, 0x78, 0x4D, 0x55, 0x50, 0x38, + 0x70, 0x49, 0x34, 0x58, 0x38, 0x57, 0x30, 0x6A, 0x0A, 0x71, 0x35, 0x52, 0x6D, 0x2B, 0x4B, 0x33, 0x37, 0x44, 0x77, 0x68, 0x75, 0x4A, 0x69, 0x31, 0x2F, 0x46, 0x77, 0x63, 0x4A, 0x73, 0x6F, 0x7A, 0x37, 0x55, 0x4D, 0x43, 0x66, 0x6C, 0x6F, 0x33, + 0x50, 0x74, 0x76, 0x30, 0x41, 0x6E, 0x56, 0x6F, 0x55, 0x6D, 0x72, 0x38, 0x43, 0x52, 0x50, 0x58, 0x42, 0x77, 0x70, 0x38, 0x69, 0x58, 0x71, 0x49, 0x50, 0x6F, 0x65, 0x4D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x44, 0x43, 0x41, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x41, 0x55, 0x54, 0x48, 0x20, 0x52, 0x35, 0x20, 0x52, 0x4F, 0x4F, 0x54, 0x0A, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x69, 0x44, 0x43, 0x43, 0x41, 0x33, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x66, 0x51, 0x6D, 0x58, 0x2F, 0x76, 0x42, 0x48, 0x36, + 0x6E, 0x6F, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x59, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, + 0x30, 0x34, 0x78, 0x4D, 0x6A, 0x41, 0x77, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x4B, 0x55, 0x64, 0x56, 0x51, 0x55, 0x35, 0x48, 0x49, 0x45, 0x52, 0x50, 0x54, 0x6B, 0x63, 0x67, 0x51, 0x30, 0x56, 0x53, 0x56, 0x45, 0x6C, 0x47, + 0x53, 0x55, 0x4E, 0x42, 0x56, 0x45, 0x55, 0x67, 0x51, 0x56, 0x56, 0x55, 0x53, 0x45, 0x39, 0x53, 0x53, 0x56, 0x52, 0x5A, 0x49, 0x45, 0x4E, 0x50, 0x4C, 0x69, 0x78, 0x4D, 0x56, 0x45, 0x51, 0x75, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x44, 0x0A, 0x44, 0x42, 0x5A, 0x48, 0x52, 0x45, 0x4E, 0x42, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x56, 0x56, 0x55, 0x53, 0x43, 0x42, 0x53, 0x4E, 0x53, 0x42, 0x53, 0x54, 0x30, 0x39, 0x55, 0x4D, 0x42, 0x34, + 0x58, 0x44, 0x54, 0x45, 0x30, 0x4D, 0x54, 0x45, 0x79, 0x4E, 0x6A, 0x41, 0x31, 0x4D, 0x54, 0x4D, 0x78, 0x4E, 0x56, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x54, 0x49, 0x7A, 0x4D, 0x54, 0x45, 0x31, 0x4E, 0x54, 0x6B, 0x31, 0x4F, 0x56, 0x6F, + 0x77, 0x0A, 0x59, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x4D, 0x6A, 0x41, 0x77, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x4B, 0x55, 0x64, 0x56, 0x51, 0x55, + 0x35, 0x48, 0x49, 0x45, 0x52, 0x50, 0x54, 0x6B, 0x63, 0x67, 0x51, 0x30, 0x56, 0x53, 0x56, 0x45, 0x6C, 0x47, 0x53, 0x55, 0x4E, 0x42, 0x56, 0x45, 0x55, 0x67, 0x51, 0x56, 0x56, 0x55, 0x53, 0x45, 0x39, 0x53, 0x53, 0x56, 0x52, 0x5A, 0x0A, 0x49, + 0x45, 0x4E, 0x50, 0x4C, 0x69, 0x78, 0x4D, 0x56, 0x45, 0x51, 0x75, 0x4D, 0x52, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x5A, 0x48, 0x52, 0x45, 0x4E, 0x42, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, + 0x56, 0x56, 0x55, 0x53, 0x43, 0x42, 0x53, 0x4E, 0x53, 0x42, 0x53, 0x54, 0x30, 0x39, 0x55, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x0A, 0x41, 0x51, 0x45, 0x46, + 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x32, 0x61, 0x4D, 0x57, 0x38, 0x4D, 0x68, 0x30, 0x64, 0x48, 0x65, 0x62, 0x37, 0x7A, 0x4D, 0x4E, 0x4F, 0x77, 0x5A, 0x2B, + 0x56, 0x66, 0x79, 0x31, 0x59, 0x49, 0x39, 0x32, 0x68, 0x68, 0x4A, 0x43, 0x66, 0x56, 0x5A, 0x6D, 0x50, 0x6F, 0x69, 0x43, 0x37, 0x58, 0x4A, 0x6A, 0x44, 0x70, 0x36, 0x4C, 0x33, 0x54, 0x51, 0x73, 0x0A, 0x41, 0x6C, 0x46, 0x52, 0x77, 0x78, 0x6E, + 0x39, 0x57, 0x56, 0x53, 0x45, 0x79, 0x66, 0x46, 0x72, 0x73, 0x30, 0x79, 0x77, 0x36, 0x65, 0x68, 0x47, 0x58, 0x54, 0x6A, 0x47, 0x6F, 0x71, 0x63, 0x75, 0x45, 0x56, 0x65, 0x36, 0x67, 0x68, 0x57, 0x69, 0x6E, 0x49, 0x39, 0x74, 0x73, 0x4A, 0x6C, + 0x4B, 0x43, 0x76, 0x4C, 0x72, 0x69, 0x58, 0x42, 0x6A, 0x54, 0x6E, 0x6E, 0x45, 0x74, 0x31, 0x75, 0x39, 0x6F, 0x6C, 0x32, 0x78, 0x38, 0x6B, 0x45, 0x43, 0x4B, 0x36, 0x32, 0x70, 0x0A, 0x4F, 0x71, 0x50, 0x73, 0x65, 0x51, 0x72, 0x73, 0x58, 0x7A, + 0x72, 0x6A, 0x2F, 0x65, 0x2B, 0x41, 0x50, 0x4B, 0x30, 0x30, 0x6D, 0x78, 0x71, 0x72, 0x69, 0x43, 0x5A, 0x37, 0x56, 0x71, 0x4B, 0x43, 0x68, 0x68, 0x2F, 0x72, 0x4E, 0x59, 0x6D, 0x44, 0x66, 0x31, 0x2B, 0x75, 0x4B, 0x55, 0x34, 0x39, 0x74, 0x6D, + 0x37, 0x73, 0x72, 0x73, 0x48, 0x77, 0x4A, 0x35, 0x75, 0x75, 0x34, 0x2F, 0x54, 0x73, 0x37, 0x36, 0x35, 0x2F, 0x39, 0x34, 0x59, 0x39, 0x63, 0x6E, 0x72, 0x72, 0x0A, 0x70, 0x66, 0x74, 0x5A, 0x54, 0x71, 0x66, 0x72, 0x6C, 0x59, 0x77, 0x69, 0x4F, + 0x58, 0x6E, 0x68, 0x4C, 0x51, 0x69, 0x50, 0x7A, 0x4C, 0x79, 0x52, 0x75, 0x45, 0x48, 0x33, 0x46, 0x4D, 0x45, 0x6A, 0x71, 0x63, 0x4F, 0x74, 0x6D, 0x6B, 0x56, 0x45, 0x73, 0x37, 0x4C, 0x58, 0x4C, 0x4D, 0x33, 0x47, 0x4B, 0x65, 0x4A, 0x51, 0x45, + 0x4B, 0x35, 0x63, 0x79, 0x34, 0x4B, 0x4F, 0x46, 0x78, 0x67, 0x32, 0x66, 0x5A, 0x66, 0x6D, 0x69, 0x4A, 0x71, 0x77, 0x54, 0x54, 0x51, 0x4A, 0x0A, 0x39, 0x43, 0x79, 0x35, 0x57, 0x6D, 0x59, 0x71, 0x73, 0x42, 0x65, 0x62, 0x6E, 0x68, 0x35, 0x32, + 0x6E, 0x55, 0x70, 0x6D, 0x4D, 0x55, 0x48, 0x66, 0x50, 0x2F, 0x76, 0x46, 0x42, 0x75, 0x38, 0x62, 0x74, 0x6E, 0x34, 0x61, 0x52, 0x6A, 0x62, 0x33, 0x5A, 0x47, 0x4D, 0x37, 0x34, 0x7A, 0x6B, 0x59, 0x49, 0x2B, 0x64, 0x6E, 0x64, 0x52, 0x54, 0x56, + 0x64, 0x56, 0x65, 0x53, 0x4E, 0x37, 0x32, 0x2B, 0x61, 0x68, 0x73, 0x6D, 0x55, 0x50, 0x49, 0x32, 0x4A, 0x67, 0x61, 0x51, 0x0A, 0x78, 0x58, 0x41, 0x42, 0x5A, 0x47, 0x31, 0x32, 0x5A, 0x75, 0x47, 0x52, 0x32, 0x32, 0x34, 0x48, 0x77, 0x47, 0x47, + 0x41, 0x4C, 0x72, 0x49, 0x75, 0x4C, 0x34, 0x78, 0x77, 0x70, 0x39, 0x45, 0x37, 0x50, 0x4C, 0x4F, 0x52, 0x35, 0x47, 0x36, 0x32, 0x78, 0x44, 0x74, 0x77, 0x38, 0x6D, 0x79, 0x53, 0x6C, 0x77, 0x6E, 0x4E, 0x52, 0x33, 0x30, 0x59, 0x77, 0x50, 0x4F, + 0x37, 0x6E, 0x67, 0x2F, 0x57, 0x69, 0x36, 0x34, 0x48, 0x74, 0x6C, 0x6F, 0x50, 0x7A, 0x67, 0x73, 0x4D, 0x0A, 0x52, 0x36, 0x66, 0x6C, 0x50, 0x72, 0x69, 0x39, 0x66, 0x63, 0x65, 0x62, 0x4E, 0x61, 0x42, 0x68, 0x6C, 0x7A, 0x70, 0x42, 0x64, 0x52, + 0x66, 0x4D, 0x4B, 0x35, 0x5A, 0x33, 0x4B, 0x70, 0x49, 0x68, 0x48, 0x74, 0x6D, 0x56, 0x64, 0x69, 0x42, 0x6E, 0x61, 0x4D, 0x38, 0x4E, 0x76, 0x64, 0x2F, 0x57, 0x48, 0x77, 0x6C, 0x71, 0x6D, 0x75, 0x4C, 0x4D, 0x63, 0x33, 0x47, 0x6B, 0x4C, 0x33, + 0x30, 0x53, 0x67, 0x4C, 0x64, 0x54, 0x4D, 0x45, 0x5A, 0x65, 0x53, 0x31, 0x53, 0x5A, 0x0A, 0x44, 0x32, 0x66, 0x4A, 0x70, 0x63, 0x6A, 0x79, 0x49, 0x4D, 0x47, 0x43, 0x37, 0x4A, 0x30, 0x52, 0x33, 0x38, 0x49, 0x43, 0x2B, 0x78, 0x6F, 0x37, 0x30, + 0x65, 0x30, 0x67, 0x6D, 0x75, 0x39, 0x6C, 0x5A, 0x4A, 0x49, 0x51, 0x44, 0x53, 0x72, 0x69, 0x33, 0x6E, 0x44, 0x78, 0x47, 0x47, 0x65, 0x43, 0x6A, 0x47, 0x48, 0x65, 0x75, 0x4C, 0x7A, 0x52, 0x4C, 0x35, 0x7A, 0x37, 0x44, 0x39, 0x41, 0x72, 0x37, + 0x52, 0x74, 0x32, 0x75, 0x65, 0x51, 0x35, 0x56, 0x66, 0x6A, 0x34, 0x0A, 0x6F, 0x52, 0x32, 0x34, 0x71, 0x6F, 0x41, 0x41, 0x54, 0x49, 0x4C, 0x6E, 0x73, 0x6E, 0x38, 0x4A, 0x75, 0x4C, 0x77, 0x77, 0x6F, 0x43, 0x38, 0x4E, 0x39, 0x56, 0x4B, 0x65, + 0x6A, 0x76, 0x65, 0x53, 0x73, 0x77, 0x6F, 0x41, 0x48, 0x51, 0x42, 0x55, 0x6C, 0x77, 0x62, 0x67, 0x73, 0x51, 0x66, 0x5A, 0x78, 0x77, 0x39, 0x63, 0x5A, 0x58, 0x30, 0x38, 0x62, 0x56, 0x6C, 0x58, 0x35, 0x4F, 0x32, 0x6C, 0x6A, 0x65, 0x6C, 0x41, + 0x55, 0x35, 0x38, 0x56, 0x53, 0x36, 0x42, 0x78, 0x0A, 0x39, 0x68, 0x6F, 0x68, 0x34, 0x39, 0x70, 0x77, 0x42, 0x69, 0x46, 0x59, 0x46, 0x49, 0x65, 0x46, 0x64, 0x33, 0x6D, 0x71, 0x67, 0x6E, 0x6B, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, + 0x43, 0x4D, 0x45, 0x41, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4F, 0x4C, 0x4A, 0x51, 0x4A, 0x39, 0x4E, 0x7A, 0x75, 0x69, 0x61, 0x6F, 0x58, 0x7A, 0x50, 0x44, 0x6A, 0x39, 0x6C, 0x78, 0x53, 0x6D, + 0x49, 0x61, 0x68, 0x6C, 0x52, 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, + 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x44, 0x52, 0x53, 0x56, + 0x66, 0x67, 0x0A, 0x70, 0x38, 0x78, 0x6F, 0x57, 0x4C, 0x6F, 0x42, 0x44, 0x79, 0x73, 0x5A, 0x7A, 0x59, 0x32, 0x77, 0x59, 0x55, 0x57, 0x73, 0x45, 0x65, 0x31, 0x6A, 0x55, 0x47, 0x6E, 0x34, 0x48, 0x33, 0x2B, 0x2B, 0x46, 0x6F, 0x2F, 0x39, 0x6E, + 0x65, 0x73, 0x4C, 0x71, 0x6A, 0x4A, 0x48, 0x64, 0x74, 0x4A, 0x6E, 0x4A, 0x4F, 0x32, 0x39, 0x66, 0x44, 0x4D, 0x79, 0x6C, 0x79, 0x72, 0x48, 0x42, 0x59, 0x5A, 0x6D, 0x44, 0x52, 0x64, 0x39, 0x46, 0x42, 0x55, 0x62, 0x31, 0x4F, 0x76, 0x39, 0x0A, + 0x48, 0x35, 0x72, 0x32, 0x58, 0x70, 0x64, 0x70, 0x74, 0x78, 0x6F, 0x6C, 0x70, 0x41, 0x71, 0x7A, 0x6B, 0x54, 0x39, 0x66, 0x4E, 0x71, 0x79, 0x4C, 0x37, 0x46, 0x65, 0x6F, 0x50, 0x75, 0x65, 0x42, 0x69, 0x68, 0x68, 0x58, 0x4F, 0x59, 0x56, 0x30, + 0x47, 0x6B, 0x4C, 0x48, 0x36, 0x56, 0x73, 0x54, 0x58, 0x34, 0x2F, 0x35, 0x43, 0x4F, 0x6D, 0x53, 0x64, 0x49, 0x33, 0x31, 0x52, 0x39, 0x4B, 0x72, 0x4F, 0x39, 0x62, 0x37, 0x65, 0x47, 0x5A, 0x4F, 0x4E, 0x6E, 0x33, 0x35, 0x0A, 0x36, 0x5A, 0x4C, + 0x70, 0x42, 0x4E, 0x37, 0x39, 0x53, 0x57, 0x50, 0x38, 0x62, 0x66, 0x73, 0x55, 0x63, 0x5A, 0x4E, 0x6E, 0x4C, 0x30, 0x64, 0x4B, 0x74, 0x37, 0x6E, 0x2F, 0x48, 0x69, 0x70, 0x7A, 0x63, 0x45, 0x59, 0x77, 0x76, 0x31, 0x72, 0x79, 0x4C, 0x33, 0x6D, + 0x6C, 0x34, 0x59, 0x30, 0x4D, 0x32, 0x66, 0x6D, 0x79, 0x59, 0x7A, 0x65, 0x4D, 0x4E, 0x32, 0x57, 0x46, 0x63, 0x47, 0x70, 0x63, 0x57, 0x77, 0x6C, 0x79, 0x75, 0x61, 0x31, 0x6A, 0x50, 0x4C, 0x48, 0x64, 0x0A, 0x2B, 0x50, 0x77, 0x79, 0x76, 0x7A, + 0x65, 0x47, 0x35, 0x4C, 0x75, 0x4F, 0x6D, 0x43, 0x64, 0x2B, 0x75, 0x68, 0x38, 0x57, 0x34, 0x58, 0x41, 0x52, 0x38, 0x67, 0x50, 0x66, 0x4A, 0x57, 0x49, 0x79, 0x4A, 0x79, 0x59, 0x59, 0x4D, 0x6F, 0x53, 0x66, 0x2F, 0x77, 0x41, 0x36, 0x45, 0x37, + 0x71, 0x61, 0x54, 0x66, 0x52, 0x50, 0x75, 0x42, 0x52, 0x77, 0x49, 0x72, 0x48, 0x4B, 0x4B, 0x35, 0x44, 0x4F, 0x4B, 0x63, 0x46, 0x77, 0x39, 0x43, 0x2B, 0x64, 0x66, 0x2F, 0x4B, 0x51, 0x0A, 0x48, 0x74, 0x5A, 0x61, 0x33, 0x37, 0x64, 0x47, 0x2F, + 0x4F, 0x61, 0x47, 0x2B, 0x73, 0x76, 0x67, 0x49, 0x48, 0x5A, 0x36, 0x75, 0x71, 0x62, 0x4C, 0x39, 0x58, 0x7A, 0x65, 0x59, 0x71, 0x57, 0x78, 0x69, 0x2B, 0x37, 0x65, 0x67, 0x6D, 0x61, 0x4B, 0x54, 0x6A, 0x6F, 0x77, 0x48, 0x7A, 0x2B, 0x41, 0x79, + 0x36, 0x30, 0x6E, 0x75, 0x67, 0x78, 0x65, 0x31, 0x39, 0x43, 0x78, 0x56, 0x73, 0x70, 0x33, 0x63, 0x62, 0x4B, 0x31, 0x64, 0x61, 0x46, 0x51, 0x71, 0x55, 0x42, 0x44, 0x0A, 0x46, 0x38, 0x49, 0x6F, 0x32, 0x63, 0x39, 0x53, 0x69, 0x31, 0x76, 0x49, + 0x59, 0x39, 0x52, 0x43, 0x50, 0x71, 0x41, 0x7A, 0x65, 0x6B, 0x59, 0x75, 0x39, 0x77, 0x6F, 0x67, 0x52, 0x6C, 0x52, 0x2B, 0x61, 0x6B, 0x38, 0x78, 0x38, 0x59, 0x46, 0x2B, 0x51, 0x6E, 0x51, 0x34, 0x5A, 0x58, 0x4D, 0x6E, 0x37, 0x73, 0x5A, 0x38, + 0x75, 0x49, 0x37, 0x58, 0x70, 0x54, 0x72, 0x58, 0x6D, 0x4B, 0x47, 0x63, 0x6A, 0x42, 0x42, 0x56, 0x30, 0x39, 0x74, 0x4C, 0x37, 0x45, 0x43, 0x51, 0x0A, 0x38, 0x73, 0x31, 0x75, 0x56, 0x39, 0x4A, 0x69, 0x44, 0x6E, 0x78, 0x58, 0x6B, 0x37, 0x47, + 0x6E, 0x62, 0x63, 0x32, 0x64, 0x67, 0x37, 0x73, 0x71, 0x35, 0x2B, 0x57, 0x32, 0x4F, 0x33, 0x46, 0x59, 0x72, 0x66, 0x33, 0x52, 0x52, 0x62, 0x78, 0x61, 0x6B, 0x65, 0x35, 0x54, 0x46, 0x57, 0x2F, 0x54, 0x52, 0x51, 0x6C, 0x31, 0x62, 0x72, 0x71, + 0x51, 0x58, 0x52, 0x34, 0x45, 0x7A, 0x7A, 0x66, 0x66, 0x48, 0x71, 0x68, 0x6D, 0x73, 0x59, 0x7A, 0x6D, 0x49, 0x47, 0x72, 0x76, 0x0A, 0x2F, 0x45, 0x68, 0x4F, 0x64, 0x4A, 0x68, 0x43, 0x72, 0x79, 0x6C, 0x76, 0x4C, 0x6D, 0x72, 0x48, 0x2B, 0x33, + 0x33, 0x52, 0x5A, 0x6A, 0x45, 0x69, 0x7A, 0x49, 0x59, 0x41, 0x66, 0x6D, 0x61, 0x44, 0x44, 0x45, 0x4C, 0x30, 0x76, 0x54, 0x53, 0x53, 0x77, 0x78, 0x72, 0x71, 0x54, 0x38, 0x70, 0x2B, 0x63, 0x6B, 0x30, 0x4C, 0x63, 0x49, 0x79, 0x6D, 0x53, 0x4C, + 0x75, 0x6D, 0x6F, 0x52, 0x54, 0x32, 0x2B, 0x31, 0x68, 0x45, 0x6D, 0x52, 0x53, 0x75, 0x71, 0x67, 0x75, 0x54, 0x0A, 0x61, 0x61, 0x41, 0x70, 0x4A, 0x55, 0x71, 0x6C, 0x79, 0x79, 0x76, 0x64, 0x69, 0x6D, 0x59, 0x48, 0x46, 0x6E, 0x67, 0x56, 0x56, + 0x33, 0x45, 0x62, 0x37, 0x50, 0x56, 0x48, 0x68, 0x50, 0x4F, 0x65, 0x4D, 0x54, 0x64, 0x36, 0x31, 0x58, 0x38, 0x6B, 0x72, 0x65, 0x53, 0x38, 0x2F, 0x66, 0x33, 0x4D, 0x62, 0x6F, 0x50, 0x6F, 0x44, 0x4B, 0x69, 0x33, 0x51, 0x57, 0x77, 0x48, 0x33, + 0x62, 0x30, 0x38, 0x68, 0x70, 0x63, 0x76, 0x30, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, + 0x53, 0x4C, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x52, 0x53, 0x41, 0x0A, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x33, 0x54, 0x43, 0x43, 0x41, 0x38, 0x57, + 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x65, 0x79, 0x79, 0x62, 0x30, 0x78, 0x61, 0x41, 0x4D, 0x70, 0x6B, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, + 0x77, 0x66, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x44, 0x6A, 0x41, 0x4D, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x4D, 0x42, 0x56, 0x52, 0x6C, 0x65, 0x47, + 0x46, 0x7A, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, 0x64, 0x49, 0x62, 0x33, 0x56, 0x7A, 0x64, 0x47, 0x39, 0x75, 0x4D, 0x52, 0x67, 0x77, 0x46, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, + 0x39, 0x54, 0x55, 0x30, 0x77, 0x67, 0x51, 0x32, 0x39, 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x78, 0x0A, 0x4D, 0x54, 0x41, 0x76, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x4B, 0x46, 0x4E, 0x54, 0x54, + 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, + 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x53, 0x55, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x0A, 0x4D, 0x6A, 0x45, 0x79, 0x4D, 0x54, 0x63, 0x7A, 0x4F, 0x54, 0x4D, 0x35, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x45, 0x77, + 0x4D, 0x6A, 0x45, 0x79, 0x4D, 0x54, 0x63, 0x7A, 0x4F, 0x54, 0x4D, 0x35, 0x57, 0x6A, 0x42, 0x38, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x4F, 0x4D, 0x41, 0x77, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x43, 0x41, 0x77, 0x46, 0x56, 0x47, 0x56, 0x34, 0x59, 0x58, 0x4D, 0x78, 0x0A, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x42, 0x30, 0x68, 0x76, 0x64, 0x58, 0x4E, 0x30, 0x62, 0x32, 0x34, + 0x78, 0x47, 0x44, 0x41, 0x57, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, 0x31, 0x4E, 0x54, 0x54, 0x43, 0x42, 0x44, 0x62, 0x33, 0x4A, 0x77, 0x62, 0x33, 0x4A, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6A, 0x45, 0x78, 0x4D, 0x43, 0x38, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x6F, 0x55, 0x31, 0x4E, 0x4D, 0x0A, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, + 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x46, 0x4A, 0x54, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, + 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x0A, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x50, 0x6B, 0x50, 0x33, 0x61, 0x4D, 0x72, 0x66, 0x63, 0x76, 0x51, 0x4B, + 0x76, 0x37, 0x73, 0x5A, 0x34, 0x57, 0x6D, 0x35, 0x79, 0x34, 0x62, 0x75, 0x6E, 0x66, 0x68, 0x34, 0x2F, 0x57, 0x76, 0x70, 0x4F, 0x7A, 0x36, 0x53, 0x6C, 0x32, 0x52, 0x78, 0x46, 0x64, 0x48, 0x61, 0x78, 0x68, 0x33, 0x61, 0x33, 0x62, 0x79, 0x2F, + 0x5A, 0x50, 0x6B, 0x50, 0x51, 0x2F, 0x43, 0x0A, 0x46, 0x70, 0x34, 0x4C, 0x5A, 0x73, 0x4E, 0x57, 0x6C, 0x4A, 0x34, 0x58, 0x67, 0x34, 0x58, 0x4F, 0x56, 0x75, 0x2F, 0x79, 0x46, 0x76, 0x30, 0x41, 0x59, 0x76, 0x55, 0x69, 0x43, 0x56, 0x54, 0x6F, + 0x5A, 0x52, 0x64, 0x4F, 0x51, 0x62, 0x6E, 0x67, 0x54, 0x30, 0x61, 0x58, 0x71, 0x68, 0x76, 0x49, 0x75, 0x47, 0x35, 0x69, 0x58, 0x6D, 0x6D, 0x78, 0x58, 0x39, 0x73, 0x71, 0x41, 0x6E, 0x37, 0x38, 0x62, 0x4D, 0x72, 0x7A, 0x51, 0x64, 0x6A, 0x74, + 0x30, 0x4F, 0x6A, 0x38, 0x0A, 0x50, 0x32, 0x46, 0x49, 0x37, 0x62, 0x41, 0x44, 0x46, 0x42, 0x30, 0x51, 0x44, 0x6B, 0x73, 0x5A, 0x34, 0x4C, 0x74, 0x4F, 0x37, 0x49, 0x5A, 0x6C, 0x2F, 0x7A, 0x62, 0x7A, 0x58, 0x6D, 0x63, 0x43, 0x43, 0x35, 0x32, + 0x47, 0x56, 0x57, 0x48, 0x39, 0x65, 0x6A, 0x6A, 0x74, 0x2F, 0x75, 0x49, 0x5A, 0x41, 0x4C, 0x64, 0x76, 0x6F, 0x56, 0x42, 0x69, 0x64, 0x58, 0x51, 0x38, 0x6F, 0x50, 0x72, 0x49, 0x4A, 0x5A, 0x4B, 0x30, 0x62, 0x6E, 0x6F, 0x69, 0x78, 0x2F, 0x67, + 0x65, 0x0A, 0x6F, 0x65, 0x4F, 0x79, 0x33, 0x5A, 0x45, 0x78, 0x71, 0x79, 0x73, 0x64, 0x42, 0x50, 0x2B, 0x6C, 0x53, 0x67, 0x51, 0x33, 0x36, 0x59, 0x57, 0x6B, 0x4D, 0x79, 0x76, 0x39, 0x34, 0x74, 0x5A, 0x56, 0x4E, 0x48, 0x77, 0x5A, 0x70, 0x45, + 0x70, 0x6F, 0x78, 0x37, 0x4B, 0x6F, 0x30, 0x37, 0x66, 0x4B, 0x6F, 0x5A, 0x4F, 0x49, 0x36, 0x38, 0x47, 0x58, 0x76, 0x49, 0x7A, 0x35, 0x48, 0x64, 0x6B, 0x69, 0x68, 0x43, 0x52, 0x30, 0x78, 0x77, 0x51, 0x39, 0x61, 0x71, 0x6B, 0x70, 0x0A, 0x6B, + 0x38, 0x7A, 0x72, 0x75, 0x46, 0x76, 0x68, 0x2F, 0x6C, 0x38, 0x6C, 0x71, 0x6A, 0x52, 0x59, 0x79, 0x4D, 0x45, 0x6A, 0x56, 0x4A, 0x30, 0x62, 0x6D, 0x42, 0x48, 0x44, 0x4F, 0x4A, 0x78, 0x2B, 0x50, 0x59, 0x5A, 0x73, 0x70, 0x51, 0x39, 0x41, 0x68, + 0x6E, 0x77, 0x43, 0x39, 0x46, 0x77, 0x43, 0x54, 0x79, 0x6A, 0x4C, 0x72, 0x6E, 0x47, 0x66, 0x44, 0x7A, 0x72, 0x49, 0x4D, 0x2F, 0x34, 0x52, 0x4A, 0x54, 0x58, 0x71, 0x2F, 0x4C, 0x72, 0x46, 0x59, 0x44, 0x33, 0x5A, 0x0A, 0x66, 0x42, 0x6A, 0x56, + 0x73, 0x71, 0x6E, 0x54, 0x64, 0x58, 0x67, 0x44, 0x63, 0x69, 0x4C, 0x4B, 0x4F, 0x73, 0x4D, 0x66, 0x37, 0x79, 0x7A, 0x6C, 0x4C, 0x71, 0x6E, 0x36, 0x6E, 0x69, 0x79, 0x32, 0x55, 0x55, 0x62, 0x39, 0x72, 0x77, 0x50, 0x57, 0x36, 0x6D, 0x42, 0x6F, + 0x36, 0x6F, 0x55, 0x57, 0x4E, 0x6D, 0x75, 0x46, 0x36, 0x52, 0x37, 0x41, 0x73, 0x39, 0x33, 0x45, 0x4A, 0x4E, 0x79, 0x41, 0x4B, 0x6F, 0x46, 0x42, 0x62, 0x5A, 0x51, 0x2B, 0x79, 0x4F, 0x44, 0x4A, 0x0A, 0x67, 0x55, 0x45, 0x41, 0x6E, 0x6C, 0x36, + 0x2F, 0x66, 0x38, 0x55, 0x49, 0x6D, 0x4B, 0x49, 0x59, 0x4C, 0x45, 0x4A, 0x41, 0x73, 0x2F, 0x6C, 0x76, 0x4F, 0x43, 0x64, 0x4C, 0x54, 0x6F, 0x44, 0x30, 0x50, 0x59, 0x46, 0x48, 0x34, 0x49, 0x68, 0x38, 0x36, 0x68, 0x7A, 0x4F, 0x74, 0x58, 0x56, + 0x63, 0x55, 0x53, 0x34, 0x63, 0x4B, 0x33, 0x38, 0x61, 0x63, 0x69, 0x6A, 0x6E, 0x41, 0x4C, 0x58, 0x52, 0x64, 0x4D, 0x62, 0x58, 0x35, 0x4A, 0x2B, 0x74, 0x42, 0x35, 0x4F, 0x32, 0x0A, 0x55, 0x7A, 0x55, 0x31, 0x2F, 0x44, 0x66, 0x6B, 0x77, 0x2F, + 0x5A, 0x64, 0x46, 0x72, 0x34, 0x68, 0x63, 0x39, 0x36, 0x53, 0x43, 0x76, 0x69, 0x67, 0x59, 0x32, 0x71, 0x38, 0x6C, 0x70, 0x4A, 0x71, 0x50, 0x76, 0x69, 0x38, 0x5A, 0x56, 0x57, 0x62, 0x33, 0x76, 0x55, 0x4E, 0x69, 0x53, 0x59, 0x45, 0x2F, 0x43, + 0x55, 0x61, 0x70, 0x69, 0x56, 0x70, 0x79, 0x38, 0x4A, 0x74, 0x79, 0x6E, 0x7A, 0x69, 0x57, 0x56, 0x2B, 0x58, 0x72, 0x4F, 0x76, 0x76, 0x4C, 0x73, 0x69, 0x38, 0x0A, 0x31, 0x78, 0x74, 0x5A, 0x50, 0x43, 0x76, 0x4D, 0x38, 0x68, 0x6E, 0x49, 0x6B, + 0x32, 0x73, 0x6E, 0x59, 0x78, 0x6E, 0x50, 0x2F, 0x4F, 0x6B, 0x6D, 0x2B, 0x4D, 0x70, 0x78, 0x6D, 0x33, 0x2B, 0x54, 0x2F, 0x6A, 0x52, 0x6E, 0x68, 0x45, 0x36, 0x5A, 0x36, 0x2F, 0x79, 0x7A, 0x65, 0x41, 0x6B, 0x7A, 0x63, 0x4C, 0x70, 0x6D, 0x70, + 0x6E, 0x62, 0x74, 0x47, 0x33, 0x50, 0x72, 0x47, 0x71, 0x55, 0x4E, 0x78, 0x43, 0x49, 0x54, 0x49, 0x4A, 0x52, 0x57, 0x43, 0x6B, 0x34, 0x73, 0x0A, 0x62, 0x45, 0x36, 0x78, 0x2F, 0x63, 0x2B, 0x63, 0x43, 0x62, 0x71, 0x69, 0x4D, 0x2B, 0x32, 0x48, + 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x64, 0x42, 0x41, 0x6B, 0x48, 0x6F, 0x76, 0x56, 0x36, 0x66, 0x56, 0x4A, 0x54, + 0x45, 0x70, 0x4B, 0x56, 0x37, 0x6A, 0x69, 0x41, 0x4A, 0x51, 0x32, 0x6D, 0x57, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x38, + 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x4E, 0x30, 0x45, 0x43, 0x51, 0x65, 0x69, 0x39, 0x58, 0x70, 0x39, 0x55, 0x6C, 0x4D, 0x53, 0x6B, 0x70, 0x58, 0x75, 0x4F, 0x49, 0x41, 0x6C, 0x44, 0x61, 0x5A, + 0x5A, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x0A, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, + 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x49, 0x42, 0x67, 0x52, 0x6C, 0x43, 0x6E, 0x37, 0x4A, 0x70, 0x30, 0x63, 0x48, 0x68, 0x35, 0x77, 0x59, 0x66, 0x47, 0x56, 0x63, 0x70, 0x4E, 0x78, 0x4A, 0x4B, 0x31, 0x6F, 0x6B, 0x31, + 0x69, 0x4F, 0x4D, 0x71, 0x38, 0x62, 0x73, 0x33, 0x41, 0x44, 0x2F, 0x43, 0x55, 0x72, 0x0A, 0x64, 0x49, 0x57, 0x51, 0x50, 0x58, 0x68, 0x71, 0x39, 0x4C, 0x6D, 0x4C, 0x70, 0x5A, 0x63, 0x37, 0x74, 0x52, 0x69, 0x52, 0x75, 0x78, 0x36, 0x6E, 0x2B, + 0x55, 0x42, 0x62, 0x6B, 0x66, 0x6C, 0x56, 0x6D, 0x61, 0x38, 0x65, 0x45, 0x64, 0x42, 0x63, 0x48, 0x61, 0x64, 0x6D, 0x34, 0x37, 0x47, 0x55, 0x42, 0x77, 0x77, 0x79, 0x4F, 0x61, 0x62, 0x71, 0x47, 0x37, 0x42, 0x35, 0x32, 0x42, 0x32, 0x63, 0x63, + 0x45, 0x54, 0x6A, 0x69, 0x74, 0x33, 0x45, 0x2B, 0x5A, 0x55, 0x66, 0x0A, 0x69, 0x6A, 0x68, 0x44, 0x50, 0x77, 0x47, 0x46, 0x70, 0x55, 0x65, 0x6E, 0x50, 0x55, 0x61, 0x79, 0x76, 0x4F, 0x55, 0x69, 0x61, 0x50, 0x64, 0x37, 0x6E, 0x4E, 0x67, 0x73, + 0x50, 0x67, 0x6F, 0x68, 0x79, 0x43, 0x30, 0x7A, 0x72, 0x4C, 0x2F, 0x46, 0x67, 0x5A, 0x6B, 0x78, 0x64, 0x4D, 0x46, 0x31, 0x63, 0x63, 0x57, 0x2B, 0x73, 0x66, 0x41, 0x6A, 0x52, 0x66, 0x53, 0x64, 0x61, 0x2F, 0x77, 0x5A, 0x59, 0x35, 0x32, 0x6A, + 0x76, 0x41, 0x54, 0x47, 0x47, 0x41, 0x73, 0x6C, 0x0A, 0x75, 0x31, 0x4F, 0x4A, 0x44, 0x37, 0x4F, 0x41, 0x55, 0x4E, 0x35, 0x46, 0x37, 0x6B, 0x52, 0x2F, 0x71, 0x35, 0x52, 0x34, 0x5A, 0x4A, 0x6A, 0x54, 0x39, 0x69, 0x6A, 0x64, 0x68, 0x39, 0x68, + 0x77, 0x5A, 0x58, 0x54, 0x37, 0x44, 0x72, 0x6B, 0x54, 0x36, 0x36, 0x63, 0x50, 0x59, 0x61, 0x6B, 0x79, 0x6C, 0x73, 0x7A, 0x65, 0x75, 0x2B, 0x31, 0x6A, 0x54, 0x42, 0x69, 0x37, 0x71, 0x55, 0x44, 0x33, 0x6F, 0x46, 0x52, 0x75, 0x49, 0x49, 0x68, + 0x78, 0x64, 0x52, 0x6A, 0x71, 0x0A, 0x65, 0x72, 0x51, 0x30, 0x63, 0x75, 0x41, 0x6A, 0x4A, 0x33, 0x64, 0x63, 0x74, 0x70, 0x44, 0x71, 0x68, 0x69, 0x56, 0x41, 0x71, 0x2B, 0x38, 0x7A, 0x44, 0x38, 0x75, 0x66, 0x67, 0x72, 0x36, 0x69, 0x49, 0x50, + 0x76, 0x32, 0x74, 0x53, 0x30, 0x61, 0x35, 0x73, 0x4B, 0x46, 0x73, 0x58, 0x51, 0x50, 0x2B, 0x38, 0x68, 0x6C, 0x41, 0x71, 0x52, 0x53, 0x41, 0x55, 0x66, 0x64, 0x53, 0x53, 0x4C, 0x42, 0x76, 0x39, 0x6A, 0x72, 0x61, 0x36, 0x78, 0x2B, 0x33, 0x75, + 0x78, 0x6A, 0x0A, 0x4D, 0x78, 0x57, 0x33, 0x49, 0x77, 0x69, 0x50, 0x78, 0x67, 0x2B, 0x4E, 0x51, 0x56, 0x72, 0x64, 0x6A, 0x73, 0x57, 0x35, 0x6A, 0x2B, 0x56, 0x46, 0x50, 0x33, 0x6A, 0x62, 0x75, 0x74, 0x49, 0x62, 0x51, 0x4C, 0x48, 0x2B, 0x63, + 0x55, 0x30, 0x2F, 0x34, 0x49, 0x47, 0x69, 0x75, 0x6C, 0x36, 0x30, 0x37, 0x42, 0x58, 0x67, 0x6B, 0x39, 0x30, 0x49, 0x48, 0x33, 0x37, 0x68, 0x56, 0x5A, 0x6B, 0x4C, 0x49, 0x64, 0x36, 0x54, 0x6E, 0x67, 0x72, 0x37, 0x35, 0x71, 0x4E, 0x4A, 0x0A, + 0x76, 0x54, 0x59, 0x77, 0x2F, 0x75, 0x64, 0x33, 0x73, 0x71, 0x42, 0x31, 0x6C, 0x37, 0x55, 0x74, 0x67, 0x59, 0x67, 0x58, 0x5A, 0x53, 0x44, 0x33, 0x32, 0x70, 0x41, 0x41, 0x6E, 0x38, 0x6C, 0x53, 0x7A, 0x44, 0x4C, 0x4B, 0x4E, 0x58, 0x7A, 0x31, + 0x50, 0x51, 0x2F, 0x59, 0x4B, 0x39, 0x66, 0x31, 0x4A, 0x6D, 0x7A, 0x4A, 0x42, 0x6A, 0x53, 0x57, 0x46, 0x75, 0x70, 0x77, 0x57, 0x52, 0x6F, 0x79, 0x65, 0x58, 0x6B, 0x4C, 0x74, 0x6F, 0x68, 0x2F, 0x44, 0x31, 0x4A, 0x49, 0x0A, 0x50, 0x62, 0x39, + 0x73, 0x32, 0x4B, 0x4A, 0x45, 0x4C, 0x74, 0x46, 0x4F, 0x74, 0x33, 0x4A, 0x59, 0x30, 0x34, 0x6B, 0x54, 0x6C, 0x66, 0x35, 0x45, 0x71, 0x2F, 0x6A, 0x58, 0x69, 0x78, 0x74, 0x75, 0x6E, 0x4C, 0x77, 0x73, 0x6F, 0x46, 0x76, 0x56, 0x61, 0x67, 0x43, + 0x76, 0x58, 0x7A, 0x66, 0x68, 0x31, 0x66, 0x6F, 0x51, 0x43, 0x35, 0x69, 0x63, 0x68, 0x75, 0x63, 0x6D, 0x6A, 0x38, 0x37, 0x77, 0x37, 0x47, 0x36, 0x4B, 0x56, 0x77, 0x75, 0x41, 0x34, 0x30, 0x36, 0x79, 0x0A, 0x77, 0x4B, 0x42, 0x6A, 0x59, 0x5A, + 0x43, 0x36, 0x56, 0x57, 0x67, 0x33, 0x64, 0x47, 0x71, 0x32, 0x6B, 0x74, 0x75, 0x66, 0x6F, 0x59, 0x59, 0x69, 0x74, 0x6D, 0x55, 0x6E, 0x44, 0x75, 0x79, 0x32, 0x6E, 0x30, 0x4A, 0x67, 0x35, 0x47, 0x66, 0x43, 0x74, 0x64, 0x70, 0x42, 0x43, 0x38, + 0x54, 0x54, 0x69, 0x32, 0x45, 0x62, 0x76, 0x50, 0x6F, 0x66, 0x6B, 0x53, 0x76, 0x58, 0x52, 0x41, 0x64, 0x65, 0x75, 0x69, 0x6D, 0x73, 0x32, 0x63, 0x58, 0x70, 0x37, 0x31, 0x4E, 0x49, 0x0A, 0x57, 0x75, 0x75, 0x41, 0x38, 0x53, 0x68, 0x59, 0x49, + 0x63, 0x32, 0x77, 0x42, 0x6C, 0x58, 0x37, 0x4A, 0x7A, 0x39, 0x54, 0x6B, 0x48, 0x43, 0x70, 0x42, 0x42, 0x35, 0x58, 0x4A, 0x37, 0x6B, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x53, 0x4C, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, + 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x45, 0x43, 0x43, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x6A, 0x54, 0x43, 0x43, 0x41, 0x68, 0x53, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x64, 0x65, 0x62, 0x66, 0x79, 0x38, 0x46, 0x6F, 0x57, 0x36, 0x67, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, + 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x49, 0x77, 0x66, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x44, 0x6A, 0x41, 0x4D, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, + 0x67, 0x4D, 0x42, 0x56, 0x52, 0x6C, 0x65, 0x47, 0x46, 0x7A, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, 0x64, 0x49, 0x62, 0x33, 0x56, 0x7A, 0x64, 0x47, 0x39, 0x75, 0x4D, 0x52, 0x67, 0x77, 0x46, 0x67, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x39, 0x54, 0x55, 0x30, 0x77, 0x67, 0x51, 0x32, 0x39, 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x78, 0x4D, 0x54, 0x41, 0x76, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x4D, 0x4D, 0x4B, 0x46, 0x4E, 0x54, 0x54, 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, + 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x4D, 0x6A, 0x45, 0x79, 0x0A, 0x4D, 0x54, 0x67, 0x78, 0x4E, 0x44, 0x41, 0x7A, + 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x45, 0x77, 0x4D, 0x6A, 0x45, 0x79, 0x4D, 0x54, 0x67, 0x78, 0x4E, 0x44, 0x41, 0x7A, 0x57, 0x6A, 0x42, 0x38, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, + 0x55, 0x7A, 0x45, 0x4F, 0x4D, 0x41, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x41, 0x77, 0x46, 0x56, 0x47, 0x56, 0x34, 0x59, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x42, 0x30, 0x68, + 0x76, 0x64, 0x58, 0x4E, 0x30, 0x62, 0x32, 0x34, 0x78, 0x47, 0x44, 0x41, 0x57, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, 0x31, 0x4E, 0x54, 0x54, 0x43, 0x42, 0x44, 0x62, 0x33, 0x4A, 0x77, 0x62, 0x33, 0x4A, 0x68, 0x64, 0x47, 0x6C, + 0x76, 0x62, 0x6A, 0x45, 0x78, 0x4D, 0x43, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x6F, 0x55, 0x31, 0x4E, 0x4D, 0x4C, 0x6D, 0x4E, 0x76, 0x0A, 0x62, 0x53, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x6C, 0x63, 0x6E, + 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x45, 0x56, 0x44, 0x51, 0x7A, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, + 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x0A, 0x42, 0x45, 0x56, 0x75, 0x71, 0x56, 0x44, 0x45, 0x70, 0x69, 0x4D, 0x32, 0x6E, 0x6C, 0x38, 0x6F, 0x6A, + 0x52, 0x66, 0x4C, 0x6C, 0x69, 0x4A, 0x6B, 0x50, 0x39, 0x78, 0x36, 0x6A, 0x68, 0x33, 0x4D, 0x43, 0x4C, 0x4F, 0x69, 0x63, 0x53, 0x53, 0x36, 0x6A, 0x6B, 0x6D, 0x35, 0x42, 0x42, 0x74, 0x48, 0x6C, 0x6C, 0x69, 0x72, 0x4C, 0x5A, 0x58, 0x49, 0x37, + 0x5A, 0x34, 0x49, 0x4E, 0x63, 0x67, 0x6E, 0x36, 0x34, 0x6D, 0x4D, 0x55, 0x31, 0x6A, 0x72, 0x59, 0x6F, 0x72, 0x2B, 0x0A, 0x38, 0x46, 0x73, 0x50, 0x61, 0x7A, 0x46, 0x53, 0x59, 0x30, 0x45, 0x37, 0x69, 0x63, 0x33, 0x73, 0x37, 0x4C, 0x61, 0x4E, + 0x47, 0x64, 0x4D, 0x30, 0x42, 0x39, 0x79, 0x37, 0x78, 0x67, 0x5A, 0x2F, 0x77, 0x6B, 0x57, 0x56, 0x37, 0x4D, 0x74, 0x2F, 0x71, 0x43, 0x50, 0x67, 0x43, 0x65, 0x6D, 0x42, 0x2B, 0x76, 0x4E, 0x48, 0x30, 0x36, 0x4E, 0x6A, 0x4D, 0x47, 0x45, 0x77, + 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x49, 0x4C, 0x52, 0x0A, 0x68, 0x58, 0x4D, 0x77, 0x35, 0x7A, 0x55, 0x45, 0x30, 0x34, 0x34, 0x43, 0x6B, 0x76, 0x76, 0x6C, 0x70, 0x4E, 0x48, 0x45, 0x49, 0x65, 0x6A, + 0x4E, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, + 0x55, 0x67, 0x74, 0x47, 0x46, 0x63, 0x7A, 0x44, 0x6E, 0x4E, 0x51, 0x54, 0x54, 0x0A, 0x6A, 0x67, 0x4B, 0x53, 0x2B, 0x2B, 0x57, 0x6B, 0x30, 0x63, 0x51, 0x68, 0x36, 0x4D, 0x30, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, + 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x32, 0x63, 0x41, 0x4D, 0x47, 0x51, 0x43, 0x4D, 0x47, 0x2F, 0x6E, 0x36, 0x31, + 0x6B, 0x52, 0x70, 0x47, 0x44, 0x50, 0x59, 0x62, 0x43, 0x57, 0x0A, 0x65, 0x2B, 0x30, 0x46, 0x2B, 0x53, 0x38, 0x54, 0x6B, 0x64, 0x7A, 0x74, 0x35, 0x66, 0x78, 0x51, 0x61, 0x78, 0x46, 0x47, 0x52, 0x72, 0x4D, 0x63, 0x49, 0x51, 0x42, 0x69, 0x75, + 0x37, 0x37, 0x44, 0x35, 0x2B, 0x6A, 0x4E, 0x42, 0x35, 0x6E, 0x35, 0x44, 0x51, 0x74, 0x64, 0x63, 0x6A, 0x37, 0x45, 0x71, 0x67, 0x49, 0x77, 0x48, 0x37, 0x79, 0x36, 0x43, 0x2B, 0x49, 0x77, 0x4A, 0x50, 0x74, 0x38, 0x62, 0x59, 0x42, 0x56, 0x43, + 0x70, 0x6B, 0x2B, 0x67, 0x41, 0x30, 0x7A, 0x0A, 0x35, 0x57, 0x61, 0x6A, 0x73, 0x36, 0x4F, 0x37, 0x70, 0x64, 0x57, 0x4C, 0x6A, 0x77, 0x6B, 0x73, 0x70, 0x6C, 0x31, 0x2B, 0x34, 0x76, 0x41, 0x48, 0x43, 0x47, 0x68, 0x74, 0x30, 0x6E, 0x78, 0x70, + 0x62, 0x6C, 0x2F, 0x66, 0x35, 0x57, 0x70, 0x6C, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x53, 0x4C, 0x2E, + 0x63, 0x6F, 0x6D, 0x20, 0x45, 0x56, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x52, 0x53, 0x41, 0x20, + 0x52, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, + 0x49, 0x46, 0x36, 0x7A, 0x43, 0x43, 0x41, 0x39, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x56, 0x72, 0x59, 0x70, 0x7A, 0x54, 0x53, 0x38, 0x65, 0x50, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, + 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x67, 0x59, 0x49, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x51, 0x34, 0x77, 0x0A, 0x44, 0x41, 0x59, 0x44, 0x56, + 0x51, 0x51, 0x49, 0x44, 0x41, 0x56, 0x55, 0x5A, 0x58, 0x68, 0x68, 0x63, 0x7A, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x77, 0x77, 0x48, 0x53, 0x47, 0x39, 0x31, 0x63, 0x33, 0x52, 0x76, 0x62, 0x6A, 0x45, 0x59, 0x4D, + 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x50, 0x55, 0x31, 0x4E, 0x4D, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, 0x4D, 0x54, 0x63, 0x77, 0x4E, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x35, 0x54, 0x55, 0x30, 0x77, 0x75, 0x59, 0x32, 0x39, 0x74, 0x49, 0x45, 0x56, 0x57, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, + 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x49, 0x79, 0x0A, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x33, 0x4D, 0x44, 0x55, + 0x7A, 0x4D, 0x54, 0x45, 0x34, 0x4D, 0x54, 0x51, 0x7A, 0x4E, 0x31, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x79, 0x4D, 0x44, 0x55, 0x7A, 0x4D, 0x44, 0x45, 0x34, 0x4D, 0x54, 0x51, 0x7A, 0x4E, 0x31, 0x6F, 0x77, 0x67, 0x59, 0x49, 0x78, 0x43, 0x7A, 0x41, + 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x51, 0x34, 0x77, 0x44, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x49, 0x0A, 0x44, 0x41, 0x56, 0x55, 0x5A, 0x58, 0x68, 0x68, 0x63, 0x7A, 0x45, 0x51, 0x4D, 0x41, + 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x77, 0x77, 0x48, 0x53, 0x47, 0x39, 0x31, 0x63, 0x33, 0x52, 0x76, 0x62, 0x6A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x50, 0x55, 0x31, 0x4E, 0x4D, 0x49, 0x45, + 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, 0x54, 0x63, 0x77, 0x4E, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x35, 0x54, 0x55, 0x30, 0x77, 0x75, 0x59, 0x32, 0x39, 0x74, 0x49, + 0x45, 0x56, 0x57, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, + 0x48, 0x6B, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x49, 0x79, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x0A, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, + 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x6A, 0x7A, 0x5A, 0x6C, 0x51, 0x4F, 0x48, 0x57, 0x54, 0x63, 0x44, 0x58, 0x74, 0x4F, 0x6C, 0x47, 0x32, 0x6D, 0x76, 0x71, 0x4D, 0x30, 0x66, 0x4E, + 0x54, 0x50, 0x6C, 0x39, 0x66, 0x62, 0x36, 0x39, 0x4C, 0x54, 0x33, 0x77, 0x32, 0x33, 0x6A, 0x68, 0x0A, 0x68, 0x71, 0x58, 0x5A, 0x75, 0x67, 0x6C, 0x58, 0x61, 0x4F, 0x31, 0x58, 0x50, 0x71, 0x44, 0x51, 0x43, 0x45, 0x47, 0x44, 0x35, 0x79, 0x68, + 0x42, 0x4A, 0x42, 0x2F, 0x6A, 0x63, 0x68, 0x58, 0x51, 0x41, 0x52, 0x72, 0x37, 0x58, 0x6E, 0x41, 0x6A, 0x73, 0x73, 0x75, 0x66, 0x4F, 0x65, 0x50, 0x50, 0x78, 0x55, 0x37, 0x47, 0x6B, 0x6D, 0x30, 0x6D, 0x78, 0x6E, 0x75, 0x37, 0x73, 0x39, 0x6F, + 0x6E, 0x6E, 0x51, 0x71, 0x47, 0x36, 0x59, 0x45, 0x33, 0x42, 0x66, 0x37, 0x77, 0x0A, 0x63, 0x58, 0x48, 0x73, 0x77, 0x78, 0x7A, 0x70, 0x59, 0x36, 0x49, 0x58, 0x46, 0x4A, 0x33, 0x76, 0x47, 0x32, 0x66, 0x54, 0x68, 0x56, 0x55, 0x43, 0x41, 0x74, + 0x5A, 0x4A, 0x79, 0x63, 0x78, 0x61, 0x34, 0x62, 0x48, 0x33, 0x62, 0x7A, 0x4B, 0x66, 0x79, 0x64, 0x51, 0x37, 0x69, 0x45, 0x47, 0x6F, 0x6E, 0x4C, 0x33, 0x4C, 0x71, 0x39, 0x74, 0x74, 0x65, 0x77, 0x6B, 0x66, 0x6F, 0x6B, 0x78, 0x79, 0x6B, 0x4E, + 0x6F, 0x72, 0x43, 0x50, 0x7A, 0x50, 0x50, 0x46, 0x54, 0x4F, 0x0A, 0x5A, 0x77, 0x2B, 0x6F, 0x7A, 0x31, 0x32, 0x57, 0x47, 0x51, 0x76, 0x45, 0x34, 0x33, 0x4C, 0x72, 0x72, 0x64, 0x46, 0x39, 0x48, 0x53, 0x66, 0x76, 0x6B, 0x75, 0x73, 0x51, 0x76, + 0x31, 0x76, 0x72, 0x4F, 0x36, 0x2F, 0x50, 0x67, 0x4E, 0x33, 0x42, 0x30, 0x70, 0x59, 0x45, 0x57, 0x33, 0x70, 0x2B, 0x70, 0x4B, 0x6B, 0x38, 0x4F, 0x48, 0x61, 0x6B, 0x59, 0x6F, 0x36, 0x67, 0x4F, 0x56, 0x37, 0x71, 0x64, 0x38, 0x39, 0x64, 0x41, + 0x46, 0x6D, 0x50, 0x5A, 0x69, 0x77, 0x2B, 0x0A, 0x42, 0x36, 0x4B, 0x6A, 0x42, 0x53, 0x59, 0x52, 0x61, 0x5A, 0x66, 0x71, 0x68, 0x62, 0x63, 0x50, 0x6C, 0x67, 0x74, 0x4C, 0x79, 0x45, 0x44, 0x68, 0x55, 0x4C, 0x6F, 0x75, 0x69, 0x73, 0x76, 0x33, + 0x44, 0x35, 0x6F, 0x69, 0x35, 0x33, 0x2B, 0x61, 0x4E, 0x78, 0x50, 0x4E, 0x38, 0x6B, 0x30, 0x54, 0x61, 0x79, 0x48, 0x52, 0x77, 0x4D, 0x77, 0x69, 0x38, 0x71, 0x46, 0x47, 0x39, 0x6B, 0x52, 0x70, 0x6E, 0x4D, 0x70, 0x68, 0x4E, 0x51, 0x63, 0x41, + 0x62, 0x39, 0x5A, 0x68, 0x0A, 0x43, 0x42, 0x48, 0x71, 0x75, 0x72, 0x6A, 0x32, 0x36, 0x62, 0x4E, 0x67, 0x35, 0x55, 0x32, 0x35, 0x37, 0x4A, 0x38, 0x55, 0x5A, 0x73, 0x6C, 0x58, 0x57, 0x4E, 0x76, 0x4E, 0x68, 0x32, 0x6E, 0x34, 0x69, 0x6F, 0x59, + 0x53, 0x41, 0x30, 0x65, 0x2F, 0x5A, 0x68, 0x4E, 0x32, 0x72, 0x48, 0x64, 0x39, 0x4E, 0x43, 0x53, 0x46, 0x67, 0x38, 0x33, 0x58, 0x71, 0x70, 0x79, 0x51, 0x47, 0x70, 0x38, 0x68, 0x4C, 0x48, 0x39, 0x34, 0x74, 0x32, 0x53, 0x34, 0x32, 0x4F, 0x69, + 0x6D, 0x0A, 0x39, 0x48, 0x69, 0x7A, 0x56, 0x63, 0x75, 0x45, 0x30, 0x6A, 0x4C, 0x45, 0x65, 0x4B, 0x36, 0x6A, 0x6A, 0x32, 0x48, 0x64, 0x7A, 0x67, 0x68, 0x54, 0x72, 0x65, 0x79, 0x49, 0x2F, 0x42, 0x58, 0x6B, 0x6D, 0x67, 0x33, 0x6D, 0x6E, 0x78, + 0x70, 0x33, 0x7A, 0x6B, 0x79, 0x50, 0x75, 0x42, 0x51, 0x56, 0x50, 0x57, 0x4B, 0x63, 0x68, 0x6A, 0x67, 0x47, 0x41, 0x47, 0x59, 0x53, 0x35, 0x46, 0x6C, 0x32, 0x57, 0x6C, 0x50, 0x41, 0x41, 0x70, 0x69, 0x69, 0x45, 0x43, 0x74, 0x6F, 0x0A, 0x52, + 0x48, 0x75, 0x4F, 0x65, 0x63, 0x34, 0x7A, 0x53, 0x6E, 0x61, 0x71, 0x57, 0x34, 0x45, 0x57, 0x47, 0x37, 0x57, 0x4B, 0x32, 0x4E, 0x41, 0x41, 0x65, 0x31, 0x35, 0x69, 0x74, 0x41, 0x6E, 0x57, 0x68, 0x6D, 0x4D, 0x4F, 0x70, 0x67, 0x57, 0x56, 0x53, + 0x62, 0x6F, 0x6F, 0x69, 0x34, 0x69, 0x54, 0x73, 0x6A, 0x51, 0x63, 0x32, 0x4B, 0x52, 0x56, 0x62, 0x72, 0x63, 0x63, 0x30, 0x4E, 0x36, 0x5A, 0x56, 0x54, 0x73, 0x6A, 0x39, 0x43, 0x4C, 0x67, 0x2B, 0x53, 0x6C, 0x6D, 0x0A, 0x4A, 0x75, 0x77, 0x67, + 0x55, 0x48, 0x66, 0x62, 0x53, 0x67, 0x75, 0x50, 0x76, 0x75, 0x55, 0x43, 0x59, 0x48, 0x42, 0x42, 0x58, 0x74, 0x53, 0x75, 0x55, 0x44, 0x6B, 0x69, 0x46, 0x43, 0x62, 0x4C, 0x73, 0x6A, 0x74, 0x7A, 0x64, 0x46, 0x56, 0x48, 0x42, 0x33, 0x6D, 0x42, + 0x4F, 0x61, 0x67, 0x77, 0x45, 0x30, 0x54, 0x6C, 0x42, 0x49, 0x71, 0x75, 0x6C, 0x68, 0x4D, 0x6C, 0x51, 0x67, 0x2B, 0x35, 0x55, 0x38, 0x53, 0x62, 0x2F, 0x4D, 0x33, 0x6B, 0x48, 0x4E, 0x34, 0x38, 0x0A, 0x2B, 0x71, 0x76, 0x57, 0x42, 0x6B, 0x6F, + 0x66, 0x5A, 0x36, 0x61, 0x59, 0x4D, 0x42, 0x7A, 0x64, 0x4C, 0x4E, 0x76, 0x63, 0x47, 0x4A, 0x56, 0x58, 0x5A, 0x73, 0x62, 0x2F, 0x58, 0x49, 0x74, 0x57, 0x39, 0x58, 0x63, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x6A, 0x4D, 0x47, 0x45, + 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, + 0x54, 0x35, 0x59, 0x4C, 0x76, 0x55, 0x34, 0x39, 0x55, 0x30, 0x39, 0x72, 0x6A, 0x31, 0x42, 0x6F, 0x41, 0x6C, 0x70, 0x33, 0x50, 0x62, 0x52, 0x6D, 0x6D, 0x6F, 0x6E, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, + 0x51, 0x55, 0x2B, 0x57, 0x43, 0x37, 0x31, 0x4F, 0x50, 0x56, 0x4E, 0x50, 0x61, 0x34, 0x39, 0x51, 0x61, 0x41, 0x4A, 0x61, 0x64, 0x7A, 0x32, 0x30, 0x5A, 0x70, 0x0A, 0x71, 0x4A, 0x34, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, + 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x42, 0x57, 0x73, + 0x34, 0x37, 0x4C, 0x43, 0x70, 0x31, 0x4A, 0x6A, 0x72, 0x2B, 0x6B, 0x78, 0x4A, 0x47, 0x37, 0x5A, 0x68, 0x63, 0x46, 0x55, 0x5A, 0x68, 0x31, 0x0A, 0x2B, 0x2B, 0x56, 0x51, 0x4C, 0x48, 0x71, 0x65, 0x38, 0x52, 0x54, 0x36, 0x71, 0x39, 0x4F, 0x4B, + 0x50, 0x76, 0x2B, 0x52, 0x4B, 0x59, 0x39, 0x6A, 0x69, 0x39, 0x69, 0x30, 0x71, 0x56, 0x51, 0x42, 0x44, 0x62, 0x36, 0x54, 0x68, 0x69, 0x2F, 0x35, 0x53, 0x6D, 0x33, 0x48, 0x58, 0x76, 0x56, 0x58, 0x2B, 0x63, 0x70, 0x56, 0x48, 0x42, 0x4B, 0x2B, + 0x52, 0x77, 0x38, 0x32, 0x78, 0x64, 0x39, 0x71, 0x74, 0x39, 0x74, 0x31, 0x77, 0x6B, 0x63, 0x6C, 0x66, 0x37, 0x6E, 0x78, 0x0A, 0x59, 0x2F, 0x68, 0x6F, 0x4C, 0x56, 0x55, 0x45, 0x30, 0x66, 0x4B, 0x4E, 0x73, 0x4B, 0x54, 0x50, 0x76, 0x44, 0x78, + 0x65, 0x48, 0x33, 0x6A, 0x6E, 0x70, 0x61, 0x41, 0x67, 0x63, 0x4C, 0x41, 0x45, 0x78, 0x62, 0x66, 0x33, 0x63, 0x71, 0x66, 0x65, 0x49, 0x67, 0x32, 0x39, 0x4D, 0x79, 0x56, 0x47, 0x6A, 0x47, 0x53, 0x53, 0x4A, 0x75, 0x4D, 0x2B, 0x4C, 0x6D, 0x4F, + 0x57, 0x32, 0x70, 0x75, 0x4D, 0x50, 0x66, 0x67, 0x59, 0x43, 0x64, 0x63, 0x44, 0x7A, 0x48, 0x32, 0x47, 0x0A, 0x67, 0x75, 0x44, 0x4B, 0x42, 0x41, 0x64, 0x52, 0x55, 0x4E, 0x66, 0x2F, 0x6B, 0x74, 0x55, 0x4D, 0x37, 0x39, 0x71, 0x47, 0x6E, 0x35, + 0x6E, 0x58, 0x36, 0x37, 0x65, 0x76, 0x61, 0x4F, 0x49, 0x35, 0x4A, 0x70, 0x53, 0x36, 0x61, 0x4C, 0x65, 0x2F, 0x67, 0x39, 0x50, 0x71, 0x65, 0x6D, 0x63, 0x39, 0x59, 0x6D, 0x65, 0x75, 0x4A, 0x65, 0x56, 0x79, 0x36, 0x4F, 0x4C, 0x6B, 0x37, 0x4B, + 0x34, 0x53, 0x39, 0x6B, 0x73, 0x72, 0x50, 0x4A, 0x2F, 0x70, 0x73, 0x45, 0x44, 0x7A, 0x0A, 0x4F, 0x46, 0x53, 0x7A, 0x2F, 0x62, 0x64, 0x6F, 0x79, 0x4E, 0x72, 0x47, 0x6A, 0x31, 0x45, 0x38, 0x73, 0x76, 0x75, 0x52, 0x33, 0x42, 0x7A, 0x6E, 0x6D, + 0x35, 0x33, 0x68, 0x74, 0x77, 0x31, 0x79, 0x6A, 0x2B, 0x4B, 0x6B, 0x78, 0x4B, 0x6C, 0x34, 0x2B, 0x65, 0x73, 0x55, 0x72, 0x4D, 0x5A, 0x44, 0x42, 0x63, 0x4A, 0x6C, 0x4F, 0x53, 0x67, 0x59, 0x41, 0x73, 0x4F, 0x43, 0x73, 0x70, 0x30, 0x46, 0x76, + 0x6D, 0x58, 0x74, 0x6C, 0x6C, 0x39, 0x6C, 0x64, 0x44, 0x7A, 0x37, 0x0A, 0x43, 0x54, 0x55, 0x75, 0x65, 0x35, 0x77, 0x54, 0x2F, 0x52, 0x73, 0x50, 0x58, 0x63, 0x64, 0x74, 0x67, 0x54, 0x70, 0x57, 0x44, 0x38, 0x77, 0x37, 0x34, 0x61, 0x38, 0x43, + 0x4C, 0x79, 0x4B, 0x73, 0x52, 0x73, 0x70, 0x47, 0x50, 0x4B, 0x41, 0x63, 0x54, 0x4E, 0x5A, 0x45, 0x74, 0x46, 0x34, 0x75, 0x58, 0x42, 0x56, 0x6D, 0x43, 0x65, 0x45, 0x6D, 0x4B, 0x66, 0x37, 0x47, 0x55, 0x6D, 0x47, 0x36, 0x73, 0x58, 0x50, 0x2F, + 0x77, 0x77, 0x79, 0x63, 0x35, 0x57, 0x78, 0x71, 0x0A, 0x6C, 0x44, 0x38, 0x55, 0x79, 0x6B, 0x41, 0x57, 0x6C, 0x59, 0x54, 0x7A, 0x57, 0x61, 0x6D, 0x73, 0x58, 0x30, 0x78, 0x68, 0x6B, 0x32, 0x33, 0x52, 0x4F, 0x38, 0x79, 0x69, 0x6C, 0x51, 0x77, + 0x69, 0x70, 0x6D, 0x64, 0x6E, 0x52, 0x43, 0x36, 0x35, 0x32, 0x64, 0x4B, 0x4B, 0x51, 0x62, 0x4E, 0x6D, 0x43, 0x31, 0x72, 0x37, 0x66, 0x53, 0x4F, 0x6C, 0x38, 0x68, 0x71, 0x77, 0x2F, 0x39, 0x36, 0x62, 0x67, 0x35, 0x51, 0x75, 0x30, 0x54, 0x2F, + 0x66, 0x6B, 0x72, 0x65, 0x52, 0x0A, 0x72, 0x77, 0x55, 0x37, 0x5A, 0x63, 0x65, 0x67, 0x62, 0x4C, 0x48, 0x4E, 0x59, 0x68, 0x4C, 0x44, 0x6B, 0x42, 0x76, 0x6A, 0x4A, 0x63, 0x34, 0x30, 0x76, 0x47, 0x39, 0x33, 0x64, 0x72, 0x45, 0x51, 0x77, 0x2F, + 0x63, 0x46, 0x47, 0x73, 0x44, 0x57, 0x72, 0x33, 0x52, 0x69, 0x53, 0x42, 0x64, 0x33, 0x6B, 0x6D, 0x6D, 0x51, 0x59, 0x52, 0x7A, 0x65, 0x6C, 0x59, 0x42, 0x30, 0x56, 0x49, 0x38, 0x59, 0x48, 0x4D, 0x50, 0x7A, 0x41, 0x39, 0x43, 0x2F, 0x70, 0x45, + 0x4E, 0x31, 0x0A, 0x68, 0x6C, 0x4D, 0x59, 0x65, 0x67, 0x6F, 0x75, 0x43, 0x52, 0x77, 0x32, 0x6E, 0x35, 0x48, 0x39, 0x67, 0x6F, 0x6F, 0x69, 0x53, 0x39, 0x45, 0x4F, 0x55, 0x43, 0x58, 0x64, 0x79, 0x77, 0x4D, 0x4D, 0x46, 0x38, 0x6D, 0x44, 0x41, + 0x41, 0x68, 0x4F, 0x4E, 0x55, 0x32, 0x4B, 0x69, 0x2B, 0x33, 0x77, 0x41, 0x70, 0x52, 0x6D, 0x4C, 0x45, 0x52, 0x2F, 0x79, 0x35, 0x55, 0x6E, 0x6C, 0x68, 0x65, 0x74, 0x43, 0x54, 0x43, 0x73, 0x74, 0x6E, 0x45, 0x58, 0x62, 0x6F, 0x73, 0x58, 0x0A, + 0x39, 0x68, 0x77, 0x4A, 0x31, 0x43, 0x30, 0x37, 0x6D, 0x4B, 0x56, 0x78, 0x30, 0x31, 0x51, 0x54, 0x32, 0x57, 0x44, 0x7A, 0x39, 0x55, 0x74, 0x6D, 0x54, 0x2F, 0x72, 0x78, 0x37, 0x69, 0x41, 0x53, 0x6A, 0x62, 0x53, 0x73, 0x56, 0x37, 0x46, 0x46, + 0x59, 0x36, 0x47, 0x73, 0x64, 0x71, 0x6E, 0x43, 0x2B, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, + 0x53, 0x53, 0x4C, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x45, 0x56, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, + 0x45, 0x43, 0x43, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, + 0x6C, 0x44, 0x43, 0x43, 0x41, 0x68, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x4C, 0x43, 0x6D, 0x63, 0x57, 0x78, 0x62, 0x74, 0x42, 0x5A, 0x55, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, + 0x41, 0x77, 0x49, 0x77, 0x66, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x44, 0x6A, 0x41, 0x4D, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x67, 0x4D, 0x42, 0x56, 0x52, + 0x6C, 0x65, 0x47, 0x46, 0x7A, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, 0x64, 0x49, 0x62, 0x33, 0x56, 0x7A, 0x64, 0x47, 0x39, 0x75, 0x4D, 0x52, 0x67, 0x77, 0x46, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x4B, 0x44, 0x41, 0x39, 0x54, 0x55, 0x30, 0x77, 0x67, 0x51, 0x32, 0x39, 0x79, 0x63, 0x47, 0x39, 0x79, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x78, 0x4E, 0x44, 0x41, 0x79, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x4B, 0x31, + 0x4E, 0x54, 0x54, 0x43, 0x35, 0x6A, 0x62, 0x32, 0x30, 0x67, 0x52, 0x56, 0x59, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, + 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x0A, 0x4D, 0x6A, 0x45, 0x79, 0x4D, 0x54, 0x67, 0x78, 0x4E, 0x54, 0x49, 0x7A, 0x57, + 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x45, 0x77, 0x4D, 0x6A, 0x45, 0x79, 0x4D, 0x54, 0x67, 0x78, 0x4E, 0x54, 0x49, 0x7A, 0x57, 0x6A, 0x42, 0x2F, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, + 0x7A, 0x45, 0x4F, 0x4D, 0x41, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x41, 0x77, 0x46, 0x56, 0x47, 0x56, 0x34, 0x59, 0x58, 0x4D, 0x78, 0x0A, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x42, 0x30, 0x68, 0x76, + 0x64, 0x58, 0x4E, 0x30, 0x62, 0x32, 0x34, 0x78, 0x47, 0x44, 0x41, 0x57, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, 0x31, 0x4E, 0x54, 0x54, 0x43, 0x42, 0x44, 0x62, 0x33, 0x4A, 0x77, 0x62, 0x33, 0x4A, 0x68, 0x64, 0x47, 0x6C, 0x76, + 0x62, 0x6A, 0x45, 0x30, 0x4D, 0x44, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x72, 0x55, 0x31, 0x4E, 0x4D, 0x0A, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x42, 0x46, 0x56, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, + 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x45, 0x56, 0x44, 0x51, 0x7A, 0x42, 0x32, 0x4D, 0x42, 0x41, + 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x0A, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4B, 0x6F, 0x53, 0x52, 0x35, 0x43, 0x59, 0x47, 0x2F, 0x76, 0x76, 0x77, 0x30, + 0x41, 0x48, 0x67, 0x79, 0x42, 0x4F, 0x38, 0x54, 0x43, 0x43, 0x6F, 0x67, 0x62, 0x52, 0x38, 0x70, 0x4B, 0x47, 0x59, 0x66, 0x4C, 0x32, 0x49, 0x57, 0x6A, 0x4B, 0x41, 0x4D, 0x54, 0x48, 0x36, 0x6B, 0x4D, 0x41, 0x56, 0x49, 0x62, 0x63, 0x2F, 0x52, + 0x2F, 0x66, 0x41, 0x4C, 0x68, 0x42, 0x59, 0x6C, 0x7A, 0x63, 0x63, 0x42, 0x59, 0x79, 0x0A, 0x33, 0x68, 0x2B, 0x5A, 0x31, 0x4D, 0x7A, 0x46, 0x42, 0x38, 0x67, 0x49, 0x48, 0x32, 0x45, 0x57, 0x42, 0x31, 0x45, 0x39, 0x66, 0x56, 0x77, 0x48, 0x55, + 0x2B, 0x4D, 0x31, 0x4F, 0x49, 0x7A, 0x66, 0x7A, 0x5A, 0x2F, 0x5A, 0x4C, 0x67, 0x31, 0x4B, 0x74, 0x68, 0x6B, 0x75, 0x57, 0x6E, 0x42, 0x61, 0x42, 0x75, 0x32, 0x2B, 0x38, 0x4B, 0x47, 0x77, 0x79, 0x74, 0x41, 0x4A, 0x4B, 0x61, 0x4E, 0x6A, 0x4D, + 0x47, 0x45, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x0A, 0x42, 0x42, 0x59, 0x45, 0x46, 0x46, 0x76, 0x4B, 0x58, 0x75, 0x58, 0x65, 0x30, 0x6F, 0x47, 0x71, 0x7A, 0x61, 0x67, 0x74, 0x5A, 0x46, 0x47, 0x32, 0x32, 0x58, 0x4B, 0x62, + 0x6C, 0x2B, 0x5A, 0x50, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, + 0x46, 0x6F, 0x41, 0x55, 0x57, 0x38, 0x70, 0x65, 0x0A, 0x35, 0x64, 0x37, 0x53, 0x67, 0x61, 0x72, 0x4E, 0x71, 0x43, 0x31, 0x6B, 0x55, 0x62, 0x62, 0x5A, 0x63, 0x70, 0x75, 0x58, 0x35, 0x6B, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, + 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x32, 0x67, 0x41, 0x4D, 0x47, 0x55, 0x43, 0x4D, 0x51, 0x43, + 0x4B, 0x35, 0x6B, 0x43, 0x4A, 0x0A, 0x4E, 0x2B, 0x76, 0x70, 0x31, 0x52, 0x50, 0x5A, 0x79, 0x74, 0x52, 0x72, 0x4A, 0x50, 0x4F, 0x77, 0x50, 0x59, 0x64, 0x47, 0x57, 0x42, 0x72, 0x73, 0x73, 0x64, 0x39, 0x76, 0x2B, 0x31, 0x61, 0x36, 0x63, 0x47, + 0x76, 0x48, 0x4F, 0x4D, 0x7A, 0x6F, 0x73, 0x59, 0x78, 0x50, 0x44, 0x2F, 0x66, 0x78, 0x5A, 0x33, 0x59, 0x4F, 0x67, 0x39, 0x41, 0x65, 0x55, 0x59, 0x38, 0x43, 0x4D, 0x44, 0x33, 0x32, 0x49, 0x79, 0x67, 0x6D, 0x54, 0x4D, 0x5A, 0x67, 0x68, 0x35, + 0x4D, 0x6D, 0x0A, 0x6D, 0x37, 0x49, 0x31, 0x48, 0x72, 0x72, 0x57, 0x39, 0x7A, 0x7A, 0x52, 0x48, 0x4D, 0x37, 0x36, 0x4A, 0x54, 0x79, 0x6D, 0x47, 0x6F, 0x45, 0x56, 0x57, 0x2F, 0x4D, 0x53, 0x44, 0x32, 0x7A, 0x75, 0x5A, 0x59, 0x72, 0x4A, 0x68, + 0x36, 0x6A, 0x35, 0x42, 0x2B, 0x42, 0x69, 0x6D, 0x6F, 0x78, 0x63, 0x53, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x0A, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x52, 0x36, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, + 0x49, 0x49, 0x46, 0x67, 0x7A, 0x43, 0x43, 0x41, 0x32, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4F, 0x52, 0x65, 0x61, 0x37, 0x41, 0x34, 0x4D, 0x7A, 0x77, 0x34, 0x56, 0x6C, 0x53, 0x4F, 0x62, 0x2F, 0x52, 0x56, 0x45, 0x77, 0x44, + 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x77, 0x54, 0x44, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x58, 0x0A, 0x52, 0x32, 0x78, 0x76, + 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x55, 0x6A, 0x59, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, + 0x43, 0x6B, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x43, 0x6B, 0x64, 0x73, 0x0A, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, + 0x70, 0x5A, 0x32, 0x34, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x51, 0x78, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x51, 0x78, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x44, 0x41, + 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x4D, 0x4D, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x64, 0x48, 0x62, 0x47, 0x39, 0x69, 0x0A, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, + 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4C, 0x53, 0x42, 0x53, 0x4E, 0x6A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4B, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x55, 0x32, + 0x6C, 0x6E, 0x62, 0x6A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4B, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x0A, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x6A, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, + 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4A, 0x55, 0x48, 0x36, + 0x48, 0x50, 0x4B, 0x5A, 0x76, 0x6E, 0x73, 0x46, 0x4D, 0x70, 0x37, 0x50, 0x50, 0x63, 0x4E, 0x43, 0x50, 0x47, 0x30, 0x52, 0x51, 0x73, 0x73, 0x0A, 0x67, 0x72, 0x52, 0x49, 0x78, 0x75, 0x74, 0x62, 0x50, 0x4B, 0x36, 0x44, 0x75, 0x45, 0x47, 0x53, + 0x4D, 0x78, 0x53, 0x6B, 0x62, 0x33, 0x2F, 0x70, 0x4B, 0x73, 0x7A, 0x47, 0x73, 0x49, 0x68, 0x72, 0x78, 0x62, 0x61, 0x4A, 0x30, 0x63, 0x61, 0x79, 0x2F, 0x78, 0x54, 0x4F, 0x55, 0x52, 0x51, 0x68, 0x37, 0x45, 0x72, 0x64, 0x47, 0x31, 0x72, 0x47, + 0x31, 0x6F, 0x66, 0x75, 0x54, 0x54, 0x6F, 0x56, 0x42, 0x75, 0x31, 0x6B, 0x5A, 0x67, 0x75, 0x53, 0x67, 0x4D, 0x70, 0x45, 0x0A, 0x33, 0x6E, 0x4F, 0x55, 0x54, 0x76, 0x4F, 0x6E, 0x69, 0x58, 0x39, 0x50, 0x65, 0x47, 0x4D, 0x49, 0x79, 0x42, 0x4A, + 0x51, 0x62, 0x55, 0x4A, 0x6D, 0x4C, 0x30, 0x32, 0x35, 0x65, 0x53, 0x68, 0x4E, 0x55, 0x68, 0x71, 0x4B, 0x47, 0x6F, 0x43, 0x33, 0x47, 0x59, 0x45, 0x4F, 0x66, 0x73, 0x53, 0x4B, 0x76, 0x47, 0x52, 0x4D, 0x49, 0x52, 0x78, 0x44, 0x61, 0x4E, 0x63, + 0x39, 0x50, 0x49, 0x72, 0x46, 0x73, 0x6D, 0x62, 0x56, 0x6B, 0x4A, 0x71, 0x33, 0x4D, 0x51, 0x62, 0x46, 0x0A, 0x76, 0x75, 0x4A, 0x74, 0x4D, 0x67, 0x61, 0x6D, 0x48, 0x76, 0x6D, 0x35, 0x36, 0x36, 0x71, 0x6A, 0x75, 0x4C, 0x2B, 0x2B, 0x67, 0x6D, + 0x4E, 0x51, 0x30, 0x50, 0x41, 0x59, 0x69, 0x64, 0x2F, 0x6B, 0x44, 0x33, 0x6E, 0x31, 0x36, 0x71, 0x49, 0x66, 0x4B, 0x74, 0x4A, 0x77, 0x4C, 0x6E, 0x76, 0x6E, 0x76, 0x4A, 0x4F, 0x37, 0x62, 0x56, 0x50, 0x69, 0x53, 0x48, 0x79, 0x4D, 0x45, 0x41, + 0x63, 0x34, 0x2F, 0x32, 0x61, 0x79, 0x64, 0x32, 0x46, 0x2B, 0x34, 0x4F, 0x71, 0x4D, 0x0A, 0x50, 0x4B, 0x71, 0x30, 0x70, 0x50, 0x62, 0x7A, 0x6C, 0x55, 0x6F, 0x53, 0x42, 0x32, 0x33, 0x39, 0x6A, 0x4C, 0x4B, 0x4A, 0x7A, 0x39, 0x43, 0x67, 0x59, + 0x58, 0x66, 0x49, 0x57, 0x48, 0x53, 0x77, 0x31, 0x43, 0x4D, 0x36, 0x39, 0x31, 0x30, 0x36, 0x79, 0x71, 0x4C, 0x62, 0x6E, 0x51, 0x6E, 0x65, 0x58, 0x55, 0x51, 0x74, 0x6B, 0x50, 0x47, 0x42, 0x7A, 0x56, 0x65, 0x53, 0x2B, 0x6E, 0x36, 0x38, 0x55, + 0x41, 0x52, 0x6A, 0x4E, 0x4E, 0x39, 0x72, 0x6B, 0x78, 0x69, 0x2B, 0x0A, 0x61, 0x7A, 0x61, 0x79, 0x4F, 0x65, 0x53, 0x73, 0x4A, 0x44, 0x61, 0x33, 0x38, 0x4F, 0x2B, 0x32, 0x48, 0x42, 0x4E, 0x58, 0x6B, 0x37, 0x62, 0x65, 0x73, 0x76, 0x6A, 0x69, + 0x68, 0x62, 0x64, 0x7A, 0x6F, 0x72, 0x67, 0x31, 0x71, 0x6B, 0x58, 0x79, 0x34, 0x4A, 0x30, 0x32, 0x6F, 0x57, 0x39, 0x55, 0x69, 0x76, 0x46, 0x79, 0x56, 0x6D, 0x34, 0x75, 0x69, 0x4D, 0x56, 0x52, 0x51, 0x6B, 0x51, 0x56, 0x6C, 0x4F, 0x36, 0x6A, + 0x78, 0x54, 0x69, 0x57, 0x6D, 0x30, 0x35, 0x4F, 0x0A, 0x57, 0x67, 0x74, 0x48, 0x38, 0x77, 0x59, 0x32, 0x53, 0x58, 0x63, 0x77, 0x76, 0x48, 0x45, 0x33, 0x35, 0x61, 0x62, 0x73, 0x49, 0x51, 0x68, 0x31, 0x2F, 0x4F, 0x5A, 0x68, 0x46, 0x6A, 0x39, + 0x33, 0x31, 0x64, 0x6D, 0x52, 0x6C, 0x34, 0x51, 0x4B, 0x62, 0x4E, 0x51, 0x43, 0x54, 0x58, 0x54, 0x41, 0x46, 0x4F, 0x33, 0x39, 0x4F, 0x66, 0x75, 0x44, 0x38, 0x6C, 0x34, 0x55, 0x6F, 0x51, 0x53, 0x77, 0x43, 0x2B, 0x6E, 0x2B, 0x37, 0x6F, 0x2F, + 0x68, 0x62, 0x67, 0x75, 0x79, 0x0A, 0x43, 0x4C, 0x4E, 0x68, 0x5A, 0x67, 0x6C, 0x71, 0x73, 0x51, 0x59, 0x36, 0x5A, 0x5A, 0x5A, 0x5A, 0x77, 0x50, 0x41, 0x31, 0x2F, 0x63, 0x6E, 0x61, 0x4B, 0x49, 0x30, 0x61, 0x45, 0x59, 0x64, 0x77, 0x67, 0x51, + 0x71, 0x6F, 0x6D, 0x6E, 0x55, 0x64, 0x6E, 0x6A, 0x71, 0x47, 0x42, 0x51, 0x43, 0x65, 0x32, 0x34, 0x44, 0x57, 0x4A, 0x66, 0x6E, 0x63, 0x42, 0x5A, 0x34, 0x6E, 0x57, 0x55, 0x78, 0x32, 0x4F, 0x56, 0x76, 0x71, 0x2B, 0x61, 0x57, 0x68, 0x32, 0x49, + 0x4D, 0x50, 0x0A, 0x30, 0x66, 0x2F, 0x66, 0x4D, 0x42, 0x48, 0x35, 0x68, 0x63, 0x38, 0x7A, 0x53, 0x50, 0x58, 0x4B, 0x62, 0x57, 0x51, 0x55, 0x4C, 0x48, 0x70, 0x59, 0x54, 0x39, 0x4E, 0x4C, 0x43, 0x45, 0x6E, 0x46, 0x6C, 0x57, 0x51, 0x61, 0x59, + 0x77, 0x35, 0x35, 0x50, 0x66, 0x57, 0x7A, 0x6A, 0x4D, 0x70, 0x59, 0x72, 0x5A, 0x78, 0x43, 0x52, 0x58, 0x6C, 0x75, 0x44, 0x6F, 0x63, 0x5A, 0x58, 0x46, 0x53, 0x78, 0x5A, 0x62, 0x61, 0x2F, 0x6A, 0x4A, 0x76, 0x63, 0x45, 0x2B, 0x6B, 0x4E, 0x0A, + 0x62, 0x37, 0x67, 0x75, 0x33, 0x47, 0x64, 0x75, 0x79, 0x59, 0x73, 0x52, 0x74, 0x59, 0x51, 0x55, 0x69, 0x67, 0x41, 0x5A, 0x63, 0x49, 0x4E, 0x35, 0x6B, 0x5A, 0x65, 0x52, 0x31, 0x42, 0x6F, 0x6E, 0x76, 0x7A, 0x63, 0x65, 0x4D, 0x67, 0x66, 0x59, + 0x46, 0x47, 0x4D, 0x38, 0x4B, 0x45, 0x79, 0x76, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x0A, 0x41, 0x77, 0x49, + 0x42, 0x42, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, + 0x75, 0x62, 0x41, 0x57, 0x6A, 0x6B, 0x78, 0x50, 0x69, 0x6F, 0x75, 0x66, 0x69, 0x31, 0x78, 0x7A, 0x57, 0x78, 0x2F, 0x42, 0x2F, 0x79, 0x47, 0x64, 0x54, 0x6F, 0x44, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, + 0x41, 0x57, 0x67, 0x42, 0x53, 0x75, 0x62, 0x41, 0x57, 0x6A, 0x6B, 0x78, 0x50, 0x69, 0x6F, 0x75, 0x66, 0x69, 0x31, 0x78, 0x7A, 0x57, 0x78, 0x2F, 0x42, 0x2F, 0x79, 0x47, 0x64, 0x54, 0x6F, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, + 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x67, 0x79, 0x58, 0x74, 0x36, 0x4E, 0x48, 0x39, 0x6C, 0x56, 0x4C, 0x4E, 0x0A, 0x6E, 0x73, 0x41, 0x45, 0x6F, 0x4A, 0x46, 0x70, 0x35, + 0x6C, 0x7A, 0x51, 0x68, 0x4E, 0x37, 0x63, 0x72, 0x61, 0x4A, 0x50, 0x36, 0x45, 0x64, 0x34, 0x31, 0x6D, 0x57, 0x59, 0x71, 0x56, 0x75, 0x6F, 0x50, 0x49, 0x64, 0x38, 0x41, 0x6F, 0x72, 0x52, 0x62, 0x72, 0x63, 0x57, 0x63, 0x2B, 0x5A, 0x66, 0x77, + 0x46, 0x53, 0x59, 0x31, 0x58, 0x53, 0x2B, 0x77, 0x63, 0x33, 0x69, 0x45, 0x5A, 0x47, 0x74, 0x49, 0x78, 0x67, 0x39, 0x33, 0x65, 0x46, 0x79, 0x52, 0x4A, 0x61, 0x30, 0x0A, 0x6C, 0x56, 0x37, 0x41, 0x65, 0x34, 0x36, 0x5A, 0x65, 0x42, 0x5A, 0x44, + 0x45, 0x31, 0x5A, 0x58, 0x73, 0x36, 0x4B, 0x7A, 0x4F, 0x37, 0x56, 0x33, 0x33, 0x45, 0x42, 0x79, 0x72, 0x4B, 0x50, 0x72, 0x6D, 0x7A, 0x55, 0x2B, 0x73, 0x51, 0x67, 0x68, 0x6F, 0x65, 0x66, 0x45, 0x51, 0x7A, 0x64, 0x35, 0x4D, 0x72, 0x36, 0x31, + 0x35, 0x35, 0x77, 0x73, 0x54, 0x4C, 0x78, 0x44, 0x4B, 0x5A, 0x6D, 0x4F, 0x4D, 0x4E, 0x4F, 0x73, 0x49, 0x65, 0x44, 0x6A, 0x48, 0x66, 0x72, 0x59, 0x0A, 0x42, 0x7A, 0x4E, 0x32, 0x56, 0x41, 0x41, 0x69, 0x4B, 0x72, 0x6C, 0x4E, 0x49, 0x43, 0x35, + 0x77, 0x61, 0x4E, 0x72, 0x6C, 0x55, 0x2F, 0x79, 0x44, 0x58, 0x4E, 0x4F, 0x64, 0x38, 0x76, 0x39, 0x45, 0x44, 0x45, 0x52, 0x6D, 0x38, 0x74, 0x4C, 0x6A, 0x76, 0x55, 0x59, 0x41, 0x47, 0x6D, 0x30, 0x43, 0x75, 0x69, 0x56, 0x64, 0x6A, 0x61, 0x45, + 0x78, 0x55, 0x64, 0x31, 0x55, 0x52, 0x68, 0x78, 0x4E, 0x32, 0x35, 0x6D, 0x57, 0x37, 0x78, 0x6F, 0x63, 0x42, 0x46, 0x79, 0x6D, 0x0A, 0x46, 0x65, 0x39, 0x34, 0x34, 0x48, 0x6E, 0x2B, 0x58, 0x64, 0x73, 0x2B, 0x71, 0x6B, 0x78, 0x56, 0x2F, 0x5A, + 0x6F, 0x56, 0x71, 0x57, 0x2F, 0x68, 0x70, 0x76, 0x76, 0x66, 0x63, 0x44, 0x44, 0x70, 0x77, 0x2B, 0x35, 0x43, 0x52, 0x75, 0x33, 0x43, 0x6B, 0x77, 0x57, 0x4A, 0x2B, 0x6E, 0x31, 0x6A, 0x65, 0x7A, 0x2F, 0x51, 0x63, 0x59, 0x46, 0x38, 0x41, 0x4F, + 0x69, 0x59, 0x72, 0x67, 0x35, 0x34, 0x4E, 0x4D, 0x4D, 0x6C, 0x2B, 0x36, 0x38, 0x4B, 0x6E, 0x79, 0x42, 0x72, 0x0A, 0x33, 0x54, 0x73, 0x54, 0x6A, 0x78, 0x4B, 0x4D, 0x34, 0x6B, 0x45, 0x61, 0x53, 0x48, 0x70, 0x7A, 0x6F, 0x48, 0x64, 0x70, 0x78, + 0x37, 0x5A, 0x63, 0x66, 0x34, 0x4C, 0x49, 0x48, 0x76, 0x35, 0x59, 0x47, 0x79, 0x67, 0x72, 0x71, 0x47, 0x79, 0x74, 0x58, 0x6D, 0x33, 0x41, 0x42, 0x64, 0x4A, 0x37, 0x74, 0x2B, 0x75, 0x41, 0x2F, 0x69, 0x55, 0x33, 0x2F, 0x67, 0x4B, 0x62, 0x61, + 0x4B, 0x78, 0x43, 0x58, 0x63, 0x50, 0x75, 0x39, 0x63, 0x7A, 0x63, 0x38, 0x46, 0x42, 0x31, 0x0A, 0x30, 0x6A, 0x5A, 0x70, 0x6E, 0x4F, 0x5A, 0x37, 0x42, 0x4E, 0x39, 0x75, 0x42, 0x6D, 0x6D, 0x32, 0x33, 0x67, 0x6F, 0x4A, 0x53, 0x46, 0x6D, 0x48, + 0x36, 0x33, 0x73, 0x55, 0x59, 0x48, 0x70, 0x6B, 0x71, 0x6D, 0x6C, 0x44, 0x37, 0x35, 0x48, 0x48, 0x54, 0x4F, 0x77, 0x59, 0x33, 0x57, 0x7A, 0x76, 0x55, 0x79, 0x32, 0x4D, 0x6D, 0x65, 0x46, 0x65, 0x38, 0x6E, 0x49, 0x2B, 0x7A, 0x31, 0x54, 0x49, + 0x76, 0x57, 0x66, 0x73, 0x70, 0x41, 0x39, 0x4D, 0x52, 0x66, 0x2F, 0x54, 0x0A, 0x75, 0x54, 0x41, 0x6A, 0x42, 0x30, 0x79, 0x50, 0x45, 0x4C, 0x2B, 0x47, 0x6C, 0x74, 0x6D, 0x5A, 0x57, 0x72, 0x53, 0x5A, 0x56, 0x78, 0x79, 0x6B, 0x7A, 0x4C, 0x73, + 0x56, 0x69, 0x56, 0x4F, 0x36, 0x4C, 0x41, 0x55, 0x50, 0x35, 0x4D, 0x53, 0x65, 0x47, 0x62, 0x45, 0x59, 0x4E, 0x4E, 0x56, 0x4D, 0x6E, 0x62, 0x72, 0x74, 0x39, 0x78, 0x2B, 0x76, 0x4A, 0x4A, 0x55, 0x45, 0x65, 0x4B, 0x67, 0x44, 0x75, 0x2B, 0x36, + 0x42, 0x35, 0x64, 0x70, 0x66, 0x66, 0x49, 0x74, 0x4B, 0x0A, 0x6F, 0x5A, 0x42, 0x30, 0x4A, 0x61, 0x65, 0x7A, 0x50, 0x6B, 0x76, 0x49, 0x4C, 0x46, 0x61, 0x39, 0x78, 0x38, 0x6A, 0x76, 0x4F, 0x4F, 0x4A, 0x63, 0x6B, 0x76, 0x42, 0x35, 0x39, 0x35, + 0x79, 0x45, 0x75, 0x6E, 0x51, 0x74, 0x59, 0x51, 0x45, 0x67, 0x66, 0x6E, 0x37, 0x52, 0x38, 0x6B, 0x38, 0x48, 0x57, 0x56, 0x2B, 0x4C, 0x4C, 0x55, 0x4E, 0x53, 0x36, 0x30, 0x59, 0x4D, 0x6C, 0x4F, 0x48, 0x31, 0x5A, 0x6B, 0x64, 0x35, 0x64, 0x39, + 0x56, 0x55, 0x57, 0x78, 0x2B, 0x74, 0x0A, 0x4A, 0x44, 0x66, 0x4C, 0x52, 0x56, 0x70, 0x4F, 0x6F, 0x45, 0x52, 0x49, 0x79, 0x4E, 0x69, 0x77, 0x6D, 0x63, 0x55, 0x56, 0x68, 0x41, 0x6E, 0x32, 0x31, 0x6B, 0x6C, 0x4A, 0x77, 0x47, 0x57, 0x34, 0x35, + 0x68, 0x70, 0x78, 0x62, 0x71, 0x43, 0x6F, 0x38, 0x59, 0x4C, 0x6F, 0x52, 0x54, 0x35, 0x73, 0x31, 0x67, 0x4C, 0x58, 0x43, 0x6D, 0x65, 0x44, 0x42, 0x56, 0x72, 0x4A, 0x70, 0x42, 0x41, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4F, 0x49, 0x53, 0x54, 0x45, 0x20, 0x57, 0x49, 0x53, 0x65, 0x4B, 0x65, 0x79, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, + 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x43, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x61, 0x54, 0x43, 0x43, 0x41, + 0x65, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x49, 0x53, 0x70, 0x57, 0x44, 0x4B, 0x37, 0x61, 0x44, 0x4B, 0x74, 0x41, 0x52, 0x62, 0x38, 0x72, 0x6F, 0x69, 0x30, 0x36, 0x36, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, + 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x74, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x0A, 0x53, 0x44, 0x45, 0x51, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x43, 0x68, 0x4D, 0x48, 0x56, 0x30, 0x6C, 0x54, 0x5A, 0x55, 0x74, 0x6C, 0x65, 0x54, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x5A, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, 0x55, 0x67, 0x52, 0x6D, 0x39, 0x31, + 0x62, 0x6D, 0x52, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x46, 0x62, 0x6D, 0x52, 0x76, 0x63, 0x6E, 0x4E, 0x6C, 0x5A, 0x44, 0x45, 0x6F, 0x0A, 0x4D, 0x43, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x66, 0x54, 0x30, 0x6C, + 0x54, 0x56, 0x45, 0x55, 0x67, 0x56, 0x30, 0x6C, 0x54, 0x5A, 0x55, 0x74, 0x6C, 0x65, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x51, 0x79, 0x42, 0x44, 0x51, 0x54, 0x41, + 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x31, 0x4D, 0x44, 0x6B, 0x77, 0x4F, 0x54, 0x51, 0x34, 0x4D, 0x7A, 0x52, 0x61, 0x0A, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x31, 0x4D, 0x44, 0x6B, 0x77, 0x4F, 0x54, 0x55, 0x34, 0x4D, 0x7A, + 0x4E, 0x61, 0x4D, 0x47, 0x30, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x49, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x64, 0x58, 0x53, 0x56, + 0x4E, 0x6C, 0x53, 0x32, 0x56, 0x35, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x0A, 0x45, 0x78, 0x6C, 0x50, 0x53, 0x56, 0x4E, 0x55, 0x52, 0x53, 0x42, 0x47, 0x62, 0x33, 0x56, 0x75, 0x5A, 0x47, 0x46, 0x30, 0x61, + 0x57, 0x39, 0x75, 0x49, 0x45, 0x56, 0x75, 0x5A, 0x47, 0x39, 0x79, 0x63, 0x32, 0x56, 0x6B, 0x4D, 0x53, 0x67, 0x77, 0x4A, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x39, 0x50, 0x53, 0x56, 0x4E, 0x55, 0x52, 0x53, 0x42, 0x58, 0x53, + 0x56, 0x4E, 0x6C, 0x53, 0x32, 0x56, 0x35, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x0A, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x64, 0x44, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x48, 0x59, 0x77, 0x45, 0x41, 0x59, 0x48, + 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x43, 0x41, 0x51, 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x54, 0x4F, 0x6C, 0x51, 0x77, 0x4D, 0x59, 0x50, 0x63, 0x68, 0x69, 0x38, 0x32, 0x50, 0x47, 0x36, + 0x73, 0x34, 0x6E, 0x69, 0x65, 0x55, 0x71, 0x6A, 0x46, 0x71, 0x64, 0x72, 0x0A, 0x56, 0x43, 0x54, 0x62, 0x55, 0x66, 0x2F, 0x71, 0x39, 0x41, 0x6B, 0x6B, 0x77, 0x77, 0x73, 0x69, 0x6E, 0x38, 0x74, 0x71, 0x4A, 0x34, 0x4B, 0x42, 0x44, 0x64, 0x4C, + 0x41, 0x72, 0x7A, 0x48, 0x6B, 0x64, 0x49, 0x4A, 0x75, 0x79, 0x69, 0x58, 0x5A, 0x6A, 0x48, 0x57, 0x64, 0x38, 0x64, 0x76, 0x51, 0x6D, 0x71, 0x4A, 0x4C, 0x49, 0x58, 0x34, 0x57, 0x70, 0x32, 0x4F, 0x51, 0x30, 0x6A, 0x6E, 0x55, 0x73, 0x59, 0x64, + 0x34, 0x58, 0x78, 0x69, 0x57, 0x44, 0x31, 0x41, 0x62, 0x0A, 0x4E, 0x54, 0x63, 0x50, 0x61, 0x73, 0x62, 0x63, 0x32, 0x52, 0x4E, 0x4E, 0x70, 0x49, 0x36, 0x51, 0x4E, 0x2B, 0x61, 0x39, 0x57, 0x7A, 0x47, 0x52, 0x6F, 0x31, 0x51, 0x77, 0x55, 0x6A, + 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, + 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x53, 0x49, 0x63, 0x55, 0x72, 0x4F, 0x50, 0x44, 0x6E, 0x70, 0x42, 0x67, 0x4F, 0x74, 0x66, 0x4B, 0x69, 0x65, 0x37, 0x54, 0x72, + 0x59, 0x79, 0x30, 0x55, 0x47, 0x59, 0x77, 0x45, 0x41, 0x59, 0x4A, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x41, 0x47, 0x43, 0x4E, 0x78, 0x55, 0x42, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x41, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, + 0x6A, 0x30, 0x45, 0x0A, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, 0x77, 0x5A, 0x51, 0x49, 0x77, 0x4A, 0x73, 0x64, 0x70, 0x57, 0x39, 0x7A, 0x56, 0x35, 0x37, 0x4C, 0x6E, 0x79, 0x41, 0x79, 0x4D, 0x6A, 0x4D, 0x50, 0x64, 0x65, 0x59, 0x77, 0x62, + 0x59, 0x39, 0x58, 0x4A, 0x55, 0x70, 0x52, 0x4F, 0x54, 0x59, 0x4A, 0x4B, 0x63, 0x78, 0x36, 0x79, 0x67, 0x49, 0x53, 0x70, 0x4A, 0x63, 0x42, 0x4D, 0x57, 0x6D, 0x31, 0x4A, 0x4B, 0x57, 0x42, 0x34, 0x45, 0x2B, 0x4A, 0x2B, 0x53, 0x4F, 0x74, 0x6B, + 0x0A, 0x41, 0x6A, 0x45, 0x41, 0x32, 0x7A, 0x51, 0x67, 0x4D, 0x67, 0x6A, 0x2F, 0x6D, 0x6B, 0x6B, 0x43, 0x74, 0x6F, 0x6A, 0x65, 0x46, 0x4B, 0x39, 0x64, 0x62, 0x4A, 0x6C, 0x78, 0x6A, 0x52, 0x6F, 0x2F, 0x69, 0x39, 0x66, 0x67, 0x6F, 0x6A, 0x61, + 0x47, 0x48, 0x41, 0x65, 0x43, 0x4F, 0x6E, 0x5A, 0x54, 0x2F, 0x63, 0x4B, 0x69, 0x37, 0x65, 0x39, 0x37, 0x73, 0x49, 0x42, 0x50, 0x57, 0x41, 0x39, 0x4C, 0x55, 0x7A, 0x6D, 0x39, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x55, 0x43, 0x41, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x47, 0x32, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x52, 0x6A, 0x43, 0x43, 0x41, 0x79, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x58, 0x64, 0x2B, 0x78, 0x32, 0x6C, 0x71, 0x6A, 0x37, 0x56, 0x32, 0x2B, 0x57, 0x6D, 0x55, 0x67, 0x5A, + 0x51, 0x4F, 0x51, 0x37, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x41, 0x39, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, + 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x49, 0x56, 0x57, 0x35, 0x70, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x4D, 0x4D, 0x45, 0x6C, 0x56, 0x44, 0x51, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x52, 0x7A, 0x49, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x44, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x0A, 0x4E, 0x6A, 0x41, + 0x7A, 0x4D, 0x54, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x44, 0x45, 0x79, 0x4D, 0x7A, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x44, 0x30, 0x78, 0x43, 0x7A, 0x41, + 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x4F, 0x4D, 0x52, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x68, 0x56, 0x62, 0x6D, 0x6C, 0x55, 0x0A, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x44, + 0x45, 0x62, 0x4D, 0x42, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x53, 0x56, 0x55, 0x4E, 0x42, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x48, 0x4D, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4D, 0x49, + 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, 0x41, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, + 0x67, 0x45, 0x41, 0x78, 0x65, 0x59, 0x72, 0x62, 0x33, 0x7A, 0x76, 0x4A, 0x67, 0x55, 0x6E, 0x6F, 0x34, 0x45, 0x6B, 0x32, 0x6D, 0x2F, 0x4C, 0x41, 0x66, 0x6D, 0x5A, 0x6D, 0x71, 0x6B, 0x79, 0x77, 0x69, 0x4B, 0x48, 0x59, 0x55, 0x47, 0x52, 0x4F, + 0x38, 0x76, 0x44, 0x61, 0x42, 0x73, 0x47, 0x78, 0x55, 0x79, 0x70, 0x4B, 0x38, 0x46, 0x6E, 0x46, 0x79, 0x49, 0x64, 0x4B, 0x2B, 0x33, 0x35, 0x4B, 0x59, 0x6D, 0x54, 0x0A, 0x6F, 0x6E, 0x69, 0x39, 0x6B, 0x6D, 0x75, 0x67, 0x6F, 0x77, 0x32, 0x69, + 0x66, 0x73, 0x71, 0x54, 0x73, 0x36, 0x62, 0x52, 0x6A, 0x44, 0x58, 0x56, 0x64, 0x66, 0x6B, 0x58, 0x39, 0x73, 0x39, 0x46, 0x78, 0x65, 0x56, 0x36, 0x37, 0x48, 0x65, 0x54, 0x6F, 0x49, 0x38, 0x6A, 0x72, 0x67, 0x34, 0x61, 0x41, 0x33, 0x2B, 0x2B, + 0x31, 0x4E, 0x44, 0x74, 0x4C, 0x6E, 0x75, 0x72, 0x52, 0x69, 0x4E, 0x62, 0x2F, 0x79, 0x7A, 0x6D, 0x56, 0x48, 0x71, 0x55, 0x77, 0x43, 0x6F, 0x56, 0x0A, 0x38, 0x4D, 0x6D, 0x4E, 0x73, 0x48, 0x6F, 0x37, 0x4A, 0x4F, 0x48, 0x58, 0x61, 0x4F, 0x49, + 0x78, 0x50, 0x41, 0x59, 0x7A, 0x52, 0x72, 0x5A, 0x55, 0x45, 0x61, 0x61, 0x6C, 0x4C, 0x79, 0x4A, 0x55, 0x4B, 0x6C, 0x67, 0x4E, 0x41, 0x51, 0x4C, 0x78, 0x2B, 0x68, 0x56, 0x52, 0x5A, 0x32, 0x7A, 0x41, 0x2B, 0x74, 0x65, 0x32, 0x47, 0x33, 0x2F, + 0x52, 0x56, 0x6F, 0x67, 0x76, 0x47, 0x6A, 0x71, 0x4E, 0x4F, 0x37, 0x75, 0x43, 0x45, 0x65, 0x42, 0x48, 0x41, 0x4E, 0x42, 0x53, 0x0A, 0x68, 0x36, 0x76, 0x37, 0x68, 0x6E, 0x34, 0x50, 0x4A, 0x47, 0x74, 0x41, 0x6E, 0x54, 0x52, 0x6E, 0x76, 0x49, + 0x33, 0x48, 0x4C, 0x59, 0x5A, 0x76, 0x65, 0x54, 0x36, 0x4F, 0x71, 0x54, 0x77, 0x58, 0x53, 0x33, 0x2B, 0x77, 0x6D, 0x65, 0x4F, 0x77, 0x63, 0x57, 0x44, 0x63, 0x43, 0x2F, 0x56, 0x6B, 0x77, 0x38, 0x35, 0x44, 0x76, 0x47, 0x31, 0x78, 0x75, 0x64, + 0x4C, 0x65, 0x4A, 0x31, 0x75, 0x4B, 0x36, 0x4E, 0x6A, 0x47, 0x72, 0x75, 0x46, 0x5A, 0x66, 0x63, 0x38, 0x6F, 0x0A, 0x4C, 0x54, 0x57, 0x34, 0x6C, 0x56, 0x59, 0x61, 0x38, 0x62, 0x4A, 0x59, 0x53, 0x37, 0x63, 0x53, 0x4E, 0x38, 0x68, 0x38, 0x73, + 0x2B, 0x31, 0x4C, 0x67, 0x4F, 0x47, 0x4E, 0x2B, 0x6A, 0x49, 0x6A, 0x74, 0x6D, 0x2B, 0x33, 0x53, 0x4A, 0x55, 0x49, 0x73, 0x55, 0x52, 0x4F, 0x68, 0x59, 0x77, 0x36, 0x41, 0x6C, 0x51, 0x67, 0x4C, 0x39, 0x2B, 0x2F, 0x56, 0x30, 0x38, 0x37, 0x4F, + 0x70, 0x41, 0x68, 0x31, 0x38, 0x45, 0x6D, 0x4E, 0x56, 0x51, 0x67, 0x37, 0x4D, 0x63, 0x2F, 0x0A, 0x52, 0x2B, 0x7A, 0x76, 0x57, 0x72, 0x39, 0x4C, 0x65, 0x73, 0x47, 0x74, 0x4F, 0x78, 0x64, 0x51, 0x58, 0x47, 0x4C, 0x59, 0x44, 0x30, 0x74, 0x4B, + 0x33, 0x43, 0x76, 0x36, 0x62, 0x72, 0x78, 0x7A, 0x6B, 0x73, 0x33, 0x73, 0x78, 0x31, 0x44, 0x6F, 0x51, 0x5A, 0x62, 0x58, 0x71, 0x58, 0x35, 0x74, 0x32, 0x4F, 0x6B, 0x64, 0x6A, 0x34, 0x71, 0x31, 0x75, 0x56, 0x69, 0x53, 0x75, 0x6B, 0x71, 0x53, + 0x4B, 0x77, 0x78, 0x57, 0x2F, 0x59, 0x44, 0x72, 0x43, 0x50, 0x42, 0x65, 0x0A, 0x4B, 0x57, 0x34, 0x62, 0x48, 0x41, 0x79, 0x76, 0x6A, 0x35, 0x4F, 0x4A, 0x72, 0x64, 0x75, 0x39, 0x6F, 0x35, 0x34, 0x68, 0x79, 0x6F, 0x6B, 0x5A, 0x37, 0x4E, 0x2B, + 0x31, 0x77, 0x78, 0x72, 0x72, 0x46, 0x76, 0x35, 0x34, 0x4E, 0x6B, 0x7A, 0x57, 0x62, 0x74, 0x41, 0x2B, 0x46, 0x78, 0x79, 0x51, 0x46, 0x32, 0x73, 0x6D, 0x75, 0x76, 0x74, 0x36, 0x4C, 0x37, 0x38, 0x52, 0x48, 0x42, 0x67, 0x4F, 0x4C, 0x58, 0x4D, + 0x44, 0x6A, 0x36, 0x44, 0x6C, 0x4E, 0x61, 0x42, 0x61, 0x0A, 0x34, 0x6B, 0x78, 0x31, 0x48, 0x58, 0x48, 0x68, 0x4F, 0x54, 0x68, 0x54, 0x65, 0x45, 0x44, 0x4D, 0x67, 0x35, 0x50, 0x58, 0x43, 0x70, 0x36, 0x64, 0x57, 0x34, 0x2B, 0x4B, 0x35, 0x4F, + 0x58, 0x67, 0x53, 0x4F, 0x52, 0x49, 0x73, 0x6B, 0x66, 0x4E, 0x54, 0x69, 0x70, 0x31, 0x4B, 0x6E, 0x76, 0x79, 0x49, 0x76, 0x62, 0x4A, 0x76, 0x67, 0x6D, 0x52, 0x6C, 0x6C, 0x64, 0x36, 0x69, 0x49, 0x69, 0x73, 0x37, 0x6E, 0x43, 0x73, 0x2B, 0x64, + 0x77, 0x70, 0x34, 0x77, 0x77, 0x63, 0x0A, 0x4F, 0x78, 0x4A, 0x4F, 0x52, 0x4E, 0x61, 0x6E, 0x54, 0x72, 0x41, 0x6D, 0x79, 0x50, 0x50, 0x5A, 0x47, 0x70, 0x65, 0x52, 0x61, 0x4F, 0x72, 0x76, 0x6A, 0x55, 0x59, 0x47, 0x30, 0x6C, 0x5A, 0x46, 0x57, + 0x4A, 0x6F, 0x38, 0x44, 0x41, 0x2B, 0x44, 0x75, 0x41, 0x55, 0x6C, 0x77, 0x7A, 0x6E, 0x50, 0x4F, 0x36, 0x51, 0x30, 0x69, 0x62, 0x64, 0x35, 0x45, 0x69, 0x39, 0x48, 0x78, 0x65, 0x65, 0x70, 0x6C, 0x32, 0x6E, 0x38, 0x70, 0x6E, 0x64, 0x6E, 0x74, + 0x64, 0x39, 0x37, 0x0A, 0x38, 0x58, 0x70, 0x6C, 0x46, 0x65, 0x52, 0x68, 0x56, 0x6D, 0x55, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, + 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, + 0x0A, 0x42, 0x42, 0x59, 0x45, 0x46, 0x49, 0x48, 0x45, 0x6A, 0x4D, 0x7A, 0x31, 0x35, 0x44, 0x44, 0x2F, 0x70, 0x51, 0x77, 0x49, 0x58, 0x34, 0x77, 0x56, 0x5A, 0x79, 0x46, 0x30, 0x41, 0x64, 0x2F, 0x66, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, + 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x41, 0x54, 0x5A, 0x53, 0x4C, 0x31, 0x6A, 0x69, 0x75, 0x74, 0x52, 0x4F, 0x54, 0x4C, 0x2F, 0x37, 0x6C, 0x6F, 0x0A, 0x35, 0x73, + 0x4F, 0x41, 0x53, 0x44, 0x30, 0x45, 0x65, 0x2F, 0x6F, 0x6A, 0x4C, 0x33, 0x72, 0x74, 0x4E, 0x74, 0x71, 0x79, 0x7A, 0x6D, 0x33, 0x32, 0x35, 0x70, 0x37, 0x6C, 0x58, 0x31, 0x69, 0x50, 0x79, 0x7A, 0x63, 0x79, 0x6F, 0x63, 0x68, 0x6C, 0x74, 0x71, + 0x34, 0x34, 0x50, 0x54, 0x55, 0x62, 0x50, 0x72, 0x77, 0x37, 0x74, 0x67, 0x54, 0x51, 0x76, 0x50, 0x6C, 0x4A, 0x39, 0x5A, 0x76, 0x33, 0x68, 0x63, 0x55, 0x32, 0x74, 0x73, 0x75, 0x38, 0x2B, 0x4D, 0x67, 0x35, 0x0A, 0x31, 0x65, 0x52, 0x66, 0x42, + 0x37, 0x30, 0x56, 0x56, 0x4A, 0x64, 0x30, 0x79, 0x73, 0x72, 0x74, 0x54, 0x37, 0x71, 0x36, 0x5A, 0x48, 0x61, 0x66, 0x67, 0x62, 0x69, 0x45, 0x52, 0x55, 0x6C, 0x4D, 0x6A, 0x57, 0x2B, 0x69, 0x36, 0x37, 0x48, 0x4D, 0x30, 0x63, 0x4F, 0x55, 0x32, + 0x6B, 0x54, 0x43, 0x35, 0x75, 0x4C, 0x71, 0x47, 0x4F, 0x69, 0x69, 0x48, 0x79, 0x63, 0x46, 0x75, 0x74, 0x66, 0x6C, 0x31, 0x71, 0x6E, 0x4E, 0x33, 0x65, 0x39, 0x32, 0x6D, 0x49, 0x30, 0x41, 0x0A, 0x44, 0x73, 0x30, 0x62, 0x2B, 0x67, 0x4F, 0x33, + 0x6A, 0x6F, 0x42, 0x59, 0x44, 0x69, 0x63, 0x2F, 0x55, 0x76, 0x75, 0x55, 0x6F, 0x73, 0x70, 0x65, 0x5A, 0x63, 0x6E, 0x57, 0x68, 0x4E, 0x71, 0x35, 0x4E, 0x58, 0x48, 0x7A, 0x4A, 0x73, 0x42, 0x50, 0x64, 0x2B, 0x61, 0x42, 0x4A, 0x39, 0x4A, 0x33, + 0x4F, 0x35, 0x6F, 0x55, 0x62, 0x33, 0x6E, 0x30, 0x39, 0x74, 0x44, 0x68, 0x30, 0x35, 0x53, 0x36, 0x30, 0x46, 0x64, 0x52, 0x76, 0x53, 0x63, 0x46, 0x44, 0x63, 0x48, 0x39, 0x0A, 0x79, 0x42, 0x49, 0x77, 0x37, 0x6D, 0x2B, 0x4E, 0x45, 0x53, 0x73, + 0x49, 0x6E, 0x64, 0x54, 0x55, 0x76, 0x34, 0x42, 0x46, 0x46, 0x4A, 0x71, 0x49, 0x52, 0x4E, 0x6F, 0x77, 0x36, 0x72, 0x53, 0x6E, 0x34, 0x2B, 0x37, 0x76, 0x57, 0x34, 0x4C, 0x56, 0x50, 0x74, 0x61, 0x74, 0x65, 0x4A, 0x4C, 0x62, 0x58, 0x44, 0x7A, + 0x7A, 0x32, 0x4B, 0x33, 0x36, 0x75, 0x47, 0x74, 0x2F, 0x78, 0x44, 0x59, 0x6F, 0x74, 0x67, 0x49, 0x56, 0x69, 0x6C, 0x51, 0x73, 0x6E, 0x4C, 0x41, 0x58, 0x0A, 0x63, 0x34, 0x37, 0x51, 0x4E, 0x36, 0x4D, 0x55, 0x50, 0x4A, 0x69, 0x56, 0x41, 0x41, + 0x77, 0x70, 0x42, 0x56, 0x75, 0x65, 0x53, 0x55, 0x6D, 0x78, 0x58, 0x38, 0x66, 0x6A, 0x79, 0x38, 0x38, 0x6E, 0x5A, 0x59, 0x34, 0x31, 0x46, 0x37, 0x64, 0x58, 0x79, 0x44, 0x44, 0x5A, 0x51, 0x56, 0x75, 0x35, 0x46, 0x4C, 0x62, 0x6F, 0x77, 0x67, + 0x2B, 0x55, 0x4D, 0x61, 0x65, 0x55, 0x6D, 0x4D, 0x78, 0x71, 0x36, 0x37, 0x58, 0x68, 0x4A, 0x2F, 0x55, 0x51, 0x71, 0x41, 0x48, 0x6F, 0x0A, 0x6A, 0x68, 0x4A, 0x69, 0x36, 0x49, 0x6A, 0x4D, 0x74, 0x58, 0x39, 0x47, 0x6C, 0x38, 0x43, 0x62, 0x45, + 0x47, 0x59, 0x34, 0x47, 0x6A, 0x5A, 0x47, 0x58, 0x79, 0x4A, 0x6F, 0x50, 0x64, 0x2F, 0x4A, 0x78, 0x68, 0x4D, 0x6E, 0x71, 0x31, 0x4D, 0x47, 0x72, 0x4B, 0x49, 0x38, 0x68, 0x67, 0x5A, 0x6C, 0x62, 0x37, 0x46, 0x2B, 0x73, 0x53, 0x6C, 0x45, 0x6D, + 0x71, 0x4F, 0x36, 0x53, 0x57, 0x6B, 0x6F, 0x61, 0x59, 0x2F, 0x58, 0x35, 0x56, 0x2B, 0x74, 0x42, 0x49, 0x5A, 0x6B, 0x0A, 0x62, 0x78, 0x71, 0x67, 0x44, 0x4D, 0x55, 0x49, 0x59, 0x73, 0x36, 0x41, 0x6F, 0x39, 0x44, 0x7A, 0x37, 0x47, 0x6A, 0x65, + 0x76, 0x6A, 0x50, 0x48, 0x46, 0x31, 0x74, 0x2F, 0x67, 0x4D, 0x52, 0x4D, 0x54, 0x4C, 0x47, 0x6D, 0x68, 0x49, 0x72, 0x44, 0x4F, 0x37, 0x67, 0x4A, 0x7A, 0x52, 0x53, 0x42, 0x75, 0x68, 0x6A, 0x6A, 0x56, 0x46, 0x63, 0x32, 0x2F, 0x74, 0x73, 0x76, + 0x66, 0x45, 0x65, 0x68, 0x4F, 0x6A, 0x50, 0x49, 0x2B, 0x56, 0x67, 0x37, 0x52, 0x45, 0x2B, 0x78, 0x0A, 0x79, 0x67, 0x4B, 0x4A, 0x42, 0x4A, 0x59, 0x6F, 0x61, 0x4D, 0x56, 0x4C, 0x75, 0x43, 0x61, 0x4A, 0x75, 0x39, 0x59, 0x7A, 0x4C, 0x31, 0x44, + 0x56, 0x2F, 0x70, 0x71, 0x4A, 0x75, 0x68, 0x67, 0x79, 0x6B, 0x6C, 0x54, 0x47, 0x57, 0x2B, 0x43, 0x64, 0x2B, 0x56, 0x37, 0x6C, 0x44, 0x53, 0x4B, 0x62, 0x39, 0x74, 0x72, 0x69, 0x79, 0x43, 0x47, 0x79, 0x59, 0x69, 0x47, 0x71, 0x68, 0x6B, 0x43, + 0x79, 0x4C, 0x6D, 0x54, 0x54, 0x58, 0x38, 0x6A, 0x6A, 0x66, 0x68, 0x46, 0x6E, 0x0A, 0x52, 0x52, 0x38, 0x46, 0x2F, 0x75, 0x4F, 0x69, 0x37, 0x37, 0x4F, 0x6F, 0x73, 0x2F, 0x4E, 0x39, 0x6A, 0x2F, 0x67, 0x4D, 0x48, 0x79, 0x49, 0x66, 0x4C, 0x58, + 0x43, 0x30, 0x75, 0x41, 0x45, 0x30, 0x64, 0x6A, 0x41, 0x41, 0x35, 0x53, 0x4E, 0x34, 0x70, 0x31, 0x62, 0x58, 0x55, 0x42, 0x2B, 0x4B, 0x2B, 0x77, 0x62, 0x31, 0x77, 0x68, 0x6E, 0x77, 0x30, 0x41, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x55, 0x43, 0x41, 0x20, 0x45, 0x78, 0x74, 0x65, 0x6E, 0x64, 0x65, 0x64, 0x20, 0x56, 0x61, 0x6C, 0x69, 0x64, + 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x57, 0x6A, 0x43, 0x43, 0x41, 0x30, 0x4B, 0x67, + 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x54, 0x39, 0x49, 0x72, 0x6A, 0x2F, 0x56, 0x6B, 0x79, 0x44, 0x4F, 0x65, 0x54, 0x7A, 0x52, 0x59, 0x5A, 0x69, 0x4E, 0x77, 0x59, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, + 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x43, 0x67, 0x77, 0x49, 0x56, 0x57, 0x35, 0x70, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x46, 0x56, 0x44, 0x51, 0x53, 0x42, 0x46, 0x65, 0x48, 0x52, + 0x6C, 0x62, 0x6D, 0x52, 0x6C, 0x5A, 0x43, 0x42, 0x57, 0x59, 0x57, 0x78, 0x70, 0x5A, 0x47, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x55, 0x77, 0x4D, 0x7A, + 0x45, 0x7A, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x78, 0x4D, 0x6A, 0x4D, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x49, 0x56, 0x57, 0x35, 0x70, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x78, 0x4A, + 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x48, 0x46, 0x56, 0x44, 0x51, 0x53, 0x42, 0x46, 0x65, 0x48, 0x52, 0x6C, 0x62, 0x6D, 0x52, 0x6C, 0x5A, 0x43, 0x42, 0x57, 0x59, 0x57, 0x78, 0x70, 0x5A, 0x47, 0x46, 0x30, 0x61, + 0x57, 0x39, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x77, 0x67, 0x67, 0x49, 0x69, 0x0A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, + 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x70, 0x43, 0x51, 0x63, 0x6F, 0x45, 0x77, 0x4B, 0x77, 0x6D, 0x65, 0x42, 0x6B, 0x71, 0x68, 0x35, 0x44, 0x46, 0x6E, 0x70, 0x7A, 0x73, 0x5A, 0x47, 0x67, + 0x64, 0x54, 0x36, 0x6F, 0x2B, 0x75, 0x4D, 0x34, 0x41, 0x48, 0x72, 0x73, 0x0A, 0x69, 0x57, 0x6F, 0x67, 0x44, 0x34, 0x76, 0x46, 0x73, 0x4A, 0x73, 0x7A, 0x41, 0x31, 0x71, 0x47, 0x78, 0x6C, 0x69, 0x47, 0x31, 0x63, 0x47, 0x46, 0x75, 0x30, 0x2F, + 0x47, 0x6E, 0x45, 0x42, 0x4E, 0x79, 0x72, 0x37, 0x75, 0x61, 0x5A, 0x61, 0x34, 0x72, 0x59, 0x45, 0x77, 0x6D, 0x6E, 0x79, 0x53, 0x42, 0x65, 0x73, 0x46, 0x4B, 0x35, 0x70, 0x49, 0x30, 0x4C, 0x68, 0x32, 0x50, 0x70, 0x62, 0x49, 0x49, 0x4C, 0x76, + 0x53, 0x73, 0x50, 0x47, 0x50, 0x32, 0x4B, 0x78, 0x46, 0x0A, 0x52, 0x76, 0x2B, 0x71, 0x5A, 0x32, 0x43, 0x30, 0x64, 0x33, 0x35, 0x71, 0x48, 0x7A, 0x77, 0x61, 0x55, 0x6E, 0x6F, 0x45, 0x50, 0x51, 0x63, 0x38, 0x68, 0x51, 0x32, 0x45, 0x30, 0x42, + 0x39, 0x32, 0x43, 0x76, 0x64, 0x71, 0x46, 0x4E, 0x39, 0x79, 0x34, 0x7A, 0x52, 0x38, 0x56, 0x30, 0x35, 0x57, 0x41, 0x54, 0x35, 0x35, 0x38, 0x61, 0x6F, 0x70, 0x4F, 0x32, 0x7A, 0x36, 0x2B, 0x49, 0x39, 0x74, 0x54, 0x63, 0x67, 0x31, 0x33, 0x36, + 0x37, 0x72, 0x33, 0x43, 0x54, 0x75, 0x0A, 0x65, 0x55, 0x57, 0x6E, 0x68, 0x62, 0x59, 0x46, 0x69, 0x4E, 0x36, 0x49, 0x58, 0x53, 0x56, 0x38, 0x6C, 0x32, 0x52, 0x6E, 0x43, 0x64, 0x6D, 0x2F, 0x57, 0x68, 0x55, 0x46, 0x68, 0x76, 0x4D, 0x4A, 0x48, + 0x75, 0x78, 0x59, 0x4D, 0x6A, 0x4D, 0x52, 0x38, 0x33, 0x64, 0x6B, 0x73, 0x48, 0x59, 0x66, 0x35, 0x42, 0x41, 0x31, 0x46, 0x78, 0x76, 0x79, 0x44, 0x72, 0x46, 0x73, 0x70, 0x43, 0x71, 0x6A, 0x63, 0x2F, 0x77, 0x4A, 0x48, 0x78, 0x34, 0x79, 0x47, + 0x56, 0x4D, 0x52, 0x0A, 0x35, 0x39, 0x6D, 0x7A, 0x4C, 0x43, 0x35, 0x32, 0x4C, 0x71, 0x47, 0x6A, 0x33, 0x6E, 0x35, 0x71, 0x69, 0x41, 0x6E, 0x6F, 0x38, 0x67, 0x65, 0x4B, 0x2B, 0x4C, 0x4C, 0x4E, 0x45, 0x4F, 0x66, 0x69, 0x63, 0x30, 0x43, 0x54, + 0x75, 0x77, 0x6A, 0x52, 0x50, 0x2B, 0x48, 0x38, 0x43, 0x35, 0x53, 0x7A, 0x4A, 0x65, 0x39, 0x38, 0x70, 0x74, 0x66, 0x52, 0x72, 0x35, 0x2F, 0x2F, 0x6C, 0x70, 0x72, 0x31, 0x6B, 0x58, 0x75, 0x59, 0x43, 0x33, 0x66, 0x55, 0x66, 0x75, 0x67, 0x48, + 0x0A, 0x30, 0x6D, 0x4B, 0x31, 0x6C, 0x54, 0x6E, 0x6A, 0x38, 0x2F, 0x46, 0x74, 0x44, 0x77, 0x35, 0x6C, 0x68, 0x49, 0x70, 0x6A, 0x56, 0x4D, 0x57, 0x41, 0x74, 0x75, 0x43, 0x65, 0x53, 0x33, 0x31, 0x48, 0x4A, 0x71, 0x63, 0x42, 0x43, 0x46, 0x33, + 0x52, 0x69, 0x4A, 0x37, 0x58, 0x77, 0x7A, 0x4A, 0x45, 0x2B, 0x6F, 0x4A, 0x4B, 0x43, 0x6D, 0x68, 0x55, 0x66, 0x7A, 0x68, 0x54, 0x41, 0x38, 0x79, 0x6B, 0x41, 0x44, 0x4E, 0x6B, 0x55, 0x56, 0x6B, 0x4C, 0x6F, 0x34, 0x4B, 0x52, 0x0A, 0x65, 0x6C, + 0x37, 0x73, 0x46, 0x73, 0x4C, 0x7A, 0x4B, 0x75, 0x5A, 0x69, 0x32, 0x69, 0x72, 0x62, 0x57, 0x57, 0x49, 0x51, 0x4A, 0x55, 0x6F, 0x71, 0x67, 0x51, 0x74, 0x48, 0x42, 0x30, 0x4D, 0x47, 0x63, 0x49, 0x66, 0x53, 0x2B, 0x70, 0x4D, 0x52, 0x4B, 0x58, + 0x70, 0x49, 0x54, 0x65, 0x75, 0x55, 0x78, 0x33, 0x42, 0x4E, 0x72, 0x32, 0x66, 0x56, 0x55, 0x62, 0x47, 0x41, 0x49, 0x41, 0x45, 0x42, 0x74, 0x48, 0x6F, 0x49, 0x70, 0x70, 0x42, 0x2F, 0x54, 0x75, 0x44, 0x76, 0x0A, 0x42, 0x30, 0x47, 0x48, 0x72, + 0x32, 0x71, 0x6C, 0x58, 0x6F, 0x76, 0x37, 0x7A, 0x31, 0x43, 0x79, 0x6D, 0x6C, 0x53, 0x76, 0x77, 0x34, 0x6D, 0x36, 0x57, 0x43, 0x33, 0x31, 0x4D, 0x4A, 0x69, 0x78, 0x4E, 0x6E, 0x49, 0x35, 0x66, 0x6B, 0x6B, 0x45, 0x2F, 0x53, 0x6D, 0x6E, 0x54, + 0x48, 0x6E, 0x6B, 0x42, 0x56, 0x66, 0x62, 0x6C, 0x4C, 0x6B, 0x57, 0x55, 0x34, 0x31, 0x47, 0x73, 0x78, 0x32, 0x56, 0x59, 0x56, 0x64, 0x57, 0x66, 0x36, 0x2F, 0x77, 0x46, 0x6C, 0x74, 0x68, 0x0A, 0x57, 0x47, 0x38, 0x32, 0x55, 0x42, 0x45, 0x4C, + 0x32, 0x4B, 0x77, 0x72, 0x6C, 0x52, 0x59, 0x61, 0x44, 0x68, 0x38, 0x49, 0x7A, 0x54, 0x59, 0x30, 0x5A, 0x52, 0x42, 0x69, 0x5A, 0x74, 0x57, 0x41, 0x58, 0x78, 0x51, 0x67, 0x58, 0x79, 0x30, 0x4D, 0x6F, 0x48, 0x67, 0x4B, 0x61, 0x4E, 0x59, 0x73, + 0x31, 0x2B, 0x6C, 0x76, 0x4B, 0x39, 0x4A, 0x4B, 0x42, 0x5A, 0x50, 0x38, 0x6E, 0x6D, 0x39, 0x72, 0x5A, 0x2F, 0x2B, 0x49, 0x38, 0x55, 0x36, 0x6C, 0x61, 0x55, 0x70, 0x53, 0x0A, 0x4E, 0x77, 0x58, 0x71, 0x78, 0x68, 0x61, 0x4E, 0x30, 0x73, 0x53, + 0x5A, 0x30, 0x59, 0x49, 0x72, 0x4F, 0x37, 0x6F, 0x31, 0x64, 0x66, 0x64, 0x52, 0x55, 0x56, 0x6A, 0x7A, 0x79, 0x41, 0x66, 0x64, 0x35, 0x4C, 0x51, 0x44, 0x66, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, + 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x32, 0x58, 0x51, 0x36, 0x35, 0x44, 0x41, 0x39, 0x44, 0x66, 0x63, 0x53, 0x0A, 0x33, 0x48, 0x35, 0x61, 0x42, 0x5A, 0x38, 0x65, 0x4E, 0x4A, 0x72, 0x33, 0x34, 0x52, + 0x51, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, + 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x0A, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x44, 0x61, 0x4E, 0x6C, 0x38, 0x78, 0x43, 0x46, + 0x57, 0x51, 0x70, 0x4E, 0x35, 0x73, 0x6D, 0x4C, 0x4E, 0x62, 0x37, 0x72, 0x68, 0x56, 0x70, 0x4C, 0x47, 0x73, 0x61, 0x47, 0x76, 0x64, 0x66, 0x74, 0x76, 0x6B, 0x48, 0x54, 0x46, 0x6E, 0x71, 0x38, 0x38, 0x6E, 0x49, 0x75, 0x61, 0x37, 0x4D, 0x75, + 0x69, 0x35, 0x36, 0x33, 0x4D, 0x44, 0x31, 0x73, 0x43, 0x33, 0x41, 0x4F, 0x36, 0x2B, 0x66, 0x63, 0x41, 0x55, 0x52, 0x0A, 0x61, 0x70, 0x38, 0x6C, 0x54, 0x77, 0x45, 0x70, 0x63, 0x4F, 0x50, 0x6C, 0x44, 0x4F, 0x48, 0x71, 0x57, 0x6E, 0x7A, 0x63, + 0x53, 0x62, 0x76, 0x42, 0x48, 0x69, 0x71, 0x42, 0x39, 0x52, 0x5A, 0x4C, 0x63, 0x70, 0x48, 0x49, 0x6F, 0x6A, 0x47, 0x35, 0x71, 0x74, 0x72, 0x38, 0x6E, 0x52, 0x2F, 0x7A, 0x58, 0x55, 0x41, 0x43, 0x45, 0x2F, 0x78, 0x4F, 0x48, 0x41, 0x62, 0x4B, + 0x73, 0x78, 0x53, 0x51, 0x56, 0x42, 0x63, 0x5A, 0x45, 0x68, 0x72, 0x78, 0x48, 0x39, 0x63, 0x4D, 0x0A, 0x61, 0x56, 0x72, 0x32, 0x63, 0x58, 0x6A, 0x30, 0x6C, 0x48, 0x32, 0x52, 0x43, 0x34, 0x37, 0x73, 0x6B, 0x46, 0x53, 0x4F, 0x76, 0x47, 0x2B, + 0x68, 0x54, 0x4B, 0x76, 0x38, 0x64, 0x47, 0x54, 0x39, 0x63, 0x5A, 0x72, 0x34, 0x51, 0x51, 0x65, 0x68, 0x7A, 0x5A, 0x48, 0x6B, 0x50, 0x4A, 0x72, 0x67, 0x6D, 0x7A, 0x49, 0x35, 0x63, 0x36, 0x73, 0x71, 0x31, 0x57, 0x6E, 0x49, 0x65, 0x4A, 0x45, + 0x6D, 0x4D, 0x58, 0x33, 0x69, 0x78, 0x7A, 0x44, 0x78, 0x2F, 0x42, 0x52, 0x34, 0x0A, 0x64, 0x78, 0x49, 0x4F, 0x45, 0x2F, 0x54, 0x64, 0x46, 0x70, 0x53, 0x2F, 0x53, 0x32, 0x64, 0x37, 0x63, 0x46, 0x4F, 0x46, 0x79, 0x72, 0x43, 0x37, 0x38, 0x7A, + 0x68, 0x4E, 0x4C, 0x4A, 0x41, 0x35, 0x77, 0x41, 0x33, 0x43, 0x58, 0x57, 0x76, 0x70, 0x34, 0x75, 0x58, 0x56, 0x69, 0x49, 0x33, 0x57, 0x4C, 0x4C, 0x2B, 0x72, 0x47, 0x37, 0x36, 0x31, 0x4B, 0x49, 0x63, 0x53, 0x46, 0x33, 0x52, 0x75, 0x2F, 0x48, + 0x33, 0x38, 0x6A, 0x39, 0x43, 0x48, 0x4A, 0x72, 0x41, 0x62, 0x0A, 0x2B, 0x37, 0x6C, 0x73, 0x71, 0x2B, 0x4B, 0x65, 0x50, 0x52, 0x58, 0x42, 0x4F, 0x79, 0x35, 0x6E, 0x41, 0x6C, 0x69, 0x52, 0x6E, 0x2B, 0x2F, 0x34, 0x51, 0x68, 0x38, 0x73, 0x74, + 0x32, 0x6A, 0x31, 0x64, 0x61, 0x33, 0x50, 0x74, 0x66, 0x62, 0x2F, 0x45, 0x58, 0x33, 0x43, 0x38, 0x43, 0x53, 0x6C, 0x72, 0x64, 0x50, 0x36, 0x6F, 0x44, 0x79, 0x70, 0x2B, 0x6C, 0x33, 0x63, 0x70, 0x61, 0x44, 0x76, 0x52, 0x4B, 0x53, 0x2B, 0x31, + 0x75, 0x6A, 0x6C, 0x35, 0x42, 0x4F, 0x57, 0x0A, 0x46, 0x33, 0x73, 0x47, 0x50, 0x6A, 0x4C, 0x74, 0x78, 0x37, 0x64, 0x43, 0x76, 0x48, 0x61, 0x6A, 0x32, 0x47, 0x55, 0x34, 0x4B, 0x7A, 0x67, 0x31, 0x55, 0x53, 0x45, 0x4F, 0x44, 0x6D, 0x38, 0x75, + 0x4E, 0x42, 0x4E, 0x41, 0x34, 0x53, 0x74, 0x6E, 0x44, 0x47, 0x31, 0x4B, 0x51, 0x54, 0x41, 0x59, 0x49, 0x31, 0x6F, 0x79, 0x56, 0x5A, 0x6E, 0x4A, 0x46, 0x2B, 0x41, 0x38, 0x33, 0x76, 0x62, 0x73, 0x65, 0x61, 0x30, 0x72, 0x57, 0x42, 0x6D, 0x69, + 0x72, 0x53, 0x77, 0x69, 0x0A, 0x47, 0x70, 0x57, 0x4F, 0x76, 0x70, 0x61, 0x51, 0x58, 0x55, 0x4A, 0x58, 0x78, 0x50, 0x6B, 0x55, 0x41, 0x7A, 0x55, 0x72, 0x48, 0x43, 0x31, 0x52, 0x56, 0x77, 0x69, 0x6E, 0x4F, 0x74, 0x34, 0x2F, 0x35, 0x4D, 0x69, + 0x30, 0x41, 0x33, 0x50, 0x43, 0x77, 0x53, 0x61, 0x41, 0x75, 0x77, 0x74, 0x43, 0x48, 0x36, 0x30, 0x4E, 0x72, 0x79, 0x5A, 0x79, 0x32, 0x73, 0x79, 0x2B, 0x73, 0x36, 0x4F, 0x44, 0x57, 0x41, 0x32, 0x43, 0x78, 0x52, 0x39, 0x47, 0x55, 0x65, 0x4F, + 0x63, 0x0A, 0x47, 0x4D, 0x79, 0x4E, 0x6D, 0x34, 0x33, 0x73, 0x53, 0x65, 0x74, 0x31, 0x55, 0x4E, 0x57, 0x4D, 0x4B, 0x46, 0x6E, 0x4B, 0x64, 0x44, 0x54, 0x61, 0x6A, 0x41, 0x73, 0x68, 0x71, 0x78, 0x37, 0x71, 0x47, 0x2B, 0x58, 0x48, 0x2F, 0x52, + 0x55, 0x2B, 0x77, 0x42, 0x65, 0x71, 0x2B, 0x79, 0x4E, 0x75, 0x4A, 0x6B, 0x62, 0x4C, 0x2B, 0x76, 0x6D, 0x78, 0x63, 0x6D, 0x74, 0x70, 0x7A, 0x79, 0x4B, 0x45, 0x43, 0x32, 0x49, 0x50, 0x72, 0x4E, 0x6B, 0x5A, 0x41, 0x4A, 0x53, 0x69, 0x0A, 0x64, + 0x6A, 0x7A, 0x55, 0x4C, 0x5A, 0x72, 0x74, 0x42, 0x4A, 0x34, 0x74, 0x42, 0x6D, 0x49, 0x51, 0x4E, 0x31, 0x49, 0x63, 0x68, 0x58, 0x49, 0x62, 0x4A, 0x2B, 0x58, 0x4D, 0x78, 0x6A, 0x48, 0x73, 0x4E, 0x2B, 0x78, 0x6A, 0x57, 0x5A, 0x73, 0x4C, 0x48, + 0x58, 0x62, 0x4D, 0x66, 0x6A, 0x4B, 0x61, 0x69, 0x4A, 0x55, 0x49, 0x4E, 0x6C, 0x4B, 0x37, 0x33, 0x6E, 0x5A, 0x66, 0x64, 0x6B, 0x6C, 0x4A, 0x72, 0x58, 0x2B, 0x39, 0x5A, 0x53, 0x43, 0x79, 0x79, 0x63, 0x45, 0x72, 0x0A, 0x64, 0x68, 0x68, 0x32, + 0x6E, 0x31, 0x61, 0x78, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x69, 0x67, 0x6E, 0x61, + 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x47, 0x57, 0x7A, 0x43, 0x43, 0x42, 0x45, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x52, 0x41, 0x4D, 0x72, 0x70, 0x47, 0x34, + 0x6E, 0x78, 0x56, 0x51, 0x4D, 0x4E, 0x6F, 0x2B, 0x5A, 0x42, 0x62, 0x63, 0x54, 0x6A, 0x70, 0x75, 0x45, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x57, 0x6A, + 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x6C, 0x49, 0x78, 0x45, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x43, 0x55, 0x52, 0x6F, 0x61, 0x57, 0x31, 0x35, 0x62, + 0x33, 0x52, 0x70, 0x63, 0x7A, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x77, 0x77, 0x54, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x69, 0x41, 0x30, 0x4F, 0x44, 0x45, 0x30, 0x4E, 0x6A, 0x4D, 0x77, 0x4F, 0x44, 0x45, 0x77, 0x4D, + 0x44, 0x41, 0x7A, 0x4E, 0x6A, 0x45, 0x5A, 0x0A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x51, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6E, 0x62, 0x6D, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, + 0x51, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4D, 0x7A, 0x45, 0x77, 0x4D, 0x44, 0x45, 0x77, 0x4F, 0x44, 0x4D, 0x79, 0x4D, 0x6A, 0x64, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4D, 0x7A, 0x45, 0x77, 0x4D, 0x44, 0x45, 0x77, 0x4F, 0x44, 0x4D, 0x79, + 0x4D, 0x6A, 0x64, 0x61, 0x0A, 0x4D, 0x46, 0x6F, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x5A, 0x53, 0x4D, 0x52, 0x49, 0x77, 0x45, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x6C, + 0x45, 0x61, 0x47, 0x6C, 0x74, 0x65, 0x57, 0x39, 0x30, 0x61, 0x58, 0x4D, 0x78, 0x48, 0x44, 0x41, 0x61, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x45, 0x7A, 0x41, 0x77, 0x4D, 0x44, 0x49, 0x67, 0x4E, 0x44, 0x67, 0x78, 0x4E, 0x44, 0x59, + 0x7A, 0x0A, 0x4D, 0x44, 0x67, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x7A, 0x59, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x45, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x32, 0x35, 0x68, 0x49, 0x46, + 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x0A, 0x44, + 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x4E, 0x47, 0x44, 0x6C, 0x6C, 0x47, 0x6C, 0x6D, 0x78, 0x36, 0x6D, 0x51, 0x57, 0x44, 0x6F, 0x79, 0x55, 0x4A, 0x4A, 0x56, 0x38, 0x67, 0x39, 0x50, 0x46, 0x4F, + 0x53, 0x62, 0x63, 0x44, 0x4F, 0x38, 0x57, 0x56, 0x34, 0x33, 0x58, 0x32, 0x4B, 0x79, 0x6A, 0x51, 0x6E, 0x2B, 0x43, 0x79, 0x75, 0x33, 0x4E, 0x57, 0x39, 0x73, 0x4F, 0x74, 0x79, 0x33, 0x74, 0x52, 0x51, 0x67, 0x58, 0x0A, 0x73, 0x74, 0x6D, 0x7A, + 0x79, 0x39, 0x59, 0x58, 0x55, 0x6E, 0x49, 0x6F, 0x32, 0x34, 0x35, 0x4F, 0x6E, 0x6F, 0x71, 0x32, 0x43, 0x2F, 0x6D, 0x65, 0x68, 0x4A, 0x70, 0x4E, 0x64, 0x74, 0x34, 0x69, 0x4B, 0x56, 0x7A, 0x53, 0x73, 0x39, 0x49, 0x47, 0x50, 0x6A, 0x41, 0x35, + 0x71, 0x58, 0x53, 0x6A, 0x6B, 0x6C, 0x59, 0x63, 0x6F, 0x57, 0x39, 0x4D, 0x43, 0x69, 0x42, 0x74, 0x6E, 0x79, 0x4E, 0x36, 0x74, 0x4D, 0x62, 0x61, 0x4C, 0x4F, 0x51, 0x64, 0x4C, 0x4E, 0x79, 0x7A, 0x0A, 0x4B, 0x4E, 0x41, 0x54, 0x38, 0x6B, 0x78, + 0x4F, 0x41, 0x6B, 0x6D, 0x68, 0x56, 0x45, 0x43, 0x65, 0x35, 0x75, 0x55, 0x46, 0x6F, 0x43, 0x32, 0x45, 0x79, 0x50, 0x2B, 0x59, 0x62, 0x4E, 0x44, 0x72, 0x69, 0x68, 0x71, 0x45, 0x43, 0x42, 0x36, 0x33, 0x61, 0x43, 0x50, 0x75, 0x49, 0x39, 0x56, + 0x77, 0x7A, 0x6D, 0x31, 0x52, 0x61, 0x52, 0x44, 0x75, 0x6F, 0x58, 0x72, 0x43, 0x30, 0x53, 0x49, 0x78, 0x77, 0x6F, 0x4B, 0x46, 0x30, 0x76, 0x4A, 0x56, 0x64, 0x6C, 0x42, 0x38, 0x0A, 0x4A, 0x58, 0x72, 0x4A, 0x68, 0x46, 0x77, 0x4C, 0x72, 0x4E, + 0x31, 0x43, 0x54, 0x69, 0x76, 0x6E, 0x67, 0x71, 0x49, 0x6B, 0x69, 0x63, 0x75, 0x51, 0x73, 0x74, 0x44, 0x75, 0x49, 0x37, 0x70, 0x6D, 0x54, 0x4C, 0x74, 0x69, 0x70, 0x50, 0x6C, 0x54, 0x57, 0x6D, 0x52, 0x37, 0x66, 0x4A, 0x6A, 0x36, 0x6F, 0x30, + 0x69, 0x65, 0x44, 0x35, 0x57, 0x75, 0x70, 0x78, 0x6A, 0x30, 0x61, 0x75, 0x77, 0x75, 0x41, 0x30, 0x57, 0x76, 0x38, 0x48, 0x54, 0x34, 0x4B, 0x73, 0x31, 0x36, 0x0A, 0x58, 0x64, 0x47, 0x2B, 0x52, 0x43, 0x59, 0x79, 0x4B, 0x66, 0x48, 0x78, 0x39, + 0x57, 0x7A, 0x4D, 0x66, 0x67, 0x49, 0x68, 0x43, 0x35, 0x39, 0x76, 0x70, 0x44, 0x2B, 0x2B, 0x6E, 0x56, 0x50, 0x69, 0x7A, 0x33, 0x32, 0x70, 0x4C, 0x48, 0x78, 0x59, 0x47, 0x70, 0x66, 0x68, 0x50, 0x54, 0x63, 0x33, 0x47, 0x47, 0x59, 0x6F, 0x30, + 0x6B, 0x44, 0x46, 0x55, 0x59, 0x71, 0x4D, 0x77, 0x79, 0x33, 0x4F, 0x55, 0x34, 0x67, 0x6B, 0x57, 0x47, 0x51, 0x77, 0x46, 0x73, 0x57, 0x71, 0x0A, 0x34, 0x4E, 0x59, 0x4B, 0x70, 0x6B, 0x44, 0x66, 0x65, 0x50, 0x62, 0x31, 0x42, 0x48, 0x78, 0x70, + 0x45, 0x34, 0x53, 0x38, 0x30, 0x64, 0x47, 0x6E, 0x42, 0x73, 0x38, 0x42, 0x39, 0x32, 0x6A, 0x41, 0x71, 0x46, 0x65, 0x37, 0x4F, 0x6D, 0x47, 0x74, 0x42, 0x49, 0x79, 0x54, 0x34, 0x36, 0x33, 0x38, 0x38, 0x4E, 0x74, 0x45, 0x62, 0x56, 0x6E, 0x63, + 0x53, 0x56, 0x6D, 0x75, 0x72, 0x4A, 0x71, 0x5A, 0x4E, 0x6A, 0x42, 0x42, 0x65, 0x33, 0x59, 0x7A, 0x49, 0x6F, 0x65, 0x6A, 0x0A, 0x77, 0x70, 0x4B, 0x47, 0x62, 0x76, 0x6C, 0x77, 0x37, 0x71, 0x36, 0x48, 0x68, 0x35, 0x55, 0x62, 0x78, 0x48, 0x71, + 0x39, 0x4D, 0x66, 0x50, 0x55, 0x30, 0x75, 0x57, 0x5A, 0x2F, 0x37, 0x35, 0x49, 0x37, 0x48, 0x58, 0x31, 0x65, 0x42, 0x59, 0x64, 0x70, 0x6E, 0x44, 0x42, 0x66, 0x7A, 0x77, 0x62, 0x6F, 0x5A, 0x4C, 0x37, 0x7A, 0x38, 0x67, 0x38, 0x31, 0x73, 0x57, + 0x54, 0x43, 0x6F, 0x2F, 0x31, 0x56, 0x54, 0x70, 0x32, 0x6C, 0x63, 0x35, 0x5A, 0x6D, 0x49, 0x6F, 0x4A, 0x0A, 0x6C, 0x58, 0x63, 0x79, 0x6D, 0x6F, 0x4F, 0x36, 0x4C, 0x41, 0x51, 0x36, 0x6C, 0x37, 0x33, 0x55, 0x4C, 0x37, 0x37, 0x58, 0x62, 0x4A, + 0x75, 0x69, 0x79, 0x6E, 0x31, 0x74, 0x4A, 0x73, 0x6C, 0x56, 0x31, 0x63, 0x2F, 0x44, 0x65, 0x56, 0x49, 0x49, 0x43, 0x5A, 0x6B, 0x48, 0x4A, 0x43, 0x31, 0x6B, 0x4A, 0x57, 0x75, 0x6D, 0x49, 0x57, 0x6D, 0x62, 0x61, 0x74, 0x31, 0x30, 0x54, 0x57, + 0x75, 0x58, 0x65, 0x6B, 0x47, 0x39, 0x71, 0x78, 0x66, 0x35, 0x6B, 0x42, 0x64, 0x49, 0x0A, 0x6A, 0x7A, 0x62, 0x35, 0x4C, 0x64, 0x58, 0x46, 0x32, 0x2B, 0x36, 0x71, 0x68, 0x55, 0x56, 0x42, 0x2B, 0x73, 0x30, 0x36, 0x52, 0x62, 0x46, 0x6F, 0x35, + 0x6A, 0x5A, 0x4D, 0x6D, 0x35, 0x42, 0x58, 0x37, 0x43, 0x4F, 0x35, 0x68, 0x77, 0x6A, 0x43, 0x78, 0x41, 0x6E, 0x78, 0x6C, 0x34, 0x59, 0x71, 0x4B, 0x45, 0x33, 0x69, 0x64, 0x4D, 0x44, 0x61, 0x78, 0x49, 0x7A, 0x62, 0x33, 0x2B, 0x4B, 0x68, 0x46, + 0x31, 0x6E, 0x4F, 0x4A, 0x46, 0x6C, 0x30, 0x4D, 0x64, 0x70, 0x2F, 0x0A, 0x2F, 0x54, 0x42, 0x74, 0x32, 0x64, 0x7A, 0x68, 0x61, 0x75, 0x48, 0x38, 0x58, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x34, 0x49, 0x42, 0x47, 0x6A, 0x43, 0x43, + 0x41, 0x52, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, + 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x0A, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x42, 0x69, 0x48, 0x56, 0x75, 0x42, 0x75, 0x64, 0x2B, 0x34, 0x6B, 0x4E, 0x54, 0x78, 0x4F, 0x63, 0x35, 0x6F, + 0x66, 0x31, 0x75, 0x48, 0x69, 0x65, 0x58, 0x34, 0x72, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x42, 0x69, 0x48, 0x56, 0x75, 0x42, 0x75, 0x64, 0x2B, 0x34, 0x6B, 0x4E, 0x54, 0x78, + 0x4F, 0x63, 0x35, 0x6F, 0x66, 0x0A, 0x31, 0x75, 0x48, 0x69, 0x65, 0x58, 0x34, 0x72, 0x4D, 0x45, 0x51, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x41, 0x51, 0x39, 0x4D, 0x44, 0x73, 0x77, 0x4F, 0x51, 0x59, 0x45, 0x56, 0x52, 0x30, 0x67, 0x41, 0x44, + 0x41, 0x78, 0x4D, 0x43, 0x38, 0x47, 0x43, 0x43, 0x73, 0x47, 0x41, 0x51, 0x55, 0x46, 0x42, 0x77, 0x49, 0x42, 0x46, 0x69, 0x4E, 0x6F, 0x64, 0x48, 0x52, 0x77, 0x63, 0x7A, 0x6F, 0x76, 0x4C, 0x33, 0x64, 0x33, 0x64, 0x33, 0x63, 0x75, 0x59, 0x32, + 0x56, 0x79, 0x0A, 0x64, 0x47, 0x6C, 0x6E, 0x62, 0x6D, 0x45, 0x75, 0x5A, 0x6E, 0x49, 0x76, 0x59, 0x58, 0x56, 0x30, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x47, 0x56, 0x7A, 0x4C, 0x7A, 0x42, 0x74, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x38, 0x45, 0x5A, + 0x6A, 0x42, 0x6B, 0x4D, 0x43, 0x2B, 0x67, 0x4C, 0x61, 0x41, 0x72, 0x68, 0x69, 0x6C, 0x6F, 0x64, 0x48, 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x6D, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x32, 0x35, 0x68, 0x0A, + 0x4C, 0x6D, 0x5A, 0x79, 0x4C, 0x32, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x70, 0x5A, 0x32, 0x35, 0x68, 0x63, 0x6D, 0x39, 0x76, 0x64, 0x47, 0x4E, 0x68, 0x4C, 0x6D, 0x4E, 0x79, 0x62, 0x44, 0x41, 0x78, 0x6F, 0x43, 0x2B, 0x67, 0x4C, 0x59, 0x59, 0x72, + 0x61, 0x48, 0x52, 0x30, 0x63, 0x44, 0x6F, 0x76, 0x4C, 0x32, 0x4E, 0x79, 0x62, 0x43, 0x35, 0x6B, 0x61, 0x47, 0x6C, 0x74, 0x65, 0x57, 0x39, 0x30, 0x61, 0x58, 0x4D, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4C, 0x32, 0x4E, 0x6C, 0x0A, 0x63, 0x6E, 0x52, + 0x70, 0x5A, 0x32, 0x35, 0x68, 0x63, 0x6D, 0x39, 0x76, 0x64, 0x47, 0x4E, 0x68, 0x4C, 0x6D, 0x4E, 0x79, 0x62, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, + 0x43, 0x41, 0x67, 0x45, 0x41, 0x6C, 0x4C, 0x69, 0x65, 0x54, 0x2F, 0x44, 0x6A, 0x6C, 0x51, 0x67, 0x69, 0x35, 0x38, 0x31, 0x6F, 0x51, 0x66, 0x63, 0x63, 0x56, 0x64, 0x56, 0x38, 0x41, 0x4F, 0x49, 0x74, 0x0A, 0x4F, 0x6F, 0x6C, 0x64, 0x61, 0x44, + 0x67, 0x76, 0x55, 0x53, 0x49, 0x4C, 0x53, 0x6F, 0x33, 0x4C, 0x36, 0x62, 0x74, 0x64, 0x50, 0x72, 0x74, 0x63, 0x50, 0x62, 0x45, 0x6F, 0x2F, 0x75, 0x52, 0x54, 0x56, 0x52, 0x50, 0x50, 0x6F, 0x5A, 0x41, 0x62, 0x41, 0x68, 0x31, 0x66, 0x5A, 0x6B, + 0x59, 0x4A, 0x4D, 0x79, 0x6A, 0x68, 0x44, 0x53, 0x53, 0x58, 0x63, 0x4E, 0x4D, 0x51, 0x48, 0x2B, 0x70, 0x6B, 0x56, 0x35, 0x61, 0x37, 0x58, 0x64, 0x72, 0x6E, 0x78, 0x49, 0x78, 0x50, 0x0A, 0x54, 0x47, 0x52, 0x47, 0x48, 0x56, 0x79, 0x48, 0x34, + 0x31, 0x6E, 0x65, 0x51, 0x74, 0x47, 0x62, 0x71, 0x48, 0x36, 0x6D, 0x69, 0x64, 0x32, 0x50, 0x48, 0x4D, 0x6B, 0x77, 0x67, 0x75, 0x30, 0x37, 0x6E, 0x4D, 0x33, 0x41, 0x36, 0x52, 0x6E, 0x67, 0x61, 0x74, 0x67, 0x43, 0x64, 0x54, 0x65, 0x72, 0x39, + 0x7A, 0x51, 0x6F, 0x4B, 0x4A, 0x48, 0x79, 0x42, 0x41, 0x70, 0x50, 0x4E, 0x65, 0x4E, 0x67, 0x4A, 0x67, 0x48, 0x36, 0x30, 0x42, 0x47, 0x4D, 0x2B, 0x52, 0x46, 0x71, 0x0A, 0x37, 0x71, 0x38, 0x39, 0x77, 0x31, 0x44, 0x54, 0x6A, 0x31, 0x38, 0x7A, + 0x65, 0x54, 0x79, 0x47, 0x71, 0x48, 0x4E, 0x46, 0x6B, 0x49, 0x77, 0x67, 0x74, 0x6E, 0x4A, 0x7A, 0x46, 0x79, 0x4F, 0x2B, 0x42, 0x32, 0x58, 0x6C, 0x65, 0x4A, 0x49, 0x4E, 0x75, 0x67, 0x48, 0x41, 0x36, 0x34, 0x77, 0x63, 0x5A, 0x72, 0x2B, 0x73, + 0x68, 0x6E, 0x63, 0x42, 0x6C, 0x41, 0x32, 0x63, 0x35, 0x75, 0x6B, 0x35, 0x6A, 0x52, 0x2B, 0x6D, 0x55, 0x59, 0x79, 0x5A, 0x44, 0x44, 0x6C, 0x33, 0x0A, 0x34, 0x62, 0x53, 0x62, 0x2B, 0x68, 0x78, 0x6E, 0x56, 0x32, 0x39, 0x71, 0x61, 0x6F, 0x36, + 0x70, 0x4B, 0x30, 0x78, 0x58, 0x65, 0x58, 0x70, 0x58, 0x49, 0x73, 0x2F, 0x4E, 0x58, 0x32, 0x4E, 0x47, 0x6A, 0x56, 0x78, 0x5A, 0x4F, 0x6F, 0x62, 0x34, 0x4D, 0x6B, 0x64, 0x69, 0x6F, 0x32, 0x63, 0x4E, 0x47, 0x4A, 0x48, 0x63, 0x2B, 0x36, 0x5A, + 0x72, 0x39, 0x55, 0x68, 0x68, 0x63, 0x79, 0x4E, 0x5A, 0x6A, 0x67, 0x4B, 0x6E, 0x76, 0x45, 0x54, 0x71, 0x39, 0x45, 0x6D, 0x64, 0x0A, 0x38, 0x56, 0x52, 0x59, 0x2B, 0x57, 0x43, 0x76, 0x32, 0x68, 0x69, 0x6B, 0x4C, 0x79, 0x68, 0x46, 0x33, 0x48, + 0x71, 0x67, 0x69, 0x49, 0x5A, 0x64, 0x38, 0x7A, 0x76, 0x6E, 0x2F, 0x79, 0x6B, 0x31, 0x67, 0x50, 0x78, 0x6B, 0x51, 0x35, 0x54, 0x6D, 0x34, 0x78, 0x78, 0x76, 0x76, 0x71, 0x30, 0x4F, 0x4B, 0x6D, 0x4F, 0x5A, 0x4B, 0x38, 0x6C, 0x2B, 0x68, 0x66, + 0x5A, 0x78, 0x36, 0x41, 0x59, 0x44, 0x6C, 0x66, 0x37, 0x65, 0x6A, 0x30, 0x67, 0x63, 0x57, 0x74, 0x53, 0x53, 0x0A, 0x36, 0x43, 0x76, 0x75, 0x35, 0x7A, 0x48, 0x62, 0x75, 0x67, 0x52, 0x71, 0x68, 0x35, 0x6A, 0x6E, 0x78, 0x56, 0x2F, 0x76, 0x66, + 0x61, 0x63, 0x69, 0x39, 0x77, 0x48, 0x59, 0x54, 0x66, 0x6D, 0x4A, 0x30, 0x41, 0x36, 0x61, 0x42, 0x56, 0x6D, 0x6B, 0x6E, 0x70, 0x6A, 0x5A, 0x62, 0x79, 0x76, 0x4B, 0x63, 0x4C, 0x35, 0x6B, 0x77, 0x6C, 0x57, 0x6A, 0x39, 0x4F, 0x6D, 0x76, 0x77, + 0x35, 0x49, 0x70, 0x33, 0x49, 0x67, 0x57, 0x4A, 0x4A, 0x6B, 0x38, 0x6A, 0x53, 0x61, 0x59, 0x0A, 0x74, 0x6C, 0x75, 0x33, 0x7A, 0x4D, 0x36, 0x33, 0x4E, 0x77, 0x66, 0x39, 0x4A, 0x74, 0x6D, 0x59, 0x68, 0x53, 0x54, 0x2F, 0x57, 0x53, 0x4D, 0x44, + 0x6D, 0x75, 0x32, 0x64, 0x6E, 0x61, 0x6A, 0x6B, 0x58, 0x6A, 0x6A, 0x4F, 0x31, 0x31, 0x49, 0x4E, 0x62, 0x39, 0x49, 0x2F, 0x62, 0x62, 0x45, 0x46, 0x61, 0x30, 0x6E, 0x4F, 0x69, 0x70, 0x46, 0x47, 0x63, 0x2F, 0x54, 0x32, 0x4C, 0x2F, 0x43, 0x6F, + 0x63, 0x33, 0x63, 0x4F, 0x5A, 0x61, 0x79, 0x68, 0x6A, 0x57, 0x5A, 0x53, 0x0A, 0x61, 0x58, 0x35, 0x4C, 0x61, 0x41, 0x7A, 0x48, 0x48, 0x6A, 0x63, 0x6E, 0x67, 0x36, 0x57, 0x4D, 0x78, 0x77, 0x4C, 0x6B, 0x46, 0x4D, 0x31, 0x4A, 0x41, 0x62, 0x42, + 0x7A, 0x73, 0x2F, 0x33, 0x47, 0x6B, 0x44, 0x70, 0x76, 0x30, 0x6D, 0x7A, 0x74, 0x4F, 0x2B, 0x37, 0x73, 0x6B, 0x62, 0x36, 0x69, 0x51, 0x31, 0x32, 0x4C, 0x41, 0x45, 0x70, 0x6D, 0x4A, 0x55, 0x52, 0x77, 0x33, 0x6B, 0x41, 0x50, 0x2B, 0x48, 0x77, + 0x56, 0x39, 0x36, 0x4C, 0x4F, 0x50, 0x4E, 0x64, 0x65, 0x0A, 0x45, 0x34, 0x79, 0x42, 0x46, 0x78, 0x67, 0x58, 0x30, 0x62, 0x33, 0x78, 0x64, 0x78, 0x41, 0x36, 0x31, 0x47, 0x55, 0x35, 0x77, 0x53, 0x65, 0x73, 0x56, 0x79, 0x77, 0x6C, 0x56, 0x50, + 0x2B, 0x69, 0x32, 0x6B, 0x2B, 0x4B, 0x59, 0x54, 0x6C, 0x65, 0x72, 0x6A, 0x31, 0x4B, 0x6A, 0x4C, 0x30, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x65, 0x6D, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x47, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x6C, 0x44, + 0x43, 0x43, 0x41, 0x6E, 0x79, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4B, 0x4D, 0x66, 0x58, 0x6B, 0x59, 0x67, 0x78, 0x73, 0x57, 0x4F, 0x33, 0x57, 0x32, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x6E, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x4A, 0x54, 0x6A, 0x45, 0x54, 0x0A, 0x4D, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, + 0x78, 0x4D, 0x4B, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x42, 0x4C, 0x53, 0x54, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x63, 0x5A, 0x55, 0x31, 0x31, 0x5A, 0x47, 0x68, 0x79, 0x59, + 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x67, 0x54, 0x47, 0x6C, 0x74, 0x61, 0x58, 0x52, 0x6C, 0x0A, 0x5A, 0x44, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x41, 0x78, 0x4D, 0x54, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4C, 0x53, 0x42, 0x48, 0x4D, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4F, 0x44, 0x41, 0x79, + 0x4D, 0x54, 0x67, 0x78, 0x4F, 0x44, 0x4D, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x7A, 0x41, 0x79, 0x4D, 0x54, 0x67, 0x78, 0x0A, 0x4F, 0x44, 0x4D, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x47, 0x63, 0x78, 0x43, 0x7A, 0x41, + 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x6C, 0x4F, 0x4D, 0x52, 0x4D, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x77, 0x70, 0x6C, 0x62, 0x56, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x55, 0x45, 0x74, + 0x4A, 0x4D, 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x78, 0x6C, 0x54, 0x58, 0x56, 0x6B, 0x0A, 0x61, 0x48, 0x4A, 0x68, 0x49, 0x46, 0x52, 0x6C, 0x59, 0x32, 0x68, 0x75, 0x62, 0x32, 0x78, 0x76, 0x5A, 0x32, + 0x6C, 0x6C, 0x63, 0x79, 0x42, 0x4D, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x52, 0x77, 0x77, 0x47, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4E, 0x6C, 0x62, 0x56, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x55, 0x6D, + 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x45, 0x63, 0x78, 0x4D, 0x49, 0x49, 0x42, 0x0A, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, + 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x6B, 0x30, 0x75, 0x37, 0x36, 0x57, 0x61, 0x4B, 0x37, 0x70, 0x31, 0x62, 0x31, 0x54, 0x53, 0x54, 0x30, 0x42, 0x73, 0x65, 0x77, + 0x2B, 0x65, 0x65, 0x75, 0x47, 0x51, 0x7A, 0x66, 0x32, 0x4E, 0x34, 0x61, 0x4C, 0x54, 0x4E, 0x0A, 0x4C, 0x6E, 0x46, 0x31, 0x31, 0x35, 0x73, 0x67, 0x78, 0x6B, 0x30, 0x70, 0x76, 0x4C, 0x5A, 0x6F, 0x59, 0x49, 0x72, 0x33, 0x49, 0x5A, 0x70, 0x57, + 0x4E, 0x56, 0x72, 0x7A, 0x64, 0x72, 0x33, 0x59, 0x7A, 0x5A, 0x72, 0x2F, 0x6B, 0x31, 0x5A, 0x4C, 0x70, 0x56, 0x6B, 0x47, 0x6F, 0x5A, 0x4D, 0x30, 0x4B, 0x64, 0x30, 0x57, 0x4E, 0x48, 0x56, 0x4F, 0x38, 0x6F, 0x47, 0x30, 0x78, 0x35, 0x5A, 0x4F, + 0x72, 0x52, 0x6B, 0x56, 0x55, 0x6B, 0x72, 0x2B, 0x50, 0x48, 0x42, 0x31, 0x0A, 0x63, 0x4D, 0x32, 0x76, 0x4B, 0x36, 0x73, 0x56, 0x6D, 0x6A, 0x4D, 0x38, 0x71, 0x72, 0x4F, 0x4C, 0x71, 0x73, 0x31, 0x44, 0x2F, 0x66, 0x58, 0x71, 0x63, 0x50, 0x2F, + 0x74, 0x7A, 0x78, 0x45, 0x37, 0x6C, 0x4D, 0x35, 0x4F, 0x4D, 0x68, 0x62, 0x54, 0x49, 0x30, 0x41, 0x71, 0x64, 0x37, 0x4F, 0x76, 0x50, 0x41, 0x45, 0x73, 0x62, 0x4F, 0x32, 0x5A, 0x4C, 0x49, 0x76, 0x5A, 0x54, 0x6D, 0x6D, 0x59, 0x73, 0x76, 0x65, + 0x50, 0x51, 0x62, 0x41, 0x79, 0x65, 0x47, 0x48, 0x57, 0x0A, 0x44, 0x56, 0x2F, 0x44, 0x2B, 0x71, 0x4A, 0x41, 0x6B, 0x68, 0x31, 0x63, 0x46, 0x2B, 0x5A, 0x77, 0x50, 0x6A, 0x58, 0x6E, 0x6F, 0x72, 0x66, 0x43, 0x59, 0x75, 0x4B, 0x72, 0x70, 0x44, + 0x68, 0x4D, 0x74, 0x54, 0x6B, 0x31, 0x62, 0x2B, 0x6F, 0x44, 0x61, 0x66, 0x6F, 0x36, 0x56, 0x47, 0x69, 0x46, 0x62, 0x64, 0x62, 0x79, 0x4C, 0x30, 0x4E, 0x56, 0x48, 0x70, 0x45, 0x4E, 0x44, 0x74, 0x6A, 0x56, 0x61, 0x71, 0x53, 0x57, 0x30, 0x52, + 0x4D, 0x38, 0x4C, 0x48, 0x68, 0x51, 0x0A, 0x36, 0x44, 0x71, 0x53, 0x30, 0x68, 0x64, 0x57, 0x35, 0x54, 0x55, 0x61, 0x51, 0x42, 0x77, 0x2B, 0x6A, 0x53, 0x7A, 0x74, 0x4F, 0x64, 0x39, 0x43, 0x34, 0x49, 0x4E, 0x42, 0x64, 0x4E, 0x2B, 0x6A, 0x7A, + 0x63, 0x4B, 0x47, 0x59, 0x45, 0x68, 0x6F, 0x34, 0x32, 0x6B, 0x4C, 0x56, 0x41, 0x43, 0x4C, 0x35, 0x48, 0x5A, 0x70, 0x49, 0x51, 0x31, 0x35, 0x54, 0x6A, 0x51, 0x49, 0x58, 0x68, 0x54, 0x43, 0x7A, 0x4C, 0x47, 0x33, 0x72, 0x64, 0x64, 0x38, 0x63, + 0x49, 0x72, 0x48, 0x0A, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x2B, 0x2B, 0x38, 0x4E, 0x68, 0x70, 0x36, 0x77, + 0x34, 0x39, 0x32, 0x70, 0x75, 0x66, 0x45, 0x68, 0x46, 0x33, 0x38, 0x2B, 0x2F, 0x50, 0x42, 0x33, 0x4B, 0x78, 0x6F, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, + 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, + 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x46, 0x6E, 0x2F, 0x38, 0x6F, 0x7A, 0x31, 0x68, 0x33, 0x31, 0x78, 0x50, 0x61, 0x4F, 0x66, 0x47, 0x31, 0x76, 0x52, 0x32, 0x76, 0x6A, 0x54, 0x6E, 0x47, 0x73, 0x32, 0x0A, 0x76, 0x5A, + 0x75, 0x70, 0x59, 0x65, 0x76, 0x65, 0x46, 0x69, 0x78, 0x30, 0x50, 0x5A, 0x37, 0x6D, 0x64, 0x64, 0x72, 0x58, 0x75, 0x71, 0x65, 0x38, 0x51, 0x68, 0x66, 0x6E, 0x50, 0x5A, 0x48, 0x72, 0x35, 0x58, 0x33, 0x64, 0x50, 0x70, 0x7A, 0x78, 0x7A, 0x35, + 0x4B, 0x73, 0x62, 0x45, 0x6A, 0x4D, 0x77, 0x69, 0x49, 0x2F, 0x61, 0x54, 0x76, 0x46, 0x74, 0x68, 0x55, 0x76, 0x6F, 0x7A, 0x58, 0x47, 0x61, 0x43, 0x6F, 0x63, 0x56, 0x36, 0x38, 0x35, 0x37, 0x34, 0x33, 0x51, 0x0A, 0x4E, 0x63, 0x4D, 0x59, 0x44, + 0x48, 0x73, 0x41, 0x56, 0x68, 0x7A, 0x4E, 0x69, 0x78, 0x6C, 0x30, 0x33, 0x72, 0x34, 0x50, 0x45, 0x75, 0x44, 0x51, 0x71, 0x71, 0x45, 0x2F, 0x41, 0x6A, 0x53, 0x78, 0x63, 0x4D, 0x36, 0x64, 0x47, 0x4E, 0x59, 0x49, 0x41, 0x77, 0x6C, 0x47, 0x37, + 0x6D, 0x44, 0x67, 0x66, 0x72, 0x62, 0x45, 0x53, 0x51, 0x52, 0x52, 0x66, 0x58, 0x42, 0x67, 0x76, 0x4B, 0x71, 0x79, 0x2F, 0x33, 0x6C, 0x79, 0x65, 0x71, 0x59, 0x64, 0x50, 0x56, 0x38, 0x71, 0x0A, 0x2B, 0x4D, 0x72, 0x69, 0x2F, 0x54, 0x6D, 0x33, + 0x52, 0x37, 0x6E, 0x72, 0x66, 0x74, 0x38, 0x45, 0x49, 0x36, 0x2F, 0x36, 0x6E, 0x41, 0x59, 0x48, 0x36, 0x66, 0x74, 0x6A, 0x6B, 0x34, 0x42, 0x41, 0x74, 0x63, 0x5A, 0x73, 0x43, 0x6A, 0x45, 0x6F, 0x7A, 0x67, 0x79, 0x66, 0x7A, 0x37, 0x4D, 0x6A, + 0x4E, 0x59, 0x42, 0x42, 0x6A, 0x57, 0x7A, 0x45, 0x4E, 0x33, 0x75, 0x42, 0x4C, 0x34, 0x43, 0x68, 0x51, 0x45, 0x4B, 0x46, 0x36, 0x64, 0x6B, 0x34, 0x6A, 0x65, 0x69, 0x68, 0x0A, 0x55, 0x38, 0x30, 0x42, 0x76, 0x32, 0x6E, 0x6F, 0x57, 0x67, 0x62, + 0x79, 0x52, 0x51, 0x75, 0x51, 0x2B, 0x71, 0x37, 0x68, 0x76, 0x35, 0x33, 0x79, 0x72, 0x6C, 0x63, 0x38, 0x70, 0x61, 0x36, 0x79, 0x56, 0x76, 0x53, 0x4C, 0x5A, 0x55, 0x44, 0x70, 0x2F, 0x54, 0x47, 0x42, 0x4C, 0x50, 0x51, 0x35, 0x43, 0x64, 0x6A, + 0x75, 0x61, 0x36, 0x65, 0x30, 0x70, 0x68, 0x30, 0x56, 0x70, 0x5A, 0x6A, 0x33, 0x41, 0x59, 0x48, 0x59, 0x68, 0x58, 0x33, 0x7A, 0x55, 0x56, 0x78, 0x78, 0x0A, 0x69, 0x4E, 0x36, 0x36, 0x7A, 0x42, 0x2B, 0x41, 0x66, 0x6B, 0x6F, 0x3D, 0x0A, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x65, 0x6D, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, + 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, + 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x54, 0x6A, 0x43, 0x43, 0x41, 0x64, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x4B, 0x50, 0x50, 0x59, 0x48, 0x71, 0x57, 0x68, 0x77, 0x44, 0x74, 0x71, 0x4C, 0x68, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x72, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x4A, 0x54, 0x6A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x4B, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x42, 0x4C, 0x53, + 0x54, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x63, 0x5A, 0x55, 0x31, 0x31, 0x5A, 0x47, 0x68, 0x79, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, + 0x58, 0x4D, 0x67, 0x54, 0x47, 0x6C, 0x74, 0x61, 0x58, 0x52, 0x6C, 0x5A, 0x44, 0x45, 0x67, 0x0A, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x58, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x45, 0x56, 0x44, + 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x52, 0x7A, 0x4D, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x67, 0x77, 0x4D, 0x6A, 0x45, 0x34, 0x4D, 0x54, 0x67, 0x7A, 0x4D, 0x44, 0x41, 0x77, + 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x4D, 0x77, 0x4D, 0x6A, 0x45, 0x34, 0x0A, 0x4D, 0x54, 0x67, 0x7A, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x72, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, + 0x4A, 0x54, 0x6A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x4B, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x42, 0x4C, 0x53, 0x54, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x43, 0x68, 0x4D, 0x63, 0x5A, 0x55, 0x31, 0x31, 0x0A, 0x5A, 0x47, 0x68, 0x79, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x67, 0x54, 0x47, 0x6C, 0x74, 0x61, 0x58, + 0x52, 0x6C, 0x5A, 0x44, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x58, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, + 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x0A, 0x52, 0x7A, 0x4D, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, + 0x41, 0x51, 0x6A, 0x70, 0x51, 0x79, 0x34, 0x4C, 0x52, 0x4C, 0x31, 0x4B, 0x50, 0x4F, 0x78, 0x73, 0x74, 0x33, 0x69, 0x41, 0x68, 0x4B, 0x41, 0x6E, 0x6A, 0x6C, 0x66, 0x53, 0x55, 0x32, 0x66, 0x79, 0x53, 0x55, 0x30, 0x57, 0x58, 0x54, 0x73, 0x75, + 0x77, 0x59, 0x63, 0x0A, 0x35, 0x38, 0x42, 0x79, 0x72, 0x2B, 0x69, 0x75, 0x4C, 0x2B, 0x46, 0x42, 0x56, 0x49, 0x63, 0x55, 0x71, 0x45, 0x71, 0x79, 0x36, 0x48, 0x79, 0x43, 0x35, 0x6C, 0x74, 0x71, 0x74, 0x64, 0x79, 0x7A, 0x64, 0x63, 0x36, 0x4C, + 0x42, 0x74, 0x43, 0x47, 0x49, 0x37, 0x39, 0x47, 0x31, 0x59, 0x34, 0x50, 0x50, 0x77, 0x54, 0x30, 0x31, 0x78, 0x79, 0x53, 0x66, 0x76, 0x61, 0x6C, 0x59, 0x38, 0x4C, 0x31, 0x58, 0x34, 0x34, 0x75, 0x54, 0x36, 0x45, 0x59, 0x47, 0x51, 0x49, 0x72, + 0x0A, 0x4D, 0x67, 0x71, 0x43, 0x5A, 0x48, 0x30, 0x57, 0x6B, 0x39, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x38, 0x58, 0x51, 0x4B, 0x45, 0x45, 0x39, 0x54, + 0x4D, 0x69, 0x70, 0x75, 0x42, 0x7A, 0x68, 0x63, 0x63, 0x4C, 0x69, 0x6B, 0x65, 0x6E, 0x45, 0x68, 0x6A, 0x51, 0x6A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x0A, 0x41, 0x51, + 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, + 0x4E, 0x70, 0x41, 0x44, 0x42, 0x6D, 0x41, 0x6A, 0x45, 0x41, 0x76, 0x76, 0x4E, 0x68, 0x7A, 0x77, 0x49, 0x51, 0x48, 0x57, 0x53, 0x56, 0x42, 0x37, 0x67, 0x59, 0x62, 0x6F, 0x69, 0x46, 0x42, 0x53, 0x2B, 0x44, 0x0A, 0x43, 0x42, 0x65, 0x51, 0x79, + 0x68, 0x2B, 0x4B, 0x54, 0x4F, 0x67, 0x4E, 0x47, 0x33, 0x71, 0x78, 0x72, 0x64, 0x57, 0x42, 0x43, 0x55, 0x66, 0x76, 0x4F, 0x36, 0x77, 0x49, 0x42, 0x48, 0x78, 0x63, 0x6D, 0x62, 0x48, 0x74, 0x52, 0x77, 0x66, 0x53, 0x41, 0x6A, 0x45, 0x41, 0x6E, + 0x62, 0x70, 0x56, 0x2F, 0x4B, 0x6C, 0x4B, 0x36, 0x4F, 0x33, 0x74, 0x35, 0x6E, 0x59, 0x42, 0x51, 0x6E, 0x76, 0x49, 0x2B, 0x47, 0x44, 0x5A, 0x6A, 0x56, 0x47, 0x4C, 0x56, 0x54, 0x76, 0x37, 0x0A, 0x6A, 0x48, 0x76, 0x72, 0x5A, 0x51, 0x6E, 0x44, + 0x2B, 0x4A, 0x62, 0x4E, 0x52, 0x36, 0x69, 0x43, 0x38, 0x68, 0x5A, 0x56, 0x64, 0x79, 0x52, 0x2B, 0x45, 0x68, 0x43, 0x56, 0x42, 0x43, 0x79, 0x6A, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x65, 0x6D, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x43, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x4D, 0x49, 0x49, 0x44, 0x63, 0x7A, 0x43, 0x43, 0x41, 0x6C, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4C, 0x41, 0x4B, 0x37, 0x50, 0x41, 0x4C, 0x72, 0x45, 0x7A, 0x7A, 0x4C, 0x34, 0x51, 0x37, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, + 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x56, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x0A, 0x45, 0x7A, 0x41, + 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x43, 0x6D, 0x56, 0x74, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x51, 0x53, 0x30, 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, 0x32, 0x56, + 0x4E, 0x64, 0x57, 0x52, 0x6F, 0x63, 0x6D, 0x45, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x77, 0x77, 0x47, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4E, 0x6C, 0x62, 0x56, 0x4E, 0x70, 0x0A, 0x5A, 0x32, 0x34, 0x67, 0x55, 0x6D, + 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x45, 0x4D, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x34, 0x4D, 0x44, 0x49, 0x78, 0x4F, 0x44, 0x45, 0x34, 0x4D, 0x7A, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, + 0x51, 0x7A, 0x4D, 0x44, 0x49, 0x78, 0x4F, 0x44, 0x45, 0x34, 0x4D, 0x7A, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x56, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, + 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x43, 0x6D, 0x56, 0x74, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x51, 0x53, 0x30, 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, + 0x32, 0x56, 0x4E, 0x64, 0x57, 0x52, 0x6F, 0x63, 0x6D, 0x45, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x52, 0x77, 0x77, 0x47, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x45, 0x78, 0x4E, 0x6C, 0x62, 0x56, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, + 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x45, 0x4D, 0x78, 0x4D, 0x49, 0x49, 0x42, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, + 0x41, 0x41, 0x4F, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x51, 0x45, 0x41, 0x7A, 0x2B, 0x75, 0x70, 0x0A, 0x75, 0x66, 0x47, 0x5A, 0x42, 0x63, 0x7A, 0x59, 0x4B, 0x43, 0x46, 0x4B, 0x38, 0x33, 0x4D, + 0x30, 0x55, 0x59, 0x52, 0x57, 0x45, 0x50, 0x57, 0x67, 0x54, 0x79, 0x77, 0x53, 0x34, 0x2F, 0x6F, 0x54, 0x6D, 0x69, 0x66, 0x51, 0x7A, 0x2F, 0x6C, 0x35, 0x47, 0x6E, 0x52, 0x66, 0x48, 0x58, 0x6B, 0x35, 0x2F, 0x46, 0x76, 0x34, 0x63, 0x49, 0x37, + 0x67, 0x6B, 0x6C, 0x4C, 0x33, 0x35, 0x43, 0x58, 0x35, 0x56, 0x49, 0x50, 0x5A, 0x48, 0x64, 0x50, 0x49, 0x57, 0x6F, 0x55, 0x2F, 0x0A, 0x58, 0x73, 0x65, 0x32, 0x42, 0x2B, 0x34, 0x2B, 0x77, 0x4D, 0x36, 0x61, 0x72, 0x36, 0x78, 0x57, 0x51, 0x69, + 0x6F, 0x35, 0x4A, 0x58, 0x44, 0x57, 0x76, 0x37, 0x56, 0x37, 0x4E, 0x71, 0x32, 0x73, 0x39, 0x6E, 0x50, 0x63, 0x7A, 0x64, 0x63, 0x64, 0x69, 0x6F, 0x4F, 0x6C, 0x2B, 0x79, 0x75, 0x51, 0x46, 0x54, 0x64, 0x72, 0x48, 0x43, 0x5A, 0x48, 0x33, 0x44, + 0x73, 0x70, 0x56, 0x70, 0x4E, 0x71, 0x73, 0x38, 0x46, 0x71, 0x4F, 0x70, 0x30, 0x39, 0x39, 0x63, 0x47, 0x58, 0x0A, 0x4F, 0x46, 0x67, 0x46, 0x69, 0x78, 0x77, 0x52, 0x34, 0x2B, 0x53, 0x30, 0x75, 0x46, 0x32, 0x46, 0x48, 0x59, 0x50, 0x2B, 0x65, + 0x46, 0x38, 0x4C, 0x52, 0x57, 0x67, 0x59, 0x53, 0x4B, 0x56, 0x47, 0x63, 0x7A, 0x51, 0x37, 0x2F, 0x67, 0x2F, 0x49, 0x64, 0x72, 0x76, 0x48, 0x47, 0x50, 0x4D, 0x46, 0x30, 0x59, 0x62, 0x7A, 0x68, 0x65, 0x33, 0x6E, 0x75, 0x64, 0x6B, 0x79, 0x72, + 0x56, 0x57, 0x49, 0x7A, 0x71, 0x61, 0x32, 0x6B, 0x62, 0x42, 0x50, 0x72, 0x48, 0x34, 0x56, 0x0A, 0x49, 0x35, 0x62, 0x32, 0x50, 0x2F, 0x41, 0x67, 0x4E, 0x42, 0x62, 0x65, 0x43, 0x73, 0x62, 0x45, 0x42, 0x45, 0x56, 0x35, 0x66, 0x36, 0x66, 0x39, + 0x76, 0x74, 0x4B, 0x70, 0x70, 0x61, 0x2B, 0x63, 0x78, 0x53, 0x4D, 0x71, 0x39, 0x7A, 0x77, 0x68, 0x62, 0x4C, 0x32, 0x76, 0x6A, 0x30, 0x37, 0x46, 0x4F, 0x72, 0x4C, 0x7A, 0x4E, 0x42, 0x4C, 0x38, 0x33, 0x34, 0x41, 0x61, 0x53, 0x61, 0x54, 0x55, + 0x71, 0x5A, 0x58, 0x33, 0x6E, 0x6F, 0x6C, 0x65, 0x6F, 0x6F, 0x6D, 0x73, 0x0A, 0x6C, 0x4D, 0x75, 0x6F, 0x61, 0x4A, 0x75, 0x76, 0x69, 0x6D, 0x55, 0x6E, 0x7A, 0x59, 0x6E, 0x75, 0x33, 0x59, 0x79, 0x31, 0x61, 0x79, 0x6C, 0x77, 0x51, 0x36, 0x42, + 0x70, 0x43, 0x2B, 0x53, 0x35, 0x44, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x2F, 0x71, 0x48, 0x67, 0x63, 0x42, 0x34, + 0x71, 0x41, 0x7A, 0x6C, 0x53, 0x57, 0x6B, 0x4B, 0x2B, 0x0A, 0x58, 0x4A, 0x47, 0x46, 0x65, 0x68, 0x69, 0x71, 0x54, 0x62, 0x55, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, + 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, + 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x0A, 0x67, 0x67, 0x45, 0x42, 0x41, 0x4D, 0x4A, 0x4B, 0x56, 0x76, 0x6F, 0x56, 0x49, 0x58, 0x73, 0x6F, 0x6F, 0x75, 0x6E, 0x6C, 0x48, 0x66, 0x76, 0x34, 0x4C, 0x63, 0x51, 0x35, 0x6C, 0x6B, 0x46, 0x4D, 0x4F, + 0x79, 0x63, 0x73, 0x78, 0x47, 0x77, 0x59, 0x46, 0x59, 0x44, 0x47, 0x72, 0x4B, 0x39, 0x48, 0x57, 0x53, 0x38, 0x6D, 0x43, 0x2B, 0x4D, 0x32, 0x73, 0x4F, 0x38, 0x37, 0x2F, 0x6B, 0x4F, 0x58, 0x53, 0x54, 0x4B, 0x5A, 0x45, 0x68, 0x56, 0x62, 0x33, + 0x78, 0x45, 0x70, 0x0A, 0x2F, 0x36, 0x74, 0x54, 0x2B, 0x4C, 0x76, 0x42, 0x65, 0x41, 0x2B, 0x73, 0x6E, 0x46, 0x4F, 0x76, 0x56, 0x37, 0x31, 0x6F, 0x6A, 0x44, 0x31, 0x70, 0x4D, 0x2F, 0x43, 0x6A, 0x6F, 0x43, 0x4E, 0x6A, 0x4F, 0x32, 0x52, 0x6E, + 0x49, 0x6B, 0x53, 0x74, 0x31, 0x58, 0x48, 0x4C, 0x56, 0x69, 0x70, 0x34, 0x6B, 0x71, 0x4E, 0x50, 0x45, 0x6A, 0x45, 0x32, 0x4E, 0x75, 0x4C, 0x65, 0x2F, 0x67, 0x44, 0x45, 0x6F, 0x32, 0x41, 0x50, 0x4A, 0x36, 0x32, 0x67, 0x73, 0x49, 0x71, 0x31, + 0x0A, 0x4E, 0x6E, 0x70, 0x53, 0x6F, 0x62, 0x30, 0x6E, 0x39, 0x43, 0x41, 0x6E, 0x59, 0x75, 0x68, 0x4E, 0x6C, 0x43, 0x51, 0x54, 0x35, 0x41, 0x6F, 0x45, 0x36, 0x54, 0x79, 0x72, 0x4C, 0x73, 0x68, 0x44, 0x43, 0x55, 0x72, 0x47, 0x59, 0x51, 0x54, + 0x6C, 0x53, 0x54, 0x52, 0x2B, 0x30, 0x38, 0x54, 0x49, 0x39, 0x51, 0x2F, 0x41, 0x71, 0x75, 0x6D, 0x36, 0x56, 0x46, 0x37, 0x7A, 0x59, 0x79, 0x74, 0x50, 0x54, 0x31, 0x44, 0x55, 0x2F, 0x72, 0x6C, 0x37, 0x6D, 0x59, 0x77, 0x39, 0x0A, 0x77, 0x43, + 0x36, 0x38, 0x41, 0x69, 0x76, 0x54, 0x78, 0x45, 0x44, 0x6B, 0x69, 0x67, 0x63, 0x78, 0x48, 0x70, 0x76, 0x4F, 0x4A, 0x70, 0x6B, 0x54, 0x2B, 0x78, 0x48, 0x71, 0x6D, 0x69, 0x49, 0x4D, 0x45, 0x52, 0x6E, 0x48, 0x58, 0x68, 0x75, 0x42, 0x55, 0x44, + 0x44, 0x49, 0x6C, 0x68, 0x4A, 0x75, 0x35, 0x38, 0x74, 0x42, 0x66, 0x35, 0x45, 0x37, 0x6F, 0x6B, 0x65, 0x33, 0x56, 0x49, 0x41, 0x62, 0x33, 0x41, 0x44, 0x4D, 0x6D, 0x70, 0x44, 0x71, 0x77, 0x38, 0x4E, 0x51, 0x0A, 0x42, 0x6D, 0x49, 0x4D, 0x4D, + 0x4D, 0x41, 0x56, 0x53, 0x4B, 0x65, 0x6F, 0x57, 0x58, 0x7A, 0x68, 0x72, 0x69, 0x4B, 0x69, 0x34, 0x67, 0x70, 0x36, 0x44, 0x2F, 0x70, 0x69, 0x71, 0x31, 0x4A, 0x4D, 0x34, 0x66, 0x48, 0x66, 0x79, 0x72, 0x36, 0x44, 0x44, 0x55, 0x49, 0x3D, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x65, 0x6D, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x43, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4B, 0x7A, 0x43, 0x43, 0x41, 0x62, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, + 0x67, 0x49, 0x4B, 0x65, 0x33, 0x47, 0x32, 0x67, 0x6C, 0x61, 0x34, 0x45, 0x6E, 0x79, 0x63, 0x71, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x61, 0x4D, 0x51, 0x73, 0x77, 0x43, + 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x54, 0x4D, 0x42, 0x45, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x4B, 0x5A, 0x57, 0x31, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x42, 0x4C, + 0x53, 0x54, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4C, 0x5A, 0x55, 0x31, 0x31, 0x5A, 0x47, 0x68, 0x79, 0x59, 0x53, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x78, 0x49, 0x44, 0x41, 0x65, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x4D, 0x54, 0x46, 0x32, 0x56, 0x74, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x46, 0x0A, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x45, 0x4D, 0x7A, 0x4D, 0x42, 0x34, + 0x58, 0x44, 0x54, 0x45, 0x34, 0x4D, 0x44, 0x49, 0x78, 0x4F, 0x44, 0x45, 0x34, 0x4D, 0x7A, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x7A, 0x4D, 0x44, 0x49, 0x78, 0x4F, 0x44, 0x45, 0x34, 0x4D, 0x7A, 0x41, 0x77, 0x4D, 0x46, 0x6F, + 0x77, 0x57, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x54, 0x43, 0x6D, 0x56, 0x74, 0x55, 0x32, + 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x51, 0x53, 0x30, 0x6B, 0x78, 0x46, 0x44, 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, 0x32, 0x56, 0x4E, 0x64, 0x57, 0x52, 0x6F, 0x63, 0x6D, 0x45, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4D, 0x53, + 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x45, 0x78, 0x64, 0x6C, 0x62, 0x56, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4C, + 0x53, 0x42, 0x44, 0x4D, 0x7A, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x50, 0x32, 0x6C, 0x59, + 0x61, 0x35, 0x37, 0x4A, 0x68, 0x41, 0x64, 0x0A, 0x36, 0x62, 0x63, 0x69, 0x4D, 0x4B, 0x34, 0x47, 0x39, 0x49, 0x47, 0x7A, 0x73, 0x55, 0x4A, 0x78, 0x6C, 0x54, 0x6D, 0x38, 0x30, 0x31, 0x4C, 0x6A, 0x72, 0x36, 0x2F, 0x35, 0x38, 0x70, 0x63, 0x31, + 0x6B, 0x6A, 0x5A, 0x47, 0x44, 0x6F, 0x65, 0x56, 0x6A, 0x62, 0x6B, 0x35, 0x57, 0x75, 0x6D, 0x37, 0x33, 0x39, 0x44, 0x2B, 0x79, 0x41, 0x64, 0x42, 0x50, 0x4C, 0x74, 0x56, 0x62, 0x34, 0x4F, 0x6A, 0x61, 0x76, 0x74, 0x69, 0x73, 0x49, 0x47, 0x4A, + 0x41, 0x6E, 0x42, 0x39, 0x0A, 0x53, 0x4D, 0x56, 0x4B, 0x34, 0x2B, 0x6B, 0x69, 0x56, 0x43, 0x4A, 0x4E, 0x6B, 0x37, 0x74, 0x43, 0x44, 0x4B, 0x39, 0x33, 0x6E, 0x43, 0x4F, 0x6D, 0x66, 0x64, 0x64, 0x68, 0x45, 0x63, 0x35, 0x6C, 0x78, 0x2F, 0x68, + 0x2F, 0x2F, 0x76, 0x58, 0x79, 0x71, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x50, 0x74, 0x61, 0x53, 0x4E, 0x43, 0x41, 0x49, 0x45, 0x44, 0x79, 0x71, 0x4F, 0x6B, + 0x41, 0x0A, 0x42, 0x32, 0x6B, 0x5A, 0x64, 0x36, 0x66, 0x6D, 0x77, 0x2F, 0x54, 0x50, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x50, 0x42, 0x67, + 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x67, 0x41, 0x0A, 0x4D, + 0x47, 0x55, 0x43, 0x4D, 0x51, 0x43, 0x30, 0x32, 0x43, 0x38, 0x43, 0x69, 0x66, 0x32, 0x32, 0x54, 0x47, 0x4B, 0x36, 0x51, 0x30, 0x34, 0x54, 0x68, 0x48, 0x4B, 0x31, 0x72, 0x74, 0x30, 0x63, 0x33, 0x74, 0x61, 0x31, 0x33, 0x46, 0x61, 0x50, 0x57, + 0x45, 0x42, 0x61, 0x4C, 0x64, 0x34, 0x67, 0x54, 0x43, 0x4B, 0x44, 0x79, 0x70, 0x4F, 0x6F, 0x66, 0x75, 0x34, 0x53, 0x51, 0x4D, 0x66, 0x57, 0x68, 0x30, 0x2F, 0x34, 0x33, 0x34, 0x55, 0x43, 0x4D, 0x42, 0x77, 0x55, 0x0A, 0x5A, 0x4F, 0x52, 0x38, + 0x6C, 0x6F, 0x4D, 0x52, 0x6E, 0x4C, 0x44, 0x52, 0x57, 0x6D, 0x46, 0x4C, 0x70, 0x67, 0x39, 0x4A, 0x30, 0x77, 0x44, 0x38, 0x6F, 0x66, 0x7A, 0x6B, 0x70, 0x66, 0x39, 0x2F, 0x72, 0x64, 0x63, 0x77, 0x30, 0x4D, 0x64, 0x33, 0x66, 0x37, 0x36, 0x42, + 0x42, 0x31, 0x55, 0x77, 0x55, 0x43, 0x41, 0x55, 0x39, 0x56, 0x63, 0x34, 0x43, 0x71, 0x67, 0x78, 0x55, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x48, 0x6F, 0x6E, 0x67, 0x6B, 0x6F, 0x6E, 0x67, 0x20, 0x50, 0x6F, 0x73, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x7A, 0x7A, 0x43, 0x43, 0x41, 0x37, 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x43, 0x42, 0x5A, 0x66, 0x69, 0x6B, 0x79, 0x6C, 0x37, 0x41, 0x44, 0x4A, 0x6B, 0x30, 0x44, 0x66, + 0x78, 0x4D, 0x61, 0x75, 0x49, 0x37, 0x67, 0x63, 0x57, 0x71, 0x51, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x62, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, + 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x45, 0x73, 0x78, 0x45, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x67, 0x54, 0x43, 0x55, 0x68, 0x76, 0x62, 0x6D, 0x63, 0x67, 0x53, 0x32, 0x39, 0x75, 0x5A, 0x7A, 0x45, + 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x78, 0x4D, 0x4A, 0x53, 0x47, 0x39, 0x75, 0x5A, 0x79, 0x42, 0x4C, 0x62, 0x32, 0x35, 0x6E, 0x4D, 0x52, 0x59, 0x77, 0x46, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x45, 0x77, + 0x31, 0x49, 0x62, 0x32, 0x35, 0x6E, 0x61, 0x32, 0x39, 0x75, 0x5A, 0x79, 0x42, 0x51, 0x62, 0x33, 0x4E, 0x30, 0x4D, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x64, 0x49, 0x62, 0x32, 0x35, 0x6E, 0x61, 0x32, + 0x39, 0x75, 0x5A, 0x79, 0x42, 0x51, 0x62, 0x33, 0x4E, 0x30, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x32, 0x0A, 0x4D, 0x44, 0x4D, 0x77, 0x4D, + 0x6A, 0x49, 0x35, 0x4E, 0x44, 0x5A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x32, 0x4D, 0x44, 0x4D, 0x77, 0x4D, 0x6A, 0x49, 0x35, 0x4E, 0x44, 0x5A, 0x61, 0x4D, 0x47, 0x38, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x59, 0x54, 0x41, 0x6B, 0x68, 0x4C, 0x4D, 0x52, 0x49, 0x77, 0x45, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x49, 0x45, 0x77, 0x6C, 0x49, 0x62, 0x32, 0x35, 0x6E, 0x49, 0x45, 0x74, 0x76, 0x0A, 0x62, 0x6D, 0x63, 0x78, 0x45, 0x6A, 0x41, 0x51, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x43, 0x55, 0x68, 0x76, 0x62, 0x6D, 0x63, 0x67, 0x53, 0x32, 0x39, 0x75, 0x5A, 0x7A, 0x45, 0x57, 0x4D, 0x42, 0x51, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4E, 0x53, 0x47, 0x39, 0x75, + 0x5A, 0x32, 0x74, 0x76, 0x62, 0x6D, 0x63, 0x67, 0x55, 0x47, 0x39, 0x7A, 0x64, 0x44, 0x45, 0x67, 0x4D, 0x42, 0x34, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x58, 0x0A, 0x53, 0x47, 0x39, 0x75, 0x5A, 0x32, 0x74, 0x76, 0x62, 0x6D, 0x63, + 0x67, 0x55, 0x47, 0x39, 0x7A, 0x64, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x4D, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, + 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x7A, 0x0A, 0x69, 0x4E, 0x66, 0x71, 0x7A, 0x67, 0x38, 0x67, 0x54, 0x72, 0x37, 0x6D, 0x31, 0x67, + 0x4E, 0x74, 0x37, 0x6C, 0x6E, 0x38, 0x77, 0x6C, 0x66, 0x66, 0x4B, 0x57, 0x69, 0x68, 0x67, 0x77, 0x34, 0x2B, 0x61, 0x4D, 0x64, 0x6F, 0x57, 0x4A, 0x77, 0x63, 0x59, 0x45, 0x75, 0x4A, 0x51, 0x77, 0x79, 0x35, 0x31, 0x42, 0x57, 0x79, 0x37, 0x73, + 0x46, 0x4F, 0x64, 0x65, 0x6D, 0x31, 0x70, 0x2B, 0x2F, 0x6C, 0x36, 0x54, 0x57, 0x5A, 0x35, 0x4D, 0x77, 0x63, 0x35, 0x30, 0x74, 0x66, 0x0A, 0x6A, 0x54, 0x4D, 0x77, 0x49, 0x44, 0x4E, 0x54, 0x32, 0x61, 0x61, 0x37, 0x31, 0x54, 0x34, 0x54, 0x6A, + 0x75, 0x6B, 0x66, 0x68, 0x30, 0x6D, 0x74, 0x55, 0x43, 0x31, 0x51, 0x79, 0x68, 0x69, 0x2B, 0x41, 0x56, 0x69, 0x69, 0x45, 0x33, 0x43, 0x57, 0x75, 0x34, 0x6D, 0x49, 0x56, 0x6F, 0x42, 0x63, 0x2B, 0x4C, 0x30, 0x73, 0x50, 0x4F, 0x46, 0x4D, 0x56, + 0x34, 0x69, 0x37, 0x30, 0x37, 0x6D, 0x56, 0x37, 0x38, 0x76, 0x48, 0x39, 0x74, 0x6F, 0x78, 0x64, 0x43, 0x69, 0x6D, 0x0A, 0x35, 0x6C, 0x53, 0x4A, 0x39, 0x55, 0x45, 0x78, 0x79, 0x75, 0x55, 0x6D, 0x47, 0x73, 0x32, 0x43, 0x34, 0x48, 0x44, 0x61, + 0x4F, 0x79, 0x6D, 0x37, 0x31, 0x51, 0x50, 0x31, 0x6D, 0x62, 0x70, 0x56, 0x39, 0x57, 0x54, 0x52, 0x59, 0x41, 0x36, 0x7A, 0x69, 0x55, 0x6D, 0x34, 0x69, 0x69, 0x38, 0x46, 0x30, 0x6F, 0x52, 0x46, 0x4B, 0x48, 0x79, 0x50, 0x61, 0x46, 0x41, 0x53, + 0x65, 0x50, 0x77, 0x4C, 0x74, 0x56, 0x50, 0x4C, 0x77, 0x70, 0x67, 0x63, 0x68, 0x4B, 0x4F, 0x65, 0x0A, 0x73, 0x4C, 0x34, 0x6A, 0x70, 0x4E, 0x72, 0x63, 0x79, 0x43, 0x73, 0x65, 0x32, 0x6D, 0x35, 0x46, 0x48, 0x6F, 0x6D, 0x59, 0x32, 0x76, 0x6B, + 0x41, 0x4C, 0x67, 0x62, 0x70, 0x44, 0x44, 0x74, 0x77, 0x31, 0x56, 0x41, 0x6C, 0x69, 0x4A, 0x6E, 0x4C, 0x7A, 0x58, 0x4E, 0x67, 0x39, 0x39, 0x58, 0x2F, 0x4E, 0x57, 0x66, 0x46, 0x6F, 0x62, 0x78, 0x65, 0x71, 0x38, 0x31, 0x4B, 0x75, 0x45, 0x58, + 0x72, 0x79, 0x47, 0x67, 0x65, 0x44, 0x51, 0x30, 0x55, 0x52, 0x68, 0x4C, 0x6A, 0x0A, 0x30, 0x6D, 0x52, 0x69, 0x69, 0x6B, 0x4B, 0x59, 0x76, 0x4C, 0x54, 0x47, 0x43, 0x41, 0x6A, 0x34, 0x2F, 0x61, 0x68, 0x4D, 0x5A, 0x4A, 0x78, 0x32, 0x41, 0x62, + 0x30, 0x76, 0x71, 0x57, 0x77, 0x7A, 0x44, 0x39, 0x67, 0x2F, 0x4B, 0x4C, 0x67, 0x38, 0x61, 0x51, 0x46, 0x43, 0x68, 0x6E, 0x35, 0x70, 0x77, 0x63, 0x6B, 0x47, 0x79, 0x75, 0x56, 0x36, 0x52, 0x6D, 0x58, 0x70, 0x77, 0x74, 0x5A, 0x51, 0x51, 0x53, + 0x34, 0x2F, 0x74, 0x2B, 0x54, 0x74, 0x62, 0x4E, 0x65, 0x2F, 0x0A, 0x4A, 0x67, 0x45, 0x52, 0x6F, 0x68, 0x59, 0x70, 0x53, 0x6D, 0x73, 0x30, 0x42, 0x70, 0x44, 0x73, 0x45, 0x39, 0x4B, 0x32, 0x2B, 0x32, 0x70, 0x32, 0x30, 0x6A, 0x7A, 0x74, 0x38, + 0x4E, 0x59, 0x74, 0x33, 0x65, 0x45, 0x56, 0x37, 0x4B, 0x4F, 0x62, 0x4C, 0x79, 0x7A, 0x4A, 0x50, 0x69, 0x76, 0x6B, 0x61, 0x54, 0x76, 0x2F, 0x63, 0x69, 0x57, 0x78, 0x4E, 0x6F, 0x5A, 0x62, 0x78, 0x33, 0x39, 0x72, 0x69, 0x31, 0x55, 0x62, 0x53, + 0x73, 0x55, 0x67, 0x59, 0x54, 0x32, 0x75, 0x0A, 0x79, 0x31, 0x44, 0x68, 0x43, 0x44, 0x71, 0x2B, 0x73, 0x49, 0x39, 0x6A, 0x51, 0x56, 0x4D, 0x77, 0x43, 0x46, 0x6B, 0x38, 0x6D, 0x42, 0x31, 0x33, 0x75, 0x6D, 0x4F, 0x52, 0x65, 0x73, 0x6F, 0x51, + 0x55, 0x47, 0x43, 0x2F, 0x38, 0x4E, 0x65, 0x38, 0x6C, 0x59, 0x65, 0x50, 0x6C, 0x38, 0x58, 0x2B, 0x6C, 0x32, 0x6F, 0x42, 0x6C, 0x4B, 0x4E, 0x38, 0x57, 0x34, 0x55, 0x64, 0x4B, 0x6A, 0x6B, 0x36, 0x30, 0x46, 0x53, 0x68, 0x30, 0x54, 0x6C, 0x78, + 0x6E, 0x66, 0x30, 0x68, 0x0A, 0x2B, 0x62, 0x56, 0x37, 0x38, 0x4F, 0x4C, 0x67, 0x41, 0x6F, 0x39, 0x75, 0x6C, 0x69, 0x51, 0x6C, 0x4C, 0x4B, 0x41, 0x65, 0x4C, 0x4B, 0x6A, 0x45, 0x69, 0x61, 0x66, 0x76, 0x37, 0x5A, 0x6B, 0x47, 0x4C, 0x37, 0x59, + 0x4B, 0x54, 0x45, 0x2F, 0x62, 0x6F, 0x73, 0x77, 0x33, 0x47, 0x71, 0x39, 0x48, 0x68, 0x53, 0x32, 0x4B, 0x58, 0x38, 0x51, 0x30, 0x4E, 0x45, 0x77, 0x41, 0x2F, 0x52, 0x69, 0x54, 0x5A, 0x78, 0x50, 0x52, 0x4E, 0x2B, 0x5A, 0x49, 0x74, 0x49, 0x73, + 0x47, 0x0A, 0x78, 0x56, 0x64, 0x37, 0x47, 0x59, 0x59, 0x4B, 0x65, 0x63, 0x73, 0x41, 0x79, 0x56, 0x4B, 0x76, 0x51, 0x76, 0x38, 0x33, 0x6A, 0x2B, 0x47, 0x6A, 0x48, 0x6E, 0x6F, 0x39, 0x55, 0x4B, 0x74, 0x6A, 0x42, 0x75, 0x63, 0x56, 0x74, 0x54, + 0x2B, 0x32, 0x52, 0x54, 0x65, 0x55, 0x4E, 0x37, 0x46, 0x2B, 0x38, 0x6B, 0x6A, 0x44, 0x66, 0x38, 0x56, 0x31, 0x2F, 0x70, 0x65, 0x4E, 0x52, 0x59, 0x38, 0x61, 0x70, 0x78, 0x70, 0x79, 0x4B, 0x42, 0x70, 0x41, 0x44, 0x77, 0x49, 0x44, 0x0A, 0x41, + 0x51, 0x41, 0x42, 0x6F, 0x32, 0x4D, 0x77, 0x59, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, + 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x51, 0x58, 0x6E, 0x63, 0x30, 0x65, 0x0A, 0x69, 0x39, 0x59, 0x35, + 0x4B, 0x33, 0x44, 0x54, 0x58, 0x4E, 0x53, 0x67, 0x75, 0x42, 0x2B, 0x77, 0x41, 0x50, 0x7A, 0x46, 0x59, 0x54, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x46, 0x35, 0x33, 0x4E, 0x48, 0x6F, 0x76, 0x57, + 0x4F, 0x53, 0x74, 0x77, 0x30, 0x31, 0x7A, 0x55, 0x6F, 0x4C, 0x67, 0x66, 0x73, 0x41, 0x44, 0x38, 0x78, 0x57, 0x45, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x0A, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, + 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x46, 0x62, 0x56, 0x65, 0x32, 0x37, 0x6D, 0x49, 0x67, 0x48, 0x53, 0x51, 0x70, 0x73, 0x59, 0x31, 0x51, 0x37, 0x58, 0x5A, 0x69, 0x4E, 0x63, 0x34, 0x2F, 0x36, 0x67, 0x78, 0x35, 0x4C, 0x53, 0x36, 0x5A, 0x53, + 0x74, 0x53, 0x36, 0x4C, 0x47, 0x37, 0x42, 0x4A, 0x38, 0x64, 0x4E, 0x56, 0x49, 0x30, 0x6C, 0x6B, 0x55, 0x6D, 0x63, 0x44, 0x72, 0x75, 0x64, 0x48, 0x72, 0x39, 0x45, 0x67, 0x77, 0x0A, 0x57, 0x36, 0x32, 0x6E, 0x56, 0x33, 0x4F, 0x5A, 0x71, 0x64, + 0x50, 0x6C, 0x74, 0x39, 0x45, 0x75, 0x57, 0x53, 0x52, 0x59, 0x33, 0x47, 0x67, 0x75, 0x4C, 0x6D, 0x4C, 0x59, 0x61, 0x75, 0x52, 0x77, 0x43, 0x79, 0x30, 0x67, 0x55, 0x43, 0x43, 0x6B, 0x4D, 0x70, 0x58, 0x52, 0x41, 0x4A, 0x69, 0x37, 0x30, 0x2F, + 0x33, 0x33, 0x4D, 0x76, 0x4A, 0x4A, 0x72, 0x73, 0x5A, 0x36, 0x34, 0x45, 0x65, 0x2B, 0x62, 0x73, 0x37, 0x4C, 0x6F, 0x33, 0x49, 0x36, 0x4C, 0x57, 0x6C, 0x64, 0x0A, 0x79, 0x38, 0x6A, 0x6F, 0x52, 0x54, 0x6E, 0x55, 0x2B, 0x6B, 0x4C, 0x42, 0x45, + 0x55, 0x78, 0x33, 0x58, 0x5A, 0x4C, 0x37, 0x61, 0x76, 0x39, 0x59, 0x52, 0x4F, 0x58, 0x72, 0x67, 0x5A, 0x36, 0x76, 0x6F, 0x4A, 0x6D, 0x74, 0x76, 0x71, 0x6B, 0x42, 0x5A, 0x73, 0x73, 0x34, 0x48, 0x54, 0x7A, 0x66, 0x51, 0x78, 0x2F, 0x30, 0x54, + 0x57, 0x36, 0x30, 0x75, 0x68, 0x64, 0x47, 0x2F, 0x48, 0x33, 0x39, 0x68, 0x34, 0x46, 0x35, 0x61, 0x67, 0x30, 0x7A, 0x44, 0x2F, 0x6F, 0x76, 0x0A, 0x2B, 0x42, 0x53, 0x35, 0x67, 0x4C, 0x4E, 0x64, 0x54, 0x61, 0x71, 0x58, 0x34, 0x66, 0x6E, 0x6B, + 0x47, 0x4D, 0x58, 0x34, 0x31, 0x54, 0x69, 0x4D, 0x4A, 0x6A, 0x7A, 0x39, 0x38, 0x69, 0x6A, 0x69, 0x37, 0x6C, 0x70, 0x4A, 0x69, 0x43, 0x7A, 0x66, 0x65, 0x54, 0x32, 0x4F, 0x6E, 0x70, 0x41, 0x38, 0x76, 0x55, 0x46, 0x4B, 0x4F, 0x74, 0x31, 0x62, + 0x39, 0x70, 0x71, 0x30, 0x7A, 0x6A, 0x38, 0x6C, 0x4D, 0x48, 0x38, 0x79, 0x66, 0x61, 0x49, 0x44, 0x6C, 0x4E, 0x44, 0x63, 0x0A, 0x65, 0x71, 0x46, 0x53, 0x33, 0x6D, 0x36, 0x54, 0x6A, 0x52, 0x67, 0x6D, 0x2F, 0x56, 0x57, 0x73, 0x76, 0x59, 0x2B, + 0x62, 0x30, 0x73, 0x2B, 0x76, 0x35, 0x34, 0x59, 0x73, 0x79, 0x78, 0x38, 0x4A, 0x62, 0x36, 0x4E, 0x76, 0x71, 0x59, 0x54, 0x55, 0x63, 0x37, 0x39, 0x4E, 0x6F, 0x58, 0x51, 0x62, 0x54, 0x69, 0x4E, 0x67, 0x38, 0x73, 0x77, 0x4F, 0x71, 0x6E, 0x2B, + 0x6B, 0x6E, 0x45, 0x77, 0x6C, 0x71, 0x4C, 0x4A, 0x6D, 0x4F, 0x7A, 0x6A, 0x2F, 0x32, 0x5A, 0x51, 0x77, 0x0A, 0x39, 0x6E, 0x4B, 0x45, 0x76, 0x6D, 0x68, 0x56, 0x45, 0x41, 0x2F, 0x47, 0x63, 0x79, 0x77, 0x57, 0x61, 0x5A, 0x4D, 0x48, 0x2F, 0x72, + 0x46, 0x46, 0x37, 0x62, 0x75, 0x69, 0x56, 0x57, 0x71, 0x77, 0x32, 0x72, 0x56, 0x4B, 0x41, 0x69, 0x55, 0x6E, 0x68, 0x64, 0x65, 0x33, 0x74, 0x34, 0x5A, 0x45, 0x46, 0x6F, 0x6C, 0x73, 0x67, 0x43, 0x73, 0x2B, 0x6C, 0x36, 0x6D, 0x63, 0x31, 0x58, + 0x35, 0x56, 0x54, 0x4D, 0x62, 0x65, 0x52, 0x52, 0x41, 0x63, 0x36, 0x75, 0x6B, 0x37, 0x0A, 0x6E, 0x77, 0x4E, 0x54, 0x37, 0x75, 0x35, 0x36, 0x41, 0x51, 0x49, 0x57, 0x65, 0x4E, 0x54, 0x6F, 0x77, 0x72, 0x35, 0x47, 0x64, 0x6F, 0x67, 0x54, 0x50, + 0x79, 0x4B, 0x37, 0x53, 0x42, 0x49, 0x64, 0x55, 0x67, 0x43, 0x30, 0x41, 0x6E, 0x34, 0x68, 0x47, 0x68, 0x36, 0x63, 0x4A, 0x66, 0x54, 0x7A, 0x50, 0x56, 0x34, 0x65, 0x30, 0x68, 0x7A, 0x35, 0x73, 0x79, 0x32, 0x32, 0x39, 0x7A, 0x64, 0x63, 0x78, + 0x73, 0x73, 0x68, 0x54, 0x72, 0x44, 0x33, 0x6D, 0x55, 0x63, 0x59, 0x0A, 0x68, 0x63, 0x45, 0x72, 0x75, 0x6C, 0x57, 0x75, 0x42, 0x75, 0x72, 0x51, 0x42, 0x37, 0x4C, 0x63, 0x71, 0x39, 0x43, 0x43, 0x6C, 0x6E, 0x58, 0x4F, 0x30, 0x6C, 0x44, 0x2B, + 0x6D, 0x65, 0x66, 0x50, 0x4C, 0x35, 0x2F, 0x6E, 0x64, 0x74, 0x46, 0x68, 0x4B, 0x76, 0x73, 0x68, 0x75, 0x7A, 0x48, 0x51, 0x71, 0x70, 0x39, 0x48, 0x70, 0x4C, 0x49, 0x69, 0x79, 0x68, 0x59, 0x36, 0x55, 0x46, 0x66, 0x45, 0x57, 0x30, 0x4E, 0x6E, + 0x78, 0x57, 0x56, 0x69, 0x41, 0x30, 0x6B, 0x42, 0x0A, 0x36, 0x30, 0x50, 0x5A, 0x32, 0x50, 0x69, 0x65, 0x72, 0x63, 0x2B, 0x78, 0x59, 0x77, 0x35, 0x46, 0x39, 0x4B, 0x42, 0x61, 0x4C, 0x4A, 0x73, 0x74, 0x78, 0x61, 0x62, 0x41, 0x72, 0x61, 0x68, + 0x48, 0x39, 0x43, 0x64, 0x4D, 0x4F, 0x41, 0x30, 0x75, 0x47, 0x30, 0x6B, 0x37, 0x55, 0x76, 0x54, 0x6F, 0x69, 0x49, 0x4D, 0x72, 0x56, 0x43, 0x6A, 0x55, 0x38, 0x6A, 0x56, 0x53, 0x74, 0x44, 0x4B, 0x44, 0x59, 0x6D, 0x6C, 0x6B, 0x44, 0x4A, 0x47, + 0x63, 0x6E, 0x35, 0x66, 0x71, 0x0A, 0x64, 0x42, 0x62, 0x39, 0x48, 0x78, 0x45, 0x47, 0x6D, 0x70, 0x76, 0x30, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, + 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x32, 0x30, 0x31, 0x37, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x57, 0x54, 0x43, 0x43, 0x41, 0x64, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x5A, 0x76, 0x49, 0x39, 0x72, 0x34, 0x66, 0x65, 0x69, 0x37, 0x46, 0x4B, 0x36, 0x67, + 0x78, 0x58, 0x4D, 0x51, 0x48, 0x43, 0x37, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, + 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x65, 0x4D, 0x42, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x56, 0x54, 0x57, 0x6C, 0x6A, 0x63, 0x6D, 0x39, 0x7A, 0x62, 0x32, 0x5A, 0x30, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, + 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x31, 0x4E, 0x61, 0x57, 0x4E, 0x79, 0x62, 0x33, 0x4E, 0x76, 0x5A, 0x6E, 0x51, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x0A, + 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x55, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x44, 0x49, 0x77, + 0x4D, 0x54, 0x63, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x6B, 0x78, 0x4D, 0x6A, 0x45, 0x34, 0x4D, 0x6A, 0x4D, 0x77, 0x4E, 0x6A, 0x51, 0x31, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x49, 0x77, 0x4E, 0x7A, 0x45, 0x34, 0x0A, 0x4D, 0x6A, 0x4D, + 0x78, 0x4E, 0x6A, 0x41, 0x30, 0x57, 0x6A, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x65, 0x4D, 0x42, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, + 0x56, 0x54, 0x57, 0x6C, 0x6A, 0x63, 0x6D, 0x39, 0x7A, 0x62, 0x32, 0x5A, 0x30, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, 0x54, 0x59, 0x77, 0x0A, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x44, 0x45, 0x79, 0x31, 0x4E, 0x61, 0x57, 0x4E, 0x79, 0x62, 0x33, 0x4E, 0x76, 0x5A, 0x6E, 0x51, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, + 0x4E, 0x68, 0x64, 0x47, 0x55, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x54, 0x63, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x0A, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, + 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x54, 0x55, 0x76, 0x44, 0x30, 0x43, 0x51, 0x6E, 0x56, 0x42, 0x45, 0x79, 0x50, 0x4E, 0x67, 0x41, 0x53, 0x47, 0x41, 0x6C, 0x45, 0x76, 0x61, + 0x71, 0x69, 0x42, 0x59, 0x67, 0x74, 0x6C, 0x7A, 0x50, 0x62, 0x4B, 0x6E, 0x52, 0x35, 0x76, 0x53, 0x6D, 0x5A, 0x52, 0x6F, 0x67, 0x50, 0x5A, 0x6E, 0x5A, 0x48, 0x36, 0x0A, 0x74, 0x68, 0x61, 0x78, 0x6A, 0x47, 0x37, 0x65, 0x66, 0x4D, 0x33, 0x62, + 0x65, 0x61, 0x59, 0x76, 0x7A, 0x72, 0x76, 0x4F, 0x63, 0x53, 0x2F, 0x6C, 0x70, 0x61, 0x73, 0x6F, 0x37, 0x47, 0x4D, 0x45, 0x5A, 0x70, 0x6E, 0x34, 0x2B, 0x76, 0x4B, 0x54, 0x45, 0x41, 0x58, 0x68, 0x67, 0x53, 0x68, 0x43, 0x34, 0x38, 0x5A, 0x6F, + 0x39, 0x4F, 0x59, 0x62, 0x68, 0x47, 0x42, 0x4B, 0x69, 0x61, 0x2F, 0x74, 0x65, 0x51, 0x38, 0x37, 0x7A, 0x76, 0x48, 0x32, 0x52, 0x50, 0x55, 0x42, 0x0A, 0x65, 0x4D, 0x43, 0x6A, 0x56, 0x44, 0x42, 0x53, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, + 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, + 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x49, 0x79, 0x35, 0x6C, 0x79, 0x63, 0x46, 0x49, 0x4D, 0x0A, 0x2B, 0x4F, 0x61, 0x2B, 0x73, 0x67, 0x52, 0x58, 0x4B, 0x53, 0x72, 0x50, 0x51, 0x68, 0x44, 0x74, 0x4E, 0x54, + 0x41, 0x51, 0x42, 0x67, 0x6B, 0x72, 0x42, 0x67, 0x45, 0x45, 0x41, 0x59, 0x49, 0x33, 0x46, 0x51, 0x45, 0x45, 0x41, 0x77, 0x49, 0x42, 0x41, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, + 0x4E, 0x6F, 0x41, 0x44, 0x42, 0x6C, 0x41, 0x6A, 0x42, 0x59, 0x38, 0x6B, 0x33, 0x71, 0x44, 0x50, 0x6C, 0x66, 0x0A, 0x58, 0x75, 0x35, 0x67, 0x4B, 0x63, 0x73, 0x36, 0x38, 0x74, 0x76, 0x57, 0x4D, 0x6F, 0x51, 0x5A, 0x50, 0x33, 0x7A, 0x56, 0x4C, + 0x38, 0x4B, 0x78, 0x7A, 0x4A, 0x4F, 0x75, 0x55, 0x4C, 0x73, 0x4A, 0x4D, 0x73, 0x62, 0x47, 0x37, 0x58, 0x37, 0x4A, 0x4E, 0x70, 0x51, 0x53, 0x35, 0x47, 0x69, 0x46, 0x42, 0x71, 0x49, 0x62, 0x30, 0x43, 0x38, 0x43, 0x4D, 0x51, 0x43, 0x5A, 0x36, + 0x52, 0x61, 0x30, 0x44, 0x76, 0x70, 0x57, 0x53, 0x4E, 0x53, 0x6B, 0x4D, 0x42, 0x61, 0x52, 0x0A, 0x65, 0x4E, 0x74, 0x55, 0x6A, 0x47, 0x55, 0x42, 0x69, 0x75, 0x64, 0x51, 0x5A, 0x73, 0x49, 0x78, 0x74, 0x7A, 0x6D, 0x36, 0x75, 0x42, 0x6F, 0x69, + 0x42, 0x30, 0x37, 0x38, 0x61, 0x31, 0x51, 0x57, 0x49, 0x50, 0x38, 0x72, 0x74, 0x65, 0x64, 0x4D, 0x44, 0x45, 0x32, 0x6D, 0x54, 0x33, 0x4D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x52, 0x53, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x20, 0x32, 0x30, 0x31, 0x37, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x71, 0x44, 0x43, 0x43, 0x41, 0x35, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x48, 0x74, 0x4F, 0x58, 0x43, 0x56, 0x2F, 0x59, + 0x74, 0x4C, 0x4E, 0x48, 0x63, 0x42, 0x36, 0x71, 0x76, 0x6E, 0x39, 0x46, 0x73, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, + 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x65, 0x4D, 0x42, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x56, 0x54, 0x57, 0x6C, 0x6A, 0x63, 0x6D, 0x39, 0x7A, 0x62, 0x32, 0x5A, + 0x30, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x31, 0x4E, 0x61, 0x57, 0x4E, 0x79, 0x62, 0x33, 0x4E, + 0x76, 0x5A, 0x6E, 0x51, 0x67, 0x0A, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x55, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, + 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x54, 0x63, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x6B, 0x78, 0x4D, 0x6A, 0x45, 0x34, 0x4D, 0x6A, 0x49, 0x31, 0x4D, 0x54, 0x49, 0x79, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, + 0x49, 0x77, 0x0A, 0x4E, 0x7A, 0x45, 0x34, 0x4D, 0x6A, 0x4D, 0x77, 0x4D, 0x44, 0x49, 0x7A, 0x57, 0x6A, 0x42, 0x6C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x65, 0x4D, + 0x42, 0x77, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x56, 0x54, 0x57, 0x6C, 0x6A, 0x63, 0x6D, 0x39, 0x7A, 0x62, 0x32, 0x5A, 0x30, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, + 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x31, 0x4E, 0x61, 0x57, 0x4E, 0x79, 0x62, 0x33, 0x4E, 0x76, 0x5A, 0x6E, 0x51, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, + 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x55, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x39, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x54, 0x63, 0x77, 0x0A, 0x67, 0x67, 0x49, + 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, + 0x4B, 0x57, 0x37, 0x36, 0x55, 0x4D, 0x34, 0x77, 0x70, 0x6C, 0x5A, 0x45, 0x57, 0x43, 0x70, 0x57, 0x39, 0x52, 0x32, 0x4C, 0x42, 0x69, 0x66, 0x4F, 0x5A, 0x4E, 0x74, 0x39, 0x47, 0x6B, 0x4D, 0x6D, 0x6C, 0x0A, 0x37, 0x58, 0x68, 0x71, 0x62, 0x30, + 0x65, 0x52, 0x61, 0x50, 0x67, 0x6E, 0x5A, 0x31, 0x41, 0x7A, 0x48, 0x61, 0x47, 0x6D, 0x2B, 0x2B, 0x44, 0x6C, 0x51, 0x36, 0x4F, 0x45, 0x41, 0x6C, 0x63, 0x42, 0x58, 0x5A, 0x78, 0x49, 0x51, 0x49, 0x4A, 0x54, 0x45, 0x4C, 0x79, 0x2F, 0x78, 0x7A, + 0x74, 0x6F, 0x6B, 0x4C, 0x61, 0x43, 0x4C, 0x65, 0x58, 0x30, 0x5A, 0x64, 0x44, 0x4D, 0x62, 0x52, 0x6E, 0x4D, 0x6C, 0x66, 0x6C, 0x37, 0x72, 0x45, 0x71, 0x55, 0x72, 0x51, 0x37, 0x65, 0x0A, 0x53, 0x30, 0x4D, 0x64, 0x68, 0x77, 0x65, 0x53, 0x45, + 0x35, 0x43, 0x41, 0x67, 0x32, 0x51, 0x31, 0x4F, 0x51, 0x54, 0x38, 0x35, 0x65, 0x6C, 0x73, 0x73, 0x37, 0x59, 0x66, 0x55, 0x4A, 0x51, 0x34, 0x5A, 0x56, 0x42, 0x63, 0x46, 0x30, 0x61, 0x35, 0x74, 0x6F, 0x57, 0x31, 0x48, 0x4C, 0x55, 0x58, 0x36, + 0x4E, 0x5A, 0x46, 0x6E, 0x64, 0x69, 0x79, 0x4A, 0x72, 0x44, 0x4B, 0x78, 0x48, 0x42, 0x4B, 0x72, 0x6D, 0x43, 0x6B, 0x33, 0x62, 0x50, 0x5A, 0x37, 0x50, 0x77, 0x37, 0x0A, 0x31, 0x56, 0x64, 0x79, 0x76, 0x44, 0x2F, 0x49, 0x79, 0x62, 0x4C, 0x65, + 0x53, 0x32, 0x76, 0x34, 0x49, 0x32, 0x77, 0x44, 0x77, 0x41, 0x57, 0x39, 0x6C, 0x63, 0x66, 0x4E, 0x63, 0x7A, 0x74, 0x6D, 0x67, 0x47, 0x54, 0x6A, 0x47, 0x71, 0x77, 0x75, 0x2B, 0x55, 0x63, 0x46, 0x38, 0x67, 0x61, 0x32, 0x6D, 0x33, 0x50, 0x31, + 0x65, 0x44, 0x4E, 0x62, 0x78, 0x36, 0x48, 0x37, 0x4A, 0x79, 0x71, 0x68, 0x74, 0x4A, 0x71, 0x52, 0x6A, 0x4A, 0x48, 0x54, 0x4F, 0x6F, 0x49, 0x2B, 0x0A, 0x64, 0x6B, 0x43, 0x30, 0x7A, 0x56, 0x4A, 0x68, 0x55, 0x58, 0x41, 0x6F, 0x50, 0x38, 0x58, + 0x46, 0x57, 0x76, 0x4C, 0x4A, 0x6A, 0x45, 0x6D, 0x37, 0x46, 0x46, 0x74, 0x4E, 0x79, 0x50, 0x39, 0x6E, 0x54, 0x55, 0x77, 0x53, 0x6C, 0x71, 0x33, 0x31, 0x2F, 0x6E, 0x69, 0x6F, 0x6C, 0x34, 0x66, 0x58, 0x2F, 0x56, 0x34, 0x67, 0x67, 0x4E, 0x79, + 0x68, 0x53, 0x79, 0x4C, 0x37, 0x31, 0x49, 0x6D, 0x74, 0x75, 0x73, 0x35, 0x48, 0x6C, 0x30, 0x64, 0x56, 0x65, 0x34, 0x39, 0x46, 0x0A, 0x79, 0x47, 0x63, 0x6F, 0x68, 0x4A, 0x55, 0x63, 0x61, 0x44, 0x44, 0x76, 0x37, 0x30, 0x6E, 0x67, 0x4E, 0x58, + 0x74, 0x6B, 0x35, 0x35, 0x69, 0x77, 0x6C, 0x4E, 0x70, 0x4E, 0x68, 0x54, 0x73, 0x2B, 0x56, 0x63, 0x51, 0x6F, 0x72, 0x31, 0x66, 0x7A, 0x6E, 0x68, 0x50, 0x62, 0x52, 0x69, 0x65, 0x66, 0x48, 0x71, 0x4A, 0x65, 0x52, 0x49, 0x4F, 0x6B, 0x70, 0x63, + 0x72, 0x56, 0x45, 0x37, 0x4E, 0x4C, 0x50, 0x38, 0x54, 0x6A, 0x77, 0x75, 0x61, 0x47, 0x59, 0x61, 0x52, 0x53, 0x0A, 0x4D, 0x4C, 0x6C, 0x36, 0x49, 0x45, 0x39, 0x76, 0x44, 0x7A, 0x68, 0x54, 0x79, 0x7A, 0x4D, 0x4D, 0x45, 0x79, 0x75, 0x50, 0x31, + 0x70, 0x71, 0x39, 0x4B, 0x73, 0x67, 0x74, 0x73, 0x52, 0x78, 0x39, 0x53, 0x31, 0x48, 0x4B, 0x52, 0x39, 0x46, 0x49, 0x4A, 0x33, 0x4A, 0x64, 0x68, 0x2B, 0x76, 0x56, 0x52, 0x65, 0x5A, 0x49, 0x5A, 0x5A, 0x32, 0x76, 0x55, 0x70, 0x43, 0x36, 0x57, + 0x36, 0x49, 0x59, 0x5A, 0x56, 0x63, 0x53, 0x6E, 0x32, 0x69, 0x35, 0x31, 0x42, 0x56, 0x72, 0x0A, 0x6C, 0x4D, 0x52, 0x70, 0x49, 0x70, 0x6A, 0x30, 0x4D, 0x2B, 0x44, 0x74, 0x2B, 0x56, 0x47, 0x4F, 0x51, 0x56, 0x44, 0x4A, 0x4E, 0x45, 0x39, 0x32, + 0x6B, 0x4B, 0x7A, 0x38, 0x4F, 0x4D, 0x48, 0x59, 0x34, 0x58, 0x75, 0x35, 0x34, 0x2B, 0x4F, 0x55, 0x34, 0x55, 0x5A, 0x70, 0x79, 0x77, 0x34, 0x4B, 0x55, 0x47, 0x73, 0x54, 0x75, 0x71, 0x77, 0x50, 0x4E, 0x31, 0x71, 0x33, 0x45, 0x72, 0x57, 0x51, + 0x67, 0x52, 0x35, 0x57, 0x72, 0x6C, 0x63, 0x69, 0x68, 0x74, 0x6E, 0x4A, 0x0A, 0x30, 0x74, 0x48, 0x58, 0x55, 0x65, 0x4F, 0x72, 0x4F, 0x38, 0x5A, 0x56, 0x2F, 0x52, 0x34, 0x4F, 0x30, 0x33, 0x51, 0x4B, 0x30, 0x64, 0x71, 0x71, 0x36, 0x6D, 0x6D, + 0x34, 0x6C, 0x79, 0x69, 0x50, 0x53, 0x4D, 0x51, 0x48, 0x2B, 0x46, 0x4A, 0x44, 0x4F, 0x76, 0x54, 0x4B, 0x56, 0x54, 0x55, 0x73, 0x73, 0x4B, 0x5A, 0x71, 0x77, 0x4A, 0x7A, 0x35, 0x38, 0x6F, 0x48, 0x68, 0x45, 0x6D, 0x72, 0x41, 0x52, 0x64, 0x6C, + 0x6E, 0x73, 0x38, 0x37, 0x2F, 0x49, 0x36, 0x4B, 0x4A, 0x0A, 0x43, 0x6C, 0x54, 0x55, 0x46, 0x4C, 0x6B, 0x71, 0x71, 0x4E, 0x66, 0x73, 0x2B, 0x61, 0x76, 0x4E, 0x4A, 0x56, 0x67, 0x79, 0x65, 0x59, 0x2B, 0x51, 0x57, 0x35, 0x67, 0x35, 0x78, 0x41, + 0x67, 0x47, 0x77, 0x61, 0x78, 0x2F, 0x44, 0x6A, 0x30, 0x41, 0x70, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x31, 0x51, 0x77, 0x55, 0x6A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, + 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x0A, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, + 0x67, 0x51, 0x55, 0x43, 0x63, 0x74, 0x5A, 0x66, 0x34, 0x61, 0x79, 0x63, 0x49, 0x38, 0x61, 0x77, 0x7A, 0x6E, 0x6A, 0x77, 0x4E, 0x6E, 0x70, 0x76, 0x37, 0x74, 0x4E, 0x73, 0x69, 0x4D, 0x77, 0x45, 0x41, 0x59, 0x4A, 0x4B, 0x77, 0x59, 0x42, 0x42, + 0x41, 0x47, 0x43, 0x0A, 0x4E, 0x78, 0x55, 0x42, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x41, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, + 0x41, 0x4B, 0x79, 0x76, 0x50, 0x6C, 0x33, 0x43, 0x45, 0x5A, 0x61, 0x4A, 0x6A, 0x71, 0x50, 0x6E, 0x6B, 0x74, 0x61, 0x58, 0x46, 0x62, 0x67, 0x54, 0x6F, 0x71, 0x5A, 0x43, 0x4C, 0x67, 0x4C, 0x4E, 0x46, 0x67, 0x56, 0x5A, 0x4A, 0x38, 0x6F, 0x67, + 0x0A, 0x36, 0x4C, 0x71, 0x34, 0x36, 0x42, 0x72, 0x73, 0x54, 0x61, 0x69, 0x58, 0x56, 0x71, 0x35, 0x6C, 0x51, 0x37, 0x47, 0x50, 0x41, 0x4A, 0x74, 0x53, 0x7A, 0x56, 0x58, 0x4E, 0x55, 0x7A, 0x6C, 0x74, 0x59, 0x6B, 0x79, 0x4C, 0x44, 0x56, 0x74, + 0x38, 0x4C, 0x6B, 0x53, 0x2F, 0x67, 0x78, 0x43, 0x50, 0x38, 0x31, 0x4F, 0x43, 0x67, 0x4D, 0x4E, 0x50, 0x4F, 0x73, 0x64, 0x75, 0x45, 0x54, 0x2F, 0x6D, 0x34, 0x78, 0x61, 0x52, 0x68, 0x50, 0x74, 0x74, 0x68, 0x48, 0x38, 0x30, 0x0A, 0x64, 0x4B, + 0x32, 0x4A, 0x70, 0x38, 0x36, 0x35, 0x31, 0x39, 0x65, 0x66, 0x68, 0x47, 0x53, 0x53, 0x76, 0x70, 0x57, 0x68, 0x72, 0x51, 0x6C, 0x54, 0x4D, 0x39, 0x33, 0x75, 0x43, 0x75, 0x70, 0x4B, 0x55, 0x59, 0x35, 0x76, 0x56, 0x61, 0x75, 0x36, 0x74, 0x5A, + 0x52, 0x47, 0x72, 0x6F, 0x78, 0x2F, 0x32, 0x4B, 0x4A, 0x51, 0x4A, 0x57, 0x56, 0x67, 0x67, 0x45, 0x62, 0x62, 0x4D, 0x77, 0x53, 0x75, 0x62, 0x4C, 0x57, 0x59, 0x64, 0x46, 0x51, 0x6C, 0x33, 0x4A, 0x50, 0x6B, 0x0A, 0x2B, 0x4F, 0x4E, 0x56, 0x46, + 0x54, 0x32, 0x34, 0x62, 0x63, 0x4D, 0x4B, 0x70, 0x42, 0x4C, 0x42, 0x61, 0x59, 0x56, 0x75, 0x33, 0x32, 0x54, 0x78, 0x55, 0x35, 0x6E, 0x68, 0x53, 0x6E, 0x55, 0x67, 0x6E, 0x5A, 0x55, 0x50, 0x35, 0x4E, 0x62, 0x63, 0x41, 0x2F, 0x46, 0x5A, 0x47, + 0x4F, 0x68, 0x48, 0x69, 0x62, 0x4A, 0x58, 0x57, 0x70, 0x53, 0x32, 0x71, 0x64, 0x67, 0x58, 0x4B, 0x78, 0x64, 0x4A, 0x35, 0x58, 0x62, 0x4C, 0x77, 0x56, 0x61, 0x5A, 0x4F, 0x6A, 0x65, 0x78, 0x0A, 0x2F, 0x32, 0x6B, 0x73, 0x6B, 0x5A, 0x47, 0x54, + 0x34, 0x64, 0x39, 0x4D, 0x6F, 0x7A, 0x64, 0x32, 0x54, 0x61, 0x47, 0x66, 0x2B, 0x47, 0x30, 0x65, 0x48, 0x64, 0x50, 0x36, 0x37, 0x50, 0x76, 0x30, 0x52, 0x52, 0x30, 0x54, 0x62, 0x63, 0x2F, 0x33, 0x57, 0x65, 0x55, 0x69, 0x4A, 0x33, 0x49, 0x72, + 0x68, 0x76, 0x4E, 0x58, 0x75, 0x7A, 0x44, 0x74, 0x4A, 0x45, 0x33, 0x63, 0x66, 0x56, 0x61, 0x37, 0x6F, 0x37, 0x50, 0x34, 0x4E, 0x48, 0x6D, 0x4A, 0x77, 0x65, 0x44, 0x79, 0x0A, 0x41, 0x6D, 0x48, 0x33, 0x70, 0x76, 0x77, 0x50, 0x75, 0x78, 0x77, + 0x58, 0x43, 0x36, 0x35, 0x42, 0x32, 0x58, 0x79, 0x39, 0x4A, 0x36, 0x50, 0x39, 0x4C, 0x6A, 0x72, 0x52, 0x6B, 0x35, 0x53, 0x78, 0x63, 0x78, 0x30, 0x6B, 0x69, 0x36, 0x39, 0x62, 0x49, 0x49, 0x6D, 0x74, 0x74, 0x32, 0x64, 0x6D, 0x65, 0x66, 0x55, + 0x36, 0x78, 0x71, 0x61, 0x57, 0x4D, 0x2F, 0x35, 0x54, 0x6B, 0x73, 0x68, 0x47, 0x73, 0x52, 0x47, 0x52, 0x78, 0x70, 0x6C, 0x2F, 0x6A, 0x38, 0x6E, 0x57, 0x0A, 0x5A, 0x6A, 0x45, 0x67, 0x51, 0x52, 0x43, 0x48, 0x4C, 0x51, 0x7A, 0x57, 0x77, 0x61, + 0x38, 0x30, 0x6D, 0x4D, 0x70, 0x6B, 0x67, 0x2F, 0x73, 0x54, 0x56, 0x39, 0x48, 0x42, 0x38, 0x44, 0x78, 0x36, 0x6A, 0x4B, 0x58, 0x42, 0x2F, 0x5A, 0x55, 0x68, 0x6F, 0x48, 0x48, 0x42, 0x6B, 0x32, 0x64, 0x78, 0x45, 0x75, 0x71, 0x50, 0x69, 0x41, + 0x70, 0x70, 0x47, 0x57, 0x53, 0x5A, 0x49, 0x31, 0x62, 0x37, 0x72, 0x43, 0x6F, 0x75, 0x63, 0x4C, 0x35, 0x6D, 0x78, 0x41, 0x79, 0x45, 0x0A, 0x37, 0x2B, 0x57, 0x4C, 0x38, 0x35, 0x4D, 0x42, 0x2B, 0x47, 0x71, 0x51, 0x6B, 0x32, 0x64, 0x4C, 0x73, + 0x6D, 0x69, 0x6A, 0x74, 0x57, 0x4B, 0x50, 0x36, 0x54, 0x2B, 0x4D, 0x65, 0x6A, 0x74, 0x65, 0x44, 0x2B, 0x65, 0x4D, 0x75, 0x4D, 0x5A, 0x38, 0x37, 0x7A, 0x66, 0x39, 0x64, 0x4F, 0x4C, 0x49, 0x54, 0x7A, 0x4E, 0x79, 0x34, 0x5A, 0x51, 0x35, 0x62, + 0x62, 0x30, 0x53, 0x72, 0x37, 0x34, 0x4D, 0x54, 0x6E, 0x42, 0x38, 0x47, 0x32, 0x2B, 0x4E, 0x73, 0x7A, 0x4B, 0x54, 0x0A, 0x63, 0x30, 0x51, 0x57, 0x62, 0x65, 0x6A, 0x30, 0x39, 0x2B, 0x43, 0x56, 0x67, 0x49, 0x2B, 0x57, 0x58, 0x54, 0x69, 0x6B, + 0x39, 0x4B, 0x76, 0x65, 0x43, 0x6A, 0x43, 0x48, 0x6B, 0x39, 0x68, 0x4E, 0x41, 0x48, 0x46, 0x69, 0x52, 0x53, 0x64, 0x4C, 0x4F, 0x6B, 0x4B, 0x45, 0x57, 0x33, 0x39, 0x6C, 0x74, 0x32, 0x63, 0x30, 0x55, 0x69, 0x32, 0x63, 0x46, 0x6D, 0x75, 0x71, + 0x71, 0x4E, 0x68, 0x37, 0x6F, 0x30, 0x4A, 0x4D, 0x63, 0x63, 0x63, 0x4D, 0x79, 0x6A, 0x36, 0x44, 0x0A, 0x35, 0x4B, 0x62, 0x76, 0x74, 0x77, 0x45, 0x77, 0x58, 0x6C, 0x47, 0x6A, 0x65, 0x66, 0x56, 0x77, 0x61, 0x61, 0x5A, 0x42, 0x52, 0x41, 0x2B, + 0x47, 0x73, 0x43, 0x79, 0x52, 0x78, 0x6A, 0x33, 0x71, 0x72, 0x67, 0x2B, 0x45, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x0A, 0x65, 0x2D, 0x53, 0x7A, 0x69, 0x67, 0x6E, 0x6F, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x31, 0x37, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x51, 0x44, 0x43, + 0x43, 0x41, 0x65, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4D, 0x41, 0x56, 0x52, 0x49, 0x37, 0x79, 0x48, 0x39, 0x6C, 0x31, 0x6B, 0x4E, 0x39, 0x51, 0x51, 0x4B, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, + 0x39, 0x42, 0x41, 0x4D, 0x43, 0x4D, 0x48, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x68, 0x56, 0x4D, 0x52, 0x45, 0x77, 0x0A, 0x44, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, + 0x68, 0x43, 0x64, 0x57, 0x52, 0x68, 0x63, 0x47, 0x56, 0x7A, 0x64, 0x44, 0x45, 0x57, 0x4D, 0x42, 0x51, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x4E, 0x54, 0x57, 0x6C, 0x6A, 0x63, 0x6D, 0x39, 0x7A, 0x5A, 0x57, 0x4D, 0x67, 0x54, 0x48, + 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x58, 0x4D, 0x42, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x59, 0x51, 0x77, 0x4F, 0x56, 0x6B, 0x46, 0x55, 0x53, 0x46, 0x55, 0x74, 0x0A, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x44, 0x51, 0x30, 0x4F, 0x54, 0x63, 0x78, 0x48, + 0x6A, 0x41, 0x63, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x57, 0x55, 0x74, 0x55, 0x33, 0x70, 0x70, 0x5A, 0x32, 0x35, 0x76, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x78, 0x4E, + 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x34, 0x4D, 0x6A, 0x49, 0x78, 0x4D, 0x6A, 0x41, 0x33, 0x4D, 0x44, 0x5A, 0x61, 0x0A, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x34, 0x4D, 0x6A, 0x49, 0x78, 0x4D, 0x6A, 0x41, 0x33, + 0x4D, 0x44, 0x5A, 0x61, 0x4D, 0x48, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x68, 0x56, 0x4D, 0x52, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x48, 0x44, 0x41, 0x68, 0x43, + 0x64, 0x57, 0x52, 0x68, 0x63, 0x47, 0x56, 0x7A, 0x64, 0x44, 0x45, 0x57, 0x4D, 0x42, 0x51, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x67, 0x77, 0x4E, 0x54, 0x57, 0x6C, 0x6A, 0x63, 0x6D, 0x39, 0x7A, 0x5A, 0x57, 0x4D, 0x67, 0x54, 0x48, 0x52, + 0x6B, 0x4C, 0x6A, 0x45, 0x58, 0x4D, 0x42, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x59, 0x51, 0x77, 0x4F, 0x56, 0x6B, 0x46, 0x55, 0x53, 0x46, 0x55, 0x74, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x44, 0x51, 0x30, 0x4F, 0x54, 0x63, 0x78, 0x48, 0x6A, 0x41, + 0x63, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x57, 0x55, 0x74, 0x55, 0x33, 0x70, 0x70, 0x0A, 0x5A, 0x32, 0x35, 0x76, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x78, 0x4E, 0x7A, + 0x42, 0x5A, 0x4D, 0x42, 0x4D, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x77, 0x45, 0x48, 0x41, 0x30, 0x49, 0x41, 0x42, 0x4A, 0x62, 0x63, 0x50, 0x59, + 0x72, 0x59, 0x73, 0x48, 0x74, 0x76, 0x78, 0x69, 0x65, 0x2B, 0x52, 0x4A, 0x43, 0x78, 0x0A, 0x73, 0x31, 0x59, 0x56, 0x65, 0x34, 0x35, 0x44, 0x4A, 0x48, 0x30, 0x61, 0x68, 0x46, 0x6E, 0x75, 0x59, 0x32, 0x69, 0x79, 0x78, 0x6C, 0x36, 0x48, 0x30, + 0x42, 0x56, 0x49, 0x48, 0x71, 0x69, 0x51, 0x72, 0x62, 0x31, 0x54, 0x6F, 0x74, 0x72, 0x65, 0x4F, 0x70, 0x43, 0x6D, 0x59, 0x46, 0x39, 0x6F, 0x4D, 0x72, 0x57, 0x47, 0x51, 0x64, 0x2B, 0x48, 0x57, 0x79, 0x78, 0x37, 0x78, 0x66, 0x35, 0x38, 0x65, + 0x74, 0x71, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, 0x38, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, + 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x48, 0x45, 0x52, 0x55, 0x49, 0x30, 0x61, 0x72, 0x42, 0x65, 0x41, 0x79, 0x78, + 0x72, 0x38, 0x37, 0x47, 0x79, 0x5A, 0x44, 0x76, 0x0A, 0x76, 0x7A, 0x41, 0x45, 0x77, 0x44, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x53, 0x48, 0x45, 0x52, 0x55, 0x49, 0x30, 0x61, 0x72, + 0x42, 0x65, 0x41, 0x79, 0x78, 0x72, 0x38, 0x37, 0x47, 0x79, 0x5A, 0x44, 0x76, 0x76, 0x7A, 0x41, 0x45, 0x77, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x67, 0x4E, 0x4A, 0x41, 0x44, 0x42, + 0x47, 0x41, 0x69, 0x45, 0x41, 0x0A, 0x74, 0x56, 0x66, 0x64, 0x31, 0x34, 0x70, 0x56, 0x43, 0x7A, 0x62, 0x68, 0x68, 0x6B, 0x54, 0x36, 0x31, 0x4E, 0x6C, 0x6F, 0x6A, 0x62, 0x6A, 0x63, 0x49, 0x34, 0x71, 0x4B, 0x44, 0x64, 0x51, 0x76, 0x66, 0x65, + 0x70, 0x7A, 0x37, 0x4C, 0x39, 0x4E, 0x62, 0x4B, 0x67, 0x43, 0x49, 0x51, 0x44, 0x4C, 0x70, 0x62, 0x51, 0x53, 0x2B, 0x75, 0x65, 0x31, 0x36, 0x4D, 0x39, 0x2B, 0x6B, 0x2F, 0x7A, 0x7A, 0x4E, 0x59, 0x39, 0x76, 0x54, 0x6C, 0x70, 0x38, 0x74, 0x4C, + 0x78, 0x4F, 0x0A, 0x73, 0x76, 0x78, 0x79, 0x71, 0x6C, 0x74, 0x5A, 0x2B, 0x65, 0x66, 0x63, 0x4D, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x63, 0x65, 0x72, 0x74, 0x53, 0x49, 0x47, 0x4E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x47, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x52, + 0x7A, 0x43, 0x43, 0x41, 0x79, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4A, 0x45, 0x51, 0x41, 0x30, 0x74, 0x6B, 0x37, 0x47, 0x4E, 0x69, 0x30, 0x32, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, + 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x45, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x4A, 0x50, 0x4D, 0x52, 0x51, 0x77, 0x0A, 0x45, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, + 0x45, 0x77, 0x74, 0x44, 0x52, 0x56, 0x4A, 0x55, 0x55, 0x30, 0x6C, 0x48, 0x54, 0x69, 0x42, 0x54, 0x51, 0x54, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x54, 0x59, 0x32, 0x56, 0x79, 0x64, 0x46, 0x4E, 0x4A, + 0x52, 0x30, 0x34, 0x67, 0x55, 0x6B, 0x39, 0x50, 0x56, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x48, 0x4D, 0x6A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x79, 0x0A, 0x4D, 0x44, 0x59, 0x77, 0x4F, 0x54, 0x49, 0x33, 0x4D, 0x7A, 0x56, + 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x44, 0x59, 0x77, 0x4F, 0x54, 0x49, 0x33, 0x4D, 0x7A, 0x56, 0x61, 0x4D, 0x45, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x4A, + 0x50, 0x4D, 0x52, 0x51, 0x77, 0x45, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x74, 0x44, 0x52, 0x56, 0x4A, 0x55, 0x55, 0x30, 0x6C, 0x48, 0x0A, 0x54, 0x69, 0x42, 0x54, 0x51, 0x54, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, + 0x55, 0x45, 0x43, 0x78, 0x4D, 0x54, 0x59, 0x32, 0x56, 0x79, 0x64, 0x46, 0x4E, 0x4A, 0x52, 0x30, 0x34, 0x67, 0x55, 0x6B, 0x39, 0x50, 0x56, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x48, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, + 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x0A, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4D, 0x44, 0x46, 0x64, + 0x52, 0x6D, 0x52, 0x66, 0x55, 0x52, 0x30, 0x64, 0x49, 0x66, 0x2B, 0x44, 0x6A, 0x75, 0x57, 0x33, 0x4E, 0x67, 0x42, 0x46, 0x73, 0x7A, 0x75, 0x59, 0x35, 0x48, 0x6E, 0x43, 0x32, 0x2F, 0x4F, 0x4F, 0x77, 0x70, 0x70, 0x47, 0x6E, 0x7A, 0x43, 0x34, + 0x36, 0x2B, 0x43, 0x6A, 0x6F, 0x62, 0x58, 0x58, 0x6F, 0x39, 0x58, 0x36, 0x39, 0x4D, 0x68, 0x57, 0x66, 0x30, 0x35, 0x0A, 0x4E, 0x30, 0x49, 0x77, 0x76, 0x6C, 0x44, 0x71, 0x74, 0x67, 0x2B, 0x70, 0x69, 0x4E, 0x67, 0x75, 0x4C, 0x57, 0x6B, 0x68, + 0x35, 0x39, 0x45, 0x33, 0x47, 0x45, 0x35, 0x39, 0x6B, 0x64, 0x55, 0x57, 0x58, 0x32, 0x74, 0x62, 0x41, 0x4D, 0x49, 0x35, 0x51, 0x77, 0x30, 0x32, 0x68, 0x56, 0x4B, 0x35, 0x55, 0x32, 0x55, 0x50, 0x48, 0x55, 0x4C, 0x6C, 0x6A, 0x38, 0x38, 0x46, + 0x30, 0x2B, 0x37, 0x63, 0x44, 0x42, 0x72, 0x5A, 0x75, 0x49, 0x74, 0x34, 0x49, 0x6D, 0x66, 0x6B, 0x0A, 0x61, 0x62, 0x42, 0x6F, 0x78, 0x54, 0x7A, 0x6B, 0x62, 0x46, 0x70, 0x47, 0x35, 0x38, 0x33, 0x48, 0x2B, 0x75, 0x2F, 0x45, 0x37, 0x45, 0x75, + 0x39, 0x61, 0x71, 0x53, 0x73, 0x2F, 0x63, 0x77, 0x6F, 0x55, 0x65, 0x2B, 0x53, 0x74, 0x43, 0x6D, 0x72, 0x71, 0x7A, 0x57, 0x61, 0x54, 0x4F, 0x54, 0x45, 0x43, 0x4D, 0x59, 0x6D, 0x7A, 0x50, 0x68, 0x70, 0x6E, 0x2B, 0x53, 0x63, 0x38, 0x43, 0x6E, + 0x54, 0x58, 0x50, 0x6E, 0x47, 0x46, 0x69, 0x57, 0x65, 0x49, 0x38, 0x4D, 0x67, 0x0A, 0x77, 0x54, 0x30, 0x50, 0x50, 0x7A, 0x68, 0x41, 0x73, 0x50, 0x36, 0x43, 0x52, 0x44, 0x69, 0x71, 0x57, 0x68, 0x71, 0x4B, 0x61, 0x32, 0x4E, 0x59, 0x4F, 0x4C, + 0x51, 0x56, 0x30, 0x37, 0x59, 0x52, 0x61, 0x58, 0x73, 0x65, 0x56, 0x4F, 0x36, 0x4D, 0x47, 0x69, 0x4B, 0x73, 0x63, 0x70, 0x63, 0x2F, 0x49, 0x31, 0x6D, 0x62, 0x79, 0x53, 0x4B, 0x45, 0x77, 0x51, 0x64, 0x50, 0x7A, 0x48, 0x2F, 0x69, 0x56, 0x38, + 0x6F, 0x53, 0x63, 0x4C, 0x75, 0x6D, 0x5A, 0x66, 0x4E, 0x70, 0x0A, 0x64, 0x57, 0x4F, 0x39, 0x6C, 0x66, 0x73, 0x62, 0x6C, 0x38, 0x33, 0x6B, 0x71, 0x4B, 0x2F, 0x32, 0x30, 0x55, 0x36, 0x6F, 0x32, 0x59, 0x70, 0x78, 0x4A, 0x4D, 0x30, 0x32, 0x50, + 0x62, 0x79, 0x57, 0x78, 0x50, 0x46, 0x73, 0x71, 0x61, 0x37, 0x6C, 0x7A, 0x77, 0x31, 0x75, 0x4B, 0x41, 0x32, 0x77, 0x44, 0x72, 0x58, 0x4B, 0x55, 0x58, 0x74, 0x34, 0x46, 0x4D, 0x4D, 0x67, 0x4C, 0x33, 0x2F, 0x37, 0x46, 0x46, 0x58, 0x68, 0x45, + 0x5A, 0x6E, 0x39, 0x31, 0x51, 0x71, 0x68, 0x0A, 0x6E, 0x67, 0x4C, 0x6A, 0x59, 0x6C, 0x2F, 0x72, 0x4E, 0x55, 0x73, 0x73, 0x75, 0x48, 0x4C, 0x6F, 0x50, 0x6A, 0x31, 0x50, 0x72, 0x43, 0x79, 0x37, 0x4C, 0x6F, 0x62, 0x69, 0x6F, 0x33, 0x61, 0x50, + 0x35, 0x5A, 0x4D, 0x71, 0x7A, 0x36, 0x57, 0x72, 0x79, 0x46, 0x79, 0x4E, 0x53, 0x77, 0x62, 0x2F, 0x45, 0x6B, 0x61, 0x73, 0x65, 0x4D, 0x73, 0x55, 0x42, 0x7A, 0x58, 0x67, 0x71, 0x64, 0x2B, 0x4C, 0x36, 0x61, 0x38, 0x56, 0x54, 0x78, 0x61, 0x4A, + 0x57, 0x37, 0x33, 0x32, 0x0A, 0x6A, 0x63, 0x5A, 0x5A, 0x72, 0x6F, 0x69, 0x46, 0x44, 0x73, 0x47, 0x4A, 0x36, 0x78, 0x39, 0x6E, 0x78, 0x55, 0x57, 0x4F, 0x2F, 0x32, 0x30, 0x33, 0x4E, 0x69, 0x74, 0x34, 0x5A, 0x6F, 0x4F, 0x52, 0x55, 0x53, 0x73, + 0x39, 0x2F, 0x31, 0x46, 0x33, 0x64, 0x6D, 0x4B, 0x68, 0x37, 0x47, 0x63, 0x2B, 0x50, 0x6F, 0x47, 0x44, 0x34, 0x46, 0x61, 0x70, 0x55, 0x42, 0x38, 0x66, 0x65, 0x70, 0x6D, 0x72, 0x59, 0x37, 0x2B, 0x45, 0x46, 0x33, 0x66, 0x78, 0x44, 0x54, 0x76, + 0x66, 0x0A, 0x39, 0x35, 0x78, 0x68, 0x73, 0x7A, 0x57, 0x59, 0x69, 0x6A, 0x71, 0x79, 0x37, 0x44, 0x77, 0x61, 0x4E, 0x7A, 0x39, 0x2B, 0x6A, 0x35, 0x4C, 0x50, 0x32, 0x52, 0x49, 0x55, 0x5A, 0x4E, 0x6F, 0x51, 0x41, 0x68, 0x56, 0x42, 0x2F, 0x30, + 0x2F, 0x45, 0x36, 0x78, 0x79, 0x6A, 0x79, 0x66, 0x71, 0x5A, 0x39, 0x30, 0x62, 0x70, 0x34, 0x52, 0x6A, 0x5A, 0x73, 0x62, 0x67, 0x79, 0x4C, 0x63, 0x73, 0x55, 0x44, 0x46, 0x44, 0x59, 0x67, 0x32, 0x57, 0x44, 0x37, 0x72, 0x6C, 0x63, 0x0A, 0x7A, + 0x38, 0x73, 0x46, 0x57, 0x6B, 0x7A, 0x36, 0x47, 0x5A, 0x64, 0x72, 0x31, 0x6C, 0x30, 0x54, 0x30, 0x38, 0x4A, 0x63, 0x56, 0x4C, 0x77, 0x79, 0x63, 0x36, 0x42, 0x34, 0x39, 0x66, 0x46, 0x74, 0x48, 0x73, 0x75, 0x66, 0x70, 0x61, 0x61, 0x66, 0x49, + 0x74, 0x7A, 0x52, 0x55, 0x5A, 0x36, 0x43, 0x65, 0x57, 0x52, 0x67, 0x4B, 0x52, 0x4D, 0x2B, 0x6F, 0x2F, 0x31, 0x50, 0x63, 0x6D, 0x71, 0x72, 0x34, 0x74, 0x54, 0x6C, 0x75, 0x43, 0x52, 0x56, 0x4C, 0x45, 0x52, 0x4C, 0x0A, 0x69, 0x6F, 0x68, 0x45, + 0x6E, 0x4D, 0x71, 0x45, 0x30, 0x79, 0x6F, 0x37, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, + 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, + 0x43, 0x49, 0x53, 0x31, 0x6D, 0x78, 0x74, 0x65, 0x67, 0x34, 0x42, 0x58, 0x72, 0x7A, 0x6B, 0x77, 0x4A, 0x64, 0x38, 0x52, 0x67, 0x6E, 0x6C, 0x52, 0x75, 0x41, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, + 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x59, 0x4E, 0x34, 0x61, 0x75, 0x4F, 0x66, 0x79, 0x59, 0x49, 0x4C, 0x56, 0x41, 0x7A, 0x4F, 0x42, 0x0A, 0x79, 0x77, 0x61, 0x4B, 0x38, 0x53, 0x4A, 0x4A, 0x36, 0x65, + 0x6A, 0x71, 0x6B, 0x58, 0x2F, 0x47, 0x4D, 0x31, 0x35, 0x6F, 0x47, 0x51, 0x4F, 0x47, 0x4F, 0x30, 0x4D, 0x42, 0x7A, 0x77, 0x64, 0x77, 0x35, 0x41, 0x67, 0x65, 0x5A, 0x59, 0x57, 0x52, 0x35, 0x68, 0x45, 0x69, 0x74, 0x2F, 0x55, 0x43, 0x49, 0x34, + 0x36, 0x75, 0x75, 0x52, 0x35, 0x39, 0x48, 0x33, 0x35, 0x73, 0x35, 0x72, 0x30, 0x6C, 0x31, 0x5A, 0x55, 0x61, 0x38, 0x67, 0x57, 0x6D, 0x72, 0x34, 0x55, 0x43, 0x0A, 0x62, 0x36, 0x37, 0x34, 0x31, 0x6A, 0x48, 0x2F, 0x4A, 0x63, 0x6C, 0x4B, 0x79, + 0x4D, 0x65, 0x4B, 0x71, 0x64, 0x6D, 0x66, 0x53, 0x30, 0x6D, 0x62, 0x45, 0x56, 0x65, 0x5A, 0x6B, 0x6B, 0x4D, 0x52, 0x33, 0x72, 0x59, 0x7A, 0x70, 0x4D, 0x7A, 0x58, 0x6A, 0x57, 0x52, 0x39, 0x31, 0x4D, 0x30, 0x38, 0x4B, 0x43, 0x79, 0x30, 0x6D, + 0x70, 0x62, 0x71, 0x54, 0x66, 0x58, 0x45, 0x52, 0x4D, 0x51, 0x6C, 0x71, 0x69, 0x43, 0x41, 0x32, 0x43, 0x6C, 0x56, 0x39, 0x2B, 0x42, 0x42, 0x0A, 0x2F, 0x41, 0x59, 0x6D, 0x2F, 0x37, 0x6B, 0x32, 0x39, 0x55, 0x4D, 0x55, 0x41, 0x32, 0x5A, 0x34, + 0x34, 0x52, 0x47, 0x78, 0x32, 0x69, 0x42, 0x66, 0x52, 0x67, 0x42, 0x34, 0x41, 0x43, 0x47, 0x6C, 0x48, 0x67, 0x41, 0x6F, 0x59, 0x58, 0x68, 0x76, 0x71, 0x41, 0x45, 0x42, 0x6A, 0x35, 0x30, 0x30, 0x6D, 0x76, 0x2F, 0x30, 0x4F, 0x4A, 0x44, 0x37, + 0x75, 0x4E, 0x47, 0x7A, 0x63, 0x67, 0x62, 0x4A, 0x63, 0x65, 0x61, 0x42, 0x78, 0x58, 0x6E, 0x74, 0x43, 0x36, 0x5A, 0x35, 0x0A, 0x38, 0x68, 0x4D, 0x4C, 0x6E, 0x50, 0x64, 0x64, 0x44, 0x6E, 0x73, 0x6B, 0x6B, 0x37, 0x52, 0x49, 0x32, 0x34, 0x5A, + 0x66, 0x33, 0x6C, 0x43, 0x47, 0x65, 0x4F, 0x64, 0x41, 0x35, 0x6A, 0x47, 0x6F, 0x6B, 0x48, 0x5A, 0x77, 0x59, 0x61, 0x2B, 0x63, 0x4E, 0x79, 0x77, 0x52, 0x74, 0x59, 0x4B, 0x33, 0x71, 0x71, 0x34, 0x6B, 0x4E, 0x46, 0x74, 0x79, 0x44, 0x47, 0x6B, + 0x4E, 0x7A, 0x56, 0x6D, 0x66, 0x39, 0x6E, 0x47, 0x76, 0x6E, 0x41, 0x76, 0x52, 0x43, 0x6A, 0x6A, 0x35, 0x0A, 0x42, 0x69, 0x4B, 0x44, 0x55, 0x79, 0x55, 0x4D, 0x2F, 0x46, 0x48, 0x45, 0x35, 0x72, 0x37, 0x69, 0x4F, 0x5A, 0x55, 0x4C, 0x4A, 0x4B, + 0x32, 0x76, 0x30, 0x5A, 0x58, 0x6B, 0x6C, 0x74, 0x64, 0x30, 0x5A, 0x47, 0x74, 0x78, 0x54, 0x67, 0x49, 0x38, 0x71, 0x6F, 0x58, 0x7A, 0x49, 0x4B, 0x4E, 0x44, 0x4F, 0x58, 0x5A, 0x62, 0x62, 0x46, 0x44, 0x2B, 0x6D, 0x70, 0x77, 0x55, 0x48, 0x6D, + 0x55, 0x55, 0x69, 0x68, 0x57, 0x39, 0x6F, 0x34, 0x4A, 0x46, 0x57, 0x6B, 0x6C, 0x57, 0x0A, 0x61, 0x74, 0x4B, 0x63, 0x73, 0x57, 0x4D, 0x79, 0x35, 0x57, 0x48, 0x67, 0x55, 0x79, 0x49, 0x4F, 0x70, 0x77, 0x70, 0x4A, 0x36, 0x73, 0x74, 0x2B, 0x48, + 0x36, 0x6A, 0x69, 0x59, 0x6F, 0x44, 0x32, 0x45, 0x45, 0x56, 0x53, 0x6D, 0x41, 0x59, 0x59, 0x33, 0x71, 0x58, 0x4E, 0x4C, 0x33, 0x2B, 0x71, 0x31, 0x4F, 0x6B, 0x2B, 0x43, 0x48, 0x4C, 0x73, 0x49, 0x77, 0x4D, 0x43, 0x50, 0x4B, 0x61, 0x71, 0x32, + 0x4C, 0x78, 0x6E, 0x64, 0x44, 0x30, 0x55, 0x46, 0x2F, 0x74, 0x55, 0x0A, 0x53, 0x78, 0x66, 0x6A, 0x30, 0x33, 0x6B, 0x39, 0x62, 0x57, 0x74, 0x4A, 0x79, 0x53, 0x67, 0x4F, 0x4C, 0x6E, 0x52, 0x51, 0x76, 0x77, 0x7A, 0x5A, 0x52, 0x6A, 0x6F, 0x51, + 0x68, 0x73, 0x6D, 0x6E, 0x50, 0x2B, 0x6D, 0x67, 0x37, 0x48, 0x2F, 0x72, 0x70, 0x58, 0x64, 0x59, 0x61, 0x58, 0x48, 0x6D, 0x67, 0x77, 0x6F, 0x33, 0x38, 0x6F, 0x5A, 0x4A, 0x61, 0x72, 0x35, 0x35, 0x43, 0x4A, 0x44, 0x32, 0x41, 0x68, 0x5A, 0x6B, + 0x50, 0x75, 0x58, 0x61, 0x54, 0x48, 0x34, 0x4D, 0x0A, 0x4E, 0x4D, 0x6E, 0x35, 0x58, 0x37, 0x61, 0x7A, 0x4B, 0x46, 0x47, 0x6E, 0x70, 0x79, 0x75, 0x71, 0x53, 0x66, 0x71, 0x4E, 0x5A, 0x53, 0x6C, 0x4F, 0x34, 0x32, 0x73, 0x54, 0x70, 0x35, 0x53, + 0x6A, 0x4C, 0x56, 0x46, 0x74, 0x65, 0x41, 0x78, 0x45, 0x79, 0x39, 0x2F, 0x65, 0x43, 0x47, 0x2F, 0x4F, 0x6F, 0x32, 0x53, 0x72, 0x30, 0x35, 0x57, 0x45, 0x31, 0x4C, 0x6C, 0x53, 0x56, 0x48, 0x4A, 0x37, 0x6C, 0x69, 0x58, 0x4D, 0x76, 0x47, 0x6E, + 0x6A, 0x53, 0x47, 0x34, 0x4E, 0x0A, 0x30, 0x4D, 0x65, 0x64, 0x4A, 0x35, 0x71, 0x71, 0x2B, 0x42, 0x4F, 0x53, 0x33, 0x52, 0x37, 0x66, 0x59, 0x35, 0x38, 0x31, 0x71, 0x52, 0x59, 0x32, 0x37, 0x49, 0x79, 0x34, 0x67, 0x2F, 0x51, 0x39, 0x69, 0x59, + 0x2F, 0x4E, 0x74, 0x42, 0x64, 0x65, 0x31, 0x37, 0x4D, 0x58, 0x51, 0x52, 0x42, 0x64, 0x4A, 0x33, 0x4E, 0x67, 0x68, 0x56, 0x64, 0x4A, 0x49, 0x67, 0x63, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x77, 0x61, 0x76, 0x65, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x32, 0x6A, 0x43, 0x43, 0x41, 0x38, 0x4B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4D, 0x42, 0x66, 0x63, 0x4F, 0x68, 0x74, 0x70, 0x4A, 0x38, 0x30, 0x59, 0x31, 0x4C, 0x72, 0x71, 0x79, + 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x4D, 0x49, 0x47, 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, + 0x0A, 0x55, 0x7A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x41, 0x77, 0x49, 0x53, 0x57, 0x78, 0x73, 0x61, 0x57, 0x35, 0x76, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, + 0x4D, 0x42, 0x30, 0x4E, 0x6F, 0x61, 0x57, 0x4E, 0x68, 0x5A, 0x32, 0x38, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x47, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x64, 0x32, 0x46, 0x32, 0x0A, 0x5A, 0x53, + 0x42, 0x49, 0x62, 0x32, 0x78, 0x6B, 0x61, 0x57, 0x35, 0x6E, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x78, 0x4D, 0x43, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x6F, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, + 0x52, 0x33, 0x59, 0x58, 0x5A, 0x6C, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, 0x49, 0x45, 0x46, 0x31, 0x64, + 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x34, 0x4D, 0x6A, 0x4D, 0x78, 0x4F, 0x54, 0x4D, 0x30, 0x4D, 0x54, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x34, 0x4D, + 0x6A, 0x4D, 0x78, 0x4F, 0x54, 0x4D, 0x30, 0x4D, 0x54, 0x4A, 0x61, 0x4D, 0x49, 0x47, 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x43, 0x41, 0x77, 0x49, 0x53, 0x57, 0x78, 0x73, 0x61, 0x57, 0x35, 0x76, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x4D, 0x42, 0x30, 0x4E, 0x6F, 0x61, 0x57, 0x4E, 0x68, + 0x5A, 0x32, 0x38, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x47, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x64, 0x32, 0x46, 0x32, 0x0A, 0x5A, 0x53, 0x42, 0x49, 0x62, 0x32, 0x78, 0x6B, 0x61, 0x57, 0x35, + 0x6E, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x78, 0x4D, 0x43, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x6F, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x33, 0x59, 0x58, 0x5A, 0x6C, 0x49, 0x45, 0x64, + 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, + 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, + 0x49, 0x42, 0x41, 0x4C, 0x6C, 0x64, 0x55, 0x53, 0x68, 0x4C, 0x50, 0x44, 0x65, 0x53, 0x30, 0x59, 0x4C, 0x4F, 0x76, 0x52, 0x32, 0x39, 0x0A, 0x7A, 0x64, 0x32, 0x34, 0x71, 0x38, 0x38, 0x4B, 0x50, 0x75, 0x46, 0x64, 0x35, 0x64, 0x79, 0x71, 0x43, + 0x62, 0x6C, 0x58, 0x41, 0x6A, 0x37, 0x6D, 0x59, 0x32, 0x48, 0x66, 0x38, 0x67, 0x2B, 0x43, 0x59, 0x36, 0x36, 0x6A, 0x39, 0x36, 0x78, 0x7A, 0x30, 0x58, 0x7A, 0x6E, 0x73, 0x77, 0x75, 0x76, 0x43, 0x41, 0x41, 0x4A, 0x57, 0x58, 0x2F, 0x4E, 0x4B, + 0x53, 0x71, 0x49, 0x6B, 0x34, 0x63, 0x58, 0x47, 0x49, 0x44, 0x74, 0x69, 0x4C, 0x4B, 0x30, 0x74, 0x68, 0x41, 0x66, 0x0A, 0x4C, 0x64, 0x5A, 0x66, 0x56, 0x61, 0x49, 0x54, 0x58, 0x64, 0x48, 0x47, 0x36, 0x77, 0x5A, 0x57, 0x69, 0x59, 0x6A, 0x2B, + 0x72, 0x44, 0x4B, 0x64, 0x2F, 0x56, 0x7A, 0x44, 0x42, 0x63, 0x64, 0x75, 0x37, 0x6F, 0x61, 0x4A, 0x75, 0x6F, 0x67, 0x44, 0x6E, 0x58, 0x49, 0x68, 0x68, 0x70, 0x43, 0x75, 0x6A, 0x77, 0x4F, 0x6C, 0x33, 0x4A, 0x2B, 0x49, 0x4B, 0x4D, 0x75, 0x6A, + 0x6B, 0x6B, 0x6B, 0x50, 0x37, 0x4E, 0x41, 0x50, 0x34, 0x6D, 0x31, 0x45, 0x54, 0x34, 0x42, 0x71, 0x0A, 0x73, 0x74, 0x54, 0x6E, 0x6F, 0x41, 0x70, 0x54, 0x41, 0x62, 0x71, 0x4F, 0x6C, 0x35, 0x46, 0x32, 0x62, 0x72, 0x7A, 0x38, 0x31, 0x57, 0x73, + 0x32, 0x35, 0x6B, 0x43, 0x49, 0x31, 0x6E, 0x73, 0x76, 0x58, 0x77, 0x58, 0x6F, 0x4C, 0x47, 0x30, 0x52, 0x38, 0x2B, 0x65, 0x79, 0x76, 0x70, 0x4A, 0x45, 0x54, 0x4E, 0x4B, 0x58, 0x70, 0x50, 0x37, 0x53, 0x63, 0x6F, 0x46, 0x44, 0x42, 0x35, 0x7A, + 0x70, 0x45, 0x54, 0x37, 0x31, 0x69, 0x78, 0x70, 0x5A, 0x66, 0x52, 0x39, 0x6F, 0x0A, 0x57, 0x4E, 0x30, 0x45, 0x41, 0x43, 0x79, 0x57, 0x38, 0x30, 0x4F, 0x7A, 0x66, 0x70, 0x67, 0x5A, 0x64, 0x4E, 0x6D, 0x63, 0x63, 0x39, 0x6B, 0x59, 0x76, 0x6B, + 0x48, 0x48, 0x4E, 0x48, 0x6E, 0x5A, 0x39, 0x47, 0x4C, 0x43, 0x51, 0x37, 0x6D, 0x7A, 0x4A, 0x37, 0x41, 0x69, 0x79, 0x2F, 0x6B, 0x39, 0x55, 0x73, 0x63, 0x77, 0x52, 0x37, 0x50, 0x4A, 0x50, 0x72, 0x68, 0x71, 0x34, 0x75, 0x66, 0x6F, 0x67, 0x58, + 0x42, 0x65, 0x51, 0x6F, 0x74, 0x50, 0x4A, 0x71, 0x58, 0x2B, 0x0A, 0x4F, 0x73, 0x49, 0x67, 0x62, 0x72, 0x76, 0x34, 0x46, 0x6F, 0x37, 0x4E, 0x44, 0x4B, 0x6D, 0x30, 0x47, 0x32, 0x78, 0x32, 0x45, 0x4F, 0x46, 0x59, 0x65, 0x55, 0x59, 0x2B, 0x56, + 0x4D, 0x36, 0x41, 0x71, 0x46, 0x63, 0x4A, 0x4E, 0x79, 0x6B, 0x62, 0x6D, 0x52, 0x4F, 0x50, 0x44, 0x4D, 0x6A, 0x57, 0x4C, 0x42, 0x7A, 0x37, 0x42, 0x65, 0x67, 0x49, 0x6C, 0x54, 0x31, 0x6C, 0x52, 0x74, 0x7A, 0x75, 0x7A, 0x57, 0x6E, 0x69, 0x54, + 0x59, 0x2B, 0x48, 0x4B, 0x45, 0x34, 0x30, 0x0A, 0x43, 0x7A, 0x37, 0x50, 0x46, 0x4E, 0x6D, 0x37, 0x33, 0x62, 0x5A, 0x51, 0x6D, 0x71, 0x31, 0x33, 0x31, 0x42, 0x6E, 0x57, 0x32, 0x68, 0x71, 0x49, 0x79, 0x45, 0x34, 0x62, 0x4A, 0x33, 0x58, 0x59, + 0x73, 0x67, 0x6A, 0x78, 0x72, 0x6F, 0x4D, 0x77, 0x75, 0x52, 0x45, 0x4F, 0x7A, 0x59, 0x66, 0x77, 0x68, 0x49, 0x30, 0x56, 0x63, 0x6E, 0x79, 0x68, 0x37, 0x38, 0x7A, 0x79, 0x69, 0x47, 0x47, 0x36, 0x39, 0x47, 0x6D, 0x37, 0x44, 0x49, 0x77, 0x4C, + 0x64, 0x56, 0x63, 0x45, 0x0A, 0x75, 0x45, 0x34, 0x71, 0x46, 0x43, 0x34, 0x39, 0x44, 0x78, 0x77, 0x65, 0x4D, 0x71, 0x5A, 0x69, 0x4E, 0x75, 0x35, 0x6D, 0x34, 0x69, 0x4B, 0x34, 0x42, 0x55, 0x42, 0x6A, 0x45, 0x43, 0x4C, 0x7A, 0x4D, 0x78, 0x31, + 0x30, 0x63, 0x6F, 0x6F, 0x73, 0x39, 0x54, 0x6B, 0x70, 0x6F, 0x4E, 0x50, 0x6E, 0x47, 0x34, 0x43, 0x45, 0x4C, 0x63, 0x55, 0x39, 0x34, 0x30, 0x32, 0x78, 0x2F, 0x52, 0x70, 0x76, 0x75, 0x6D, 0x55, 0x48, 0x4F, 0x31, 0x6A, 0x73, 0x51, 0x6B, 0x55, + 0x6D, 0x0A, 0x2B, 0x39, 0x6A, 0x61, 0x4A, 0x58, 0x4C, 0x45, 0x39, 0x67, 0x43, 0x78, 0x49, 0x6E, 0x6D, 0x39, 0x34, 0x33, 0x78, 0x5A, 0x59, 0x6B, 0x71, 0x63, 0x42, 0x57, 0x38, 0x39, 0x7A, 0x75, 0x62, 0x57, 0x52, 0x32, 0x4F, 0x5A, 0x78, 0x69, + 0x52, 0x76, 0x63, 0x68, 0x4C, 0x49, 0x72, 0x48, 0x2B, 0x51, 0x74, 0x41, 0x75, 0x52, 0x63, 0x4F, 0x69, 0x33, 0x35, 0x68, 0x59, 0x51, 0x63, 0x52, 0x66, 0x4F, 0x33, 0x67, 0x5A, 0x50, 0x53, 0x45, 0x46, 0x39, 0x4E, 0x55, 0x71, 0x6A, 0x0A, 0x69, + 0x66, 0x4C, 0x4A, 0x53, 0x33, 0x74, 0x42, 0x45, 0x57, 0x31, 0x6E, 0x74, 0x77, 0x69, 0x59, 0x54, 0x4F, 0x55, 0x52, 0x47, 0x61, 0x35, 0x43, 0x67, 0x4E, 0x7A, 0x37, 0x6B, 0x41, 0x58, 0x55, 0x2B, 0x46, 0x44, 0x4B, 0x76, 0x75, 0x53, 0x74, 0x78, + 0x38, 0x4B, 0x55, 0x31, 0x78, 0x61, 0x64, 0x35, 0x68, 0x65, 0x50, 0x72, 0x7A, 0x62, 0x37, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x45, 0x77, 0x45, 0x42, + 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4A, 0x6E, 0x67, 0x47, 0x57, 0x63, 0x4E, 0x59, 0x74, 0x74, 0x32, 0x73, 0x39, 0x6F, 0x39, + 0x75, 0x46, 0x76, 0x6F, 0x2F, 0x55, 0x4C, 0x53, 0x4D, 0x51, 0x36, 0x48, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x0A, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, + 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x6D, 0x48, 0x4E, 0x77, 0x34, 0x72, 0x44, 0x54, 0x37, 0x54, 0x6E, 0x73, 0x54, 0x47, 0x44, 0x5A, 0x71, 0x52, 0x4B, + 0x47, 0x46, 0x78, 0x36, 0x57, 0x30, 0x4F, 0x68, 0x55, 0x4B, 0x44, 0x74, 0x6B, 0x4C, 0x53, 0x47, 0x6D, 0x2B, 0x4A, 0x31, 0x57, 0x45, 0x32, 0x70, 0x49, 0x50, 0x55, 0x2F, 0x48, 0x0A, 0x50, 0x69, 0x6E, 0x62, 0x62, 0x56, 0x69, 0x44, 0x56, 0x44, + 0x32, 0x48, 0x66, 0x53, 0x4D, 0x46, 0x31, 0x4F, 0x51, 0x63, 0x33, 0x4F, 0x67, 0x34, 0x5A, 0x59, 0x62, 0x46, 0x64, 0x61, 0x64, 0x61, 0x32, 0x7A, 0x55, 0x46, 0x76, 0x58, 0x66, 0x65, 0x75, 0x79, 0x6B, 0x33, 0x51, 0x41, 0x55, 0x48, 0x77, 0x35, + 0x52, 0x53, 0x6E, 0x38, 0x70, 0x6B, 0x33, 0x66, 0x45, 0x62, 0x4B, 0x39, 0x78, 0x47, 0x43, 0x68, 0x41, 0x43, 0x4D, 0x66, 0x31, 0x4B, 0x61, 0x41, 0x30, 0x48, 0x0A, 0x5A, 0x4A, 0x44, 0x6D, 0x48, 0x76, 0x55, 0x71, 0x6F, 0x61, 0x69, 0x37, 0x50, + 0x46, 0x33, 0x35, 0x6F, 0x77, 0x67, 0x4C, 0x45, 0x51, 0x7A, 0x78, 0x50, 0x79, 0x30, 0x51, 0x6C, 0x47, 0x2F, 0x2B, 0x34, 0x6A, 0x53, 0x48, 0x67, 0x39, 0x62, 0x50, 0x35, 0x52, 0x73, 0x31, 0x62, 0x64, 0x49, 0x44, 0x34, 0x62, 0x41, 0x4E, 0x71, + 0x4B, 0x43, 0x71, 0x52, 0x69, 0x65, 0x43, 0x4E, 0x71, 0x63, 0x56, 0x74, 0x67, 0x69, 0x6D, 0x51, 0x6C, 0x52, 0x58, 0x74, 0x70, 0x6C, 0x61, 0x0A, 0x34, 0x67, 0x74, 0x35, 0x6B, 0x4E, 0x64, 0x58, 0x45, 0x6C, 0x45, 0x31, 0x47, 0x59, 0x68, 0x42, + 0x61, 0x43, 0x58, 0x55, 0x4E, 0x78, 0x65, 0x45, 0x46, 0x66, 0x73, 0x42, 0x63, 0x74, 0x79, 0x56, 0x33, 0x6C, 0x49, 0x6D, 0x49, 0x4A, 0x67, 0x6D, 0x34, 0x6E, 0x62, 0x31, 0x4A, 0x32, 0x2F, 0x36, 0x41, 0x44, 0x74, 0x4B, 0x59, 0x64, 0x6B, 0x4E, + 0x79, 0x31, 0x47, 0x54, 0x4B, 0x76, 0x30, 0x57, 0x42, 0x70, 0x61, 0x6E, 0x49, 0x35, 0x6F, 0x6A, 0x53, 0x50, 0x35, 0x52, 0x0A, 0x76, 0x62, 0x62, 0x45, 0x73, 0x4C, 0x46, 0x55, 0x7A, 0x74, 0x35, 0x73, 0x51, 0x61, 0x30, 0x57, 0x5A, 0x33, 0x37, + 0x62, 0x2F, 0x54, 0x6A, 0x4E, 0x75, 0x54, 0x68, 0x4F, 0x73, 0x73, 0x46, 0x67, 0x79, 0x35, 0x30, 0x58, 0x33, 0x31, 0x69, 0x65, 0x65, 0x6D, 0x4B, 0x79, 0x4A, 0x6F, 0x39, 0x30, 0x6C, 0x5A, 0x76, 0x6B, 0x57, 0x78, 0x33, 0x53, 0x44, 0x39, 0x32, + 0x59, 0x48, 0x4A, 0x74, 0x5A, 0x75, 0x53, 0x50, 0x54, 0x4D, 0x61, 0x43, 0x6D, 0x2F, 0x7A, 0x6A, 0x64, 0x0A, 0x7A, 0x79, 0x42, 0x50, 0x36, 0x56, 0x68, 0x57, 0x4F, 0x6D, 0x66, 0x44, 0x30, 0x66, 0x61, 0x5A, 0x6D, 0x5A, 0x32, 0x36, 0x4E, 0x72, + 0x61, 0x41, 0x4C, 0x34, 0x68, 0x48, 0x54, 0x34, 0x61, 0x2F, 0x52, 0x44, 0x71, 0x41, 0x35, 0x44, 0x63, 0x63, 0x70, 0x72, 0x72, 0x71, 0x6C, 0x35, 0x67, 0x52, 0x30, 0x49, 0x52, 0x69, 0x52, 0x32, 0x51, 0x65, 0x71, 0x75, 0x35, 0x41, 0x76, 0x7A, + 0x53, 0x78, 0x6E, 0x49, 0x39, 0x4F, 0x34, 0x66, 0x4B, 0x53, 0x54, 0x78, 0x2B, 0x4F, 0x0A, 0x38, 0x35, 0x36, 0x58, 0x33, 0x76, 0x4F, 0x6D, 0x65, 0x57, 0x71, 0x4A, 0x63, 0x55, 0x39, 0x4C, 0x4A, 0x78, 0x64, 0x49, 0x2F, 0x75, 0x7A, 0x30, 0x55, + 0x41, 0x39, 0x50, 0x53, 0x58, 0x33, 0x4D, 0x52, 0x65, 0x4F, 0x39, 0x65, 0x6B, 0x44, 0x46, 0x51, 0x64, 0x78, 0x68, 0x56, 0x69, 0x63, 0x47, 0x61, 0x65, 0x56, 0x79, 0x51, 0x59, 0x48, 0x54, 0x74, 0x67, 0x47, 0x4A, 0x6F, 0x43, 0x38, 0x36, 0x63, + 0x6E, 0x6E, 0x2B, 0x4F, 0x6A, 0x43, 0x2F, 0x51, 0x65, 0x7A, 0x48, 0x0A, 0x59, 0x6A, 0x36, 0x52, 0x53, 0x38, 0x66, 0x5A, 0x4D, 0x58, 0x5A, 0x43, 0x2B, 0x66, 0x63, 0x38, 0x59, 0x2B, 0x77, 0x6D, 0x6A, 0x48, 0x4D, 0x4D, 0x66, 0x52, 0x6F, 0x64, + 0x36, 0x71, 0x68, 0x38, 0x68, 0x36, 0x6A, 0x43, 0x4A, 0x33, 0x7A, 0x68, 0x4D, 0x30, 0x45, 0x50, 0x7A, 0x38, 0x2F, 0x38, 0x41, 0x4B, 0x41, 0x69, 0x67, 0x4A, 0x35, 0x4B, 0x70, 0x32, 0x38, 0x41, 0x73, 0x45, 0x46, 0x46, 0x74, 0x79, 0x4C, 0x4B, + 0x61, 0x45, 0x6A, 0x46, 0x51, 0x71, 0x4B, 0x75, 0x0A, 0x33, 0x52, 0x33, 0x79, 0x34, 0x47, 0x35, 0x4F, 0x42, 0x56, 0x69, 0x78, 0x77, 0x4A, 0x41, 0x57, 0x4B, 0x71, 0x51, 0x39, 0x45, 0x45, 0x43, 0x2B, 0x6A, 0x32, 0x4A, 0x6A, 0x67, 0x36, 0x6D, + 0x63, 0x67, 0x6E, 0x30, 0x74, 0x41, 0x75, 0x6D, 0x44, 0x4D, 0x48, 0x7A, 0x4C, 0x4A, 0x38, 0x6E, 0x39, 0x48, 0x6D, 0x59, 0x41, 0x73, 0x43, 0x37, 0x54, 0x49, 0x53, 0x2B, 0x4F, 0x4D, 0x78, 0x5A, 0x73, 0x6D, 0x4F, 0x30, 0x51, 0x71, 0x41, 0x66, + 0x57, 0x7A, 0x4A, 0x50, 0x50, 0x0A, 0x32, 0x39, 0x46, 0x70, 0x48, 0x4F, 0x54, 0x4B, 0x79, 0x65, 0x43, 0x32, 0x6E, 0x4F, 0x6E, 0x4F, 0x63, 0x58, 0x48, 0x65, 0x62, 0x44, 0x38, 0x57, 0x70, 0x48, 0x6B, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x77, 0x61, 0x76, 0x65, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x45, + 0x43, 0x43, 0x20, 0x50, 0x32, 0x35, 0x36, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x59, 0x44, 0x43, 0x43, 0x41, 0x67, + 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4D, 0x44, 0x57, 0x70, 0x66, 0x43, 0x44, 0x38, 0x6F, 0x58, 0x44, 0x35, 0x52, 0x6C, 0x64, 0x39, 0x64, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, + 0x4D, 0x43, 0x4D, 0x49, 0x47, 0x52, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x52, 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x42, 0x4D, 0x49, 0x53, + 0x57, 0x78, 0x73, 0x61, 0x57, 0x35, 0x76, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x42, 0x30, 0x4E, 0x6F, 0x61, 0x57, 0x4E, 0x68, 0x5A, 0x32, 0x38, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x64, 0x32, 0x46, 0x32, 0x5A, 0x53, 0x42, 0x49, 0x0A, 0x62, 0x32, 0x78, 0x6B, 0x61, 0x57, 0x35, 0x6E, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, + 0x4C, 0x6A, 0x45, 0x36, 0x4D, 0x44, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x78, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x33, 0x59, 0x58, 0x5A, 0x6C, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x46, + 0x51, 0x30, 0x4D, 0x67, 0x55, 0x44, 0x49, 0x31, 0x4E, 0x69, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x0A, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, + 0x30, 0x65, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x34, 0x4D, 0x6A, 0x4D, 0x78, 0x4F, 0x54, 0x4D, 0x31, 0x4D, 0x54, 0x42, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x34, 0x4D, 0x6A, 0x4D, 0x78, 0x4F, 0x54, 0x4D, + 0x31, 0x4D, 0x54, 0x42, 0x61, 0x4D, 0x49, 0x47, 0x52, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x42, + 0x4D, 0x49, 0x53, 0x57, 0x78, 0x73, 0x61, 0x57, 0x35, 0x76, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x42, 0x30, 0x4E, 0x6F, 0x61, 0x57, 0x4E, 0x68, 0x5A, 0x32, 0x38, 0x78, 0x49, 0x54, + 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x46, 0x52, 0x79, 0x0A, 0x64, 0x58, 0x4E, 0x30, 0x64, 0x32, 0x46, 0x32, 0x5A, 0x53, 0x42, 0x49, 0x62, 0x32, 0x78, 0x6B, 0x61, 0x57, 0x35, 0x6E, 0x63, 0x79, 0x77, 0x67, 0x53, + 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x36, 0x4D, 0x44, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x78, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x33, 0x59, 0x58, 0x5A, 0x6C, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, + 0x43, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x44, 0x49, 0x31, 0x0A, 0x4E, 0x69, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, + 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x42, 0x5A, 0x4D, 0x42, 0x4D, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x77, 0x45, 0x48, 0x41, 0x30, 0x49, 0x41, + 0x42, 0x48, 0x37, 0x37, 0x62, 0x4F, 0x59, 0x6A, 0x0A, 0x34, 0x33, 0x4D, 0x79, 0x43, 0x4D, 0x70, 0x67, 0x35, 0x6C, 0x4F, 0x63, 0x75, 0x6E, 0x53, 0x4E, 0x47, 0x4C, 0x42, 0x34, 0x6B, 0x46, 0x4B, 0x41, 0x33, 0x54, 0x6A, 0x41, 0x53, 0x68, 0x33, + 0x52, 0x71, 0x4D, 0x79, 0x54, 0x70, 0x4A, 0x63, 0x47, 0x4F, 0x4D, 0x6F, 0x4E, 0x46, 0x57, 0x4C, 0x47, 0x6A, 0x67, 0x45, 0x71, 0x5A, 0x5A, 0x32, 0x71, 0x33, 0x7A, 0x53, 0x52, 0x4C, 0x6F, 0x48, 0x42, 0x35, 0x44, 0x4F, 0x53, 0x4D, 0x63, 0x54, + 0x39, 0x43, 0x54, 0x71, 0x6D, 0x0A, 0x50, 0x36, 0x32, 0x6A, 0x51, 0x7A, 0x42, 0x42, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x77, + 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x44, 0x41, 0x77, 0x63, 0x47, 0x41, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x6F, 0x30, 0x45, 0x47, 0x72, 0x4A, + 0x42, 0x74, 0x0A, 0x30, 0x55, 0x72, 0x72, 0x64, 0x61, 0x56, 0x4B, 0x45, 0x4A, 0x6D, 0x7A, 0x73, 0x61, 0x47, 0x4C, 0x53, 0x76, 0x63, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x49, 0x44, 0x52, + 0x77, 0x41, 0x77, 0x52, 0x41, 0x49, 0x67, 0x42, 0x2B, 0x5A, 0x55, 0x32, 0x67, 0x36, 0x67, 0x57, 0x72, 0x4B, 0x75, 0x45, 0x5A, 0x2B, 0x48, 0x78, 0x62, 0x62, 0x2F, 0x61, 0x64, 0x34, 0x6C, 0x76, 0x76, 0x69, 0x67, 0x74, 0x77, 0x6A, 0x7A, 0x0A, + 0x52, 0x4D, 0x34, 0x71, 0x33, 0x77, 0x67, 0x68, 0x44, 0x44, 0x63, 0x43, 0x49, 0x43, 0x30, 0x6D, 0x41, 0x36, 0x41, 0x46, 0x76, 0x57, 0x76, 0x52, 0x39, 0x6C, 0x7A, 0x34, 0x5A, 0x63, 0x79, 0x47, 0x62, 0x62, 0x4F, 0x63, 0x4E, 0x45, 0x68, 0x6A, + 0x68, 0x41, 0x6E, 0x46, 0x6A, 0x58, 0x63, 0x61, 0x34, 0x73, 0x79, 0x63, 0x34, 0x58, 0x52, 0x37, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x77, 0x61, 0x76, 0x65, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x45, 0x43, 0x43, 0x20, 0x50, 0x33, 0x38, 0x34, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x6E, 0x54, 0x43, 0x43, 0x41, 0x69, 0x53, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4D, 0x43, 0x4C, 0x32, 0x46, 0x6C, 0x32, 0x79, 0x5A, + 0x4A, 0x36, 0x53, 0x41, 0x61, 0x45, 0x63, 0x37, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x4D, 0x49, 0x47, 0x52, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, + 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x52, 0x0A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x42, 0x4D, 0x49, 0x53, 0x57, 0x78, 0x73, 0x61, 0x57, 0x35, 0x76, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x63, 0x54, 0x42, 0x30, 0x4E, 0x6F, 0x61, 0x57, 0x4E, 0x68, 0x5A, 0x32, 0x38, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x64, 0x32, 0x46, + 0x32, 0x5A, 0x53, 0x42, 0x49, 0x0A, 0x62, 0x32, 0x78, 0x6B, 0x61, 0x57, 0x35, 0x6E, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x36, 0x4D, 0x44, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x78, 0x56, 0x48, + 0x4A, 0x31, 0x63, 0x33, 0x52, 0x33, 0x59, 0x58, 0x5A, 0x6C, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x44, 0x4D, 0x34, 0x4E, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, + 0x5A, 0x70, 0x0A, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x7A, 0x41, 0x34, 0x4D, 0x6A, 0x4D, 0x78, 0x4F, + 0x54, 0x4D, 0x32, 0x4E, 0x44, 0x4E, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x6A, 0x41, 0x34, 0x4D, 0x6A, 0x4D, 0x78, 0x4F, 0x54, 0x4D, 0x32, 0x4E, 0x44, 0x4E, 0x61, 0x4D, 0x49, 0x47, 0x52, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x0A, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x42, 0x4D, 0x49, 0x53, 0x57, 0x78, 0x73, 0x61, 0x57, 0x35, 0x76, 0x61, 0x58, 0x4D, 0x78, 0x45, 0x44, 0x41, 0x4F, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x63, 0x54, 0x42, 0x30, 0x4E, 0x6F, 0x61, 0x57, 0x4E, 0x68, 0x5A, 0x32, 0x38, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x46, 0x52, 0x79, 0x0A, 0x64, 0x58, 0x4E, + 0x30, 0x64, 0x32, 0x46, 0x32, 0x5A, 0x53, 0x42, 0x49, 0x62, 0x32, 0x78, 0x6B, 0x61, 0x57, 0x35, 0x6E, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x36, 0x4D, 0x44, 0x67, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, + 0x78, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x33, 0x59, 0x58, 0x5A, 0x6C, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x44, 0x4D, 0x34, 0x0A, 0x4E, 0x43, 0x42, 0x44, 0x5A, 0x58, + 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, + 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x47, 0x76, 0x61, 0x44, 0x58, 0x55, 0x31, 0x43, 0x44, 0x46, 0x48, 0x0A, 0x42, 0x61, 0x35, 0x46, 0x6D, 0x56, 0x58, 0x78, 0x45, + 0x52, 0x4D, 0x75, 0x53, 0x76, 0x67, 0x51, 0x4D, 0x53, 0x4F, 0x6A, 0x66, 0x6F, 0x50, 0x54, 0x66, 0x79, 0x67, 0x49, 0x4F, 0x69, 0x59, 0x61, 0x4F, 0x73, 0x2B, 0x58, 0x67, 0x68, 0x2B, 0x41, 0x74, 0x79, 0x63, 0x4A, 0x6A, 0x39, 0x47, 0x4F, 0x4D, + 0x4D, 0x51, 0x4B, 0x6D, 0x77, 0x36, 0x73, 0x57, 0x41, 0x53, 0x72, 0x39, 0x7A, 0x5A, 0x39, 0x6C, 0x43, 0x4F, 0x6B, 0x6D, 0x77, 0x71, 0x4B, 0x69, 0x36, 0x76, 0x72, 0x0A, 0x2F, 0x54, 0x6B, 0x6C, 0x5A, 0x76, 0x46, 0x65, 0x2F, 0x6F, 0x79, 0x75, + 0x6A, 0x55, 0x46, 0x35, 0x6E, 0x51, 0x6C, 0x67, 0x7A, 0x69, 0x69, 0x70, 0x30, 0x34, 0x70, 0x74, 0x38, 0x39, 0x5A, 0x46, 0x31, 0x50, 0x4B, 0x59, 0x68, 0x44, 0x68, 0x6C, 0x6F, 0x4B, 0x4E, 0x44, 0x4D, 0x45, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, + 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x51, 0x4D, 0x44, 0x42, 0x77, 0x59, + 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x56, 0x71, 0x59, 0x53, 0x4A, 0x30, 0x73, 0x45, 0x79, 0x76, 0x52, 0x6A, 0x4C, 0x62, 0x4B, 0x59, 0x48, 0x54, 0x73, 0x6A, 0x6E, 0x6E, 0x62, 0x36, + 0x43, 0x6B, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6E, 0x0A, 0x41, 0x44, 0x42, 0x6B, 0x41, 0x6A, 0x41, 0x33, 0x41, 0x5A, 0x4B, 0x58, 0x52, 0x52, 0x4A, 0x2B, 0x6F, 0x50, + 0x4D, 0x2B, 0x72, 0x52, 0x6B, 0x36, 0x63, 0x74, 0x33, 0x30, 0x55, 0x4A, 0x4D, 0x44, 0x45, 0x72, 0x35, 0x45, 0x30, 0x6B, 0x39, 0x42, 0x70, 0x49, 0x79, 0x63, 0x6E, 0x52, 0x2B, 0x6A, 0x39, 0x73, 0x4B, 0x53, 0x35, 0x30, 0x67, 0x55, 0x2F, 0x6B, + 0x36, 0x62, 0x70, 0x5A, 0x46, 0x58, 0x72, 0x73, 0x59, 0x33, 0x63, 0x72, 0x73, 0x43, 0x4D, 0x47, 0x63, 0x6C, 0x0A, 0x43, 0x72, 0x45, 0x4D, 0x58, 0x75, 0x36, 0x70, 0x59, 0x35, 0x4A, 0x76, 0x35, 0x5A, 0x41, 0x4C, 0x2F, 0x6D, 0x59, 0x69, 0x79, + 0x6B, 0x66, 0x39, 0x69, 0x6A, 0x48, 0x33, 0x67, 0x2F, 0x35, 0x36, 0x76, 0x78, 0x43, 0x2B, 0x47, 0x43, 0x73, 0x65, 0x6A, 0x2F, 0x59, 0x70, 0x48, 0x70, 0x52, 0x5A, 0x37, 0x34, 0x34, 0x68, 0x4E, 0x38, 0x74, 0x52, 0x6D, 0x4B, 0x56, 0x75, 0x53, + 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4E, 0x41, 0x56, 0x45, 0x52, 0x20, 0x47, 0x6C, 0x6F, + 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x6F, 0x6A, 0x43, 0x43, 0x41, 0x34, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, + 0x67, 0x49, 0x55, 0x41, 0x5A, 0x51, 0x77, 0x48, 0x71, 0x49, 0x4C, 0x33, 0x66, 0x58, 0x46, 0x4D, 0x79, 0x71, 0x78, 0x51, 0x30, 0x52, 0x78, 0x2B, 0x4E, 0x5A, 0x51, 0x54, 0x51, 0x30, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, + 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x77, 0x61, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x31, 0x49, 0x78, 0x4A, 0x6A, 0x41, 0x6B, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x55, 0x35, 0x42, 0x56, 0x6B, 0x56, 0x53, 0x49, 0x45, 0x4A, 0x56, 0x55, 0x30, 0x6C, 0x4F, 0x52, 0x56, 0x4E, 0x54, 0x49, 0x46, 0x42, 0x4D, 0x51, 0x56, 0x52, 0x47, 0x54, 0x31, 0x4A, 0x4E, 0x49, 0x45, 0x4E, 0x76, + 0x63, 0x6E, 0x41, 0x75, 0x4D, 0x54, 0x49, 0x77, 0x4D, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x44, 0x43, 0x6C, 0x4F, 0x51, 0x56, 0x5A, 0x46, 0x55, 0x69, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x55, 0x6D, 0x39, + 0x76, 0x64, 0x43, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x68, 0x76, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, + 0x78, 0x4E, 0x7A, 0x41, 0x34, 0x4D, 0x54, 0x67, 0x77, 0x4F, 0x44, 0x55, 0x34, 0x0A, 0x4E, 0x44, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4E, 0x7A, 0x41, 0x34, 0x4D, 0x54, 0x67, 0x79, 0x4D, 0x7A, 0x55, 0x35, 0x4E, 0x54, 0x6C, 0x61, 0x4D, 0x47, + 0x6B, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x74, 0x53, 0x4D, 0x53, 0x59, 0x77, 0x4A, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x31, 0x4F, 0x51, 0x56, 0x5A, 0x46, 0x55, 0x69, + 0x42, 0x43, 0x56, 0x56, 0x4E, 0x4A, 0x54, 0x6B, 0x56, 0x54, 0x0A, 0x55, 0x79, 0x42, 0x51, 0x54, 0x45, 0x46, 0x55, 0x52, 0x6B, 0x39, 0x53, 0x54, 0x53, 0x42, 0x44, 0x62, 0x33, 0x4A, 0x77, 0x4C, 0x6A, 0x45, 0x79, 0x4D, 0x44, 0x41, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x70, 0x54, 0x6B, 0x46, 0x57, 0x52, 0x56, 0x49, 0x67, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, + 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x0A, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, + 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x32, 0x31, 0x50, 0x47, 0x54, 0x58, 0x4C, 0x56, 0x41, 0x69, 0x51, 0x71, 0x72, + 0x44, 0x5A, 0x42, 0x62, 0x0A, 0x55, 0x47, 0x4F, 0x75, 0x6B, 0x4A, 0x52, 0x30, 0x46, 0x30, 0x56, 0x79, 0x31, 0x6E, 0x74, 0x6C, 0x57, 0x69, 0x6C, 0x4C, 0x70, 0x31, 0x61, 0x67, 0x53, 0x37, 0x67, 0x76, 0x51, 0x6E, 0x58, 0x70, 0x32, 0x58, 0x73, + 0x6B, 0x57, 0x6A, 0x46, 0x6C, 0x71, 0x78, 0x63, 0x58, 0x30, 0x54, 0x4D, 0x36, 0x32, 0x52, 0x48, 0x63, 0x51, 0x44, 0x61, 0x48, 0x33, 0x38, 0x64, 0x71, 0x36, 0x53, 0x5A, 0x65, 0x57, 0x59, 0x70, 0x33, 0x34, 0x2B, 0x68, 0x49, 0x6E, 0x44, 0x45, + 0x57, 0x0A, 0x2B, 0x6A, 0x36, 0x52, 0x73, 0x63, 0x72, 0x4A, 0x6F, 0x2B, 0x4B, 0x66, 0x7A, 0x69, 0x46, 0x54, 0x6F, 0x77, 0x49, 0x32, 0x4D, 0x4D, 0x74, 0x53, 0x41, 0x75, 0x58, 0x61, 0x4D, 0x6C, 0x33, 0x44, 0x78, 0x65, 0x62, 0x35, 0x37, 0x68, + 0x48, 0x48, 0x69, 0x38, 0x6C, 0x45, 0x48, 0x6F, 0x53, 0x54, 0x47, 0x45, 0x71, 0x30, 0x6E, 0x2B, 0x55, 0x53, 0x5A, 0x47, 0x6E, 0x51, 0x4A, 0x6F, 0x56, 0x69, 0x41, 0x62, 0x62, 0x4A, 0x41, 0x68, 0x32, 0x2B, 0x67, 0x31, 0x47, 0x37, 0x0A, 0x58, + 0x4E, 0x72, 0x34, 0x72, 0x52, 0x56, 0x71, 0x6D, 0x66, 0x65, 0x53, 0x56, 0x50, 0x63, 0x30, 0x57, 0x2B, 0x6D, 0x2F, 0x36, 0x69, 0x6D, 0x42, 0x45, 0x74, 0x52, 0x54, 0x6B, 0x5A, 0x61, 0x7A, 0x6B, 0x56, 0x72, 0x64, 0x2F, 0x70, 0x42, 0x7A, 0x4B, + 0x50, 0x73, 0x77, 0x52, 0x72, 0x58, 0x4B, 0x43, 0x41, 0x66, 0x48, 0x63, 0x58, 0x4C, 0x4A, 0x5A, 0x74, 0x4D, 0x30, 0x6C, 0x2F, 0x61, 0x4D, 0x39, 0x42, 0x68, 0x4B, 0x34, 0x64, 0x41, 0x39, 0x57, 0x6B, 0x57, 0x32, 0x0A, 0x61, 0x61, 0x63, 0x70, + 0x2B, 0x79, 0x50, 0x4F, 0x69, 0x4E, 0x67, 0x53, 0x6E, 0x41, 0x42, 0x49, 0x71, 0x4B, 0x59, 0x50, 0x73, 0x7A, 0x75, 0x53, 0x6A, 0x58, 0x45, 0x4F, 0x64, 0x4D, 0x57, 0x4C, 0x79, 0x45, 0x7A, 0x35, 0x39, 0x4A, 0x75, 0x4F, 0x75, 0x44, 0x78, 0x70, + 0x37, 0x57, 0x38, 0x37, 0x55, 0x43, 0x39, 0x59, 0x37, 0x63, 0x53, 0x77, 0x30, 0x42, 0x77, 0x62, 0x61, 0x67, 0x7A, 0x69, 0x76, 0x45, 0x53, 0x71, 0x32, 0x4D, 0x30, 0x55, 0x58, 0x5A, 0x52, 0x34, 0x0A, 0x59, 0x62, 0x38, 0x4F, 0x62, 0x74, 0x6F, + 0x71, 0x76, 0x43, 0x38, 0x4D, 0x43, 0x33, 0x47, 0x6D, 0x73, 0x78, 0x59, 0x2F, 0x6E, 0x4F, 0x62, 0x35, 0x7A, 0x4A, 0x39, 0x54, 0x4E, 0x65, 0x49, 0x44, 0x6F, 0x4B, 0x41, 0x59, 0x76, 0x37, 0x76, 0x78, 0x76, 0x76, 0x54, 0x57, 0x6A, 0x49, 0x63, + 0x4E, 0x51, 0x76, 0x63, 0x47, 0x75, 0x66, 0x46, 0x74, 0x37, 0x51, 0x53, 0x55, 0x71, 0x50, 0x36, 0x32, 0x30, 0x77, 0x62, 0x47, 0x51, 0x47, 0x48, 0x66, 0x6E, 0x5A, 0x33, 0x7A, 0x0A, 0x56, 0x48, 0x62, 0x4F, 0x55, 0x7A, 0x6F, 0x42, 0x70, 0x70, + 0x4A, 0x42, 0x37, 0x41, 0x53, 0x6A, 0x6A, 0x77, 0x32, 0x69, 0x31, 0x51, 0x6E, 0x4B, 0x31, 0x73, 0x75, 0x61, 0x38, 0x65, 0x39, 0x44, 0x58, 0x63, 0x43, 0x72, 0x70, 0x55, 0x48, 0x50, 0x58, 0x46, 0x4E, 0x77, 0x63, 0x4D, 0x6D, 0x49, 0x70, 0x69, + 0x33, 0x55, 0x61, 0x32, 0x46, 0x7A, 0x55, 0x43, 0x61, 0x47, 0x59, 0x51, 0x35, 0x66, 0x47, 0x38, 0x49, 0x72, 0x34, 0x6F, 0x7A, 0x56, 0x75, 0x35, 0x33, 0x42, 0x0A, 0x41, 0x30, 0x4B, 0x36, 0x6C, 0x4E, 0x70, 0x66, 0x71, 0x62, 0x44, 0x4B, 0x7A, + 0x45, 0x30, 0x4B, 0x37, 0x30, 0x64, 0x70, 0x41, 0x79, 0x38, 0x69, 0x2B, 0x2F, 0x45, 0x6F, 0x7A, 0x72, 0x39, 0x64, 0x55, 0x47, 0x57, 0x6F, 0x6B, 0x47, 0x32, 0x7A, 0x64, 0x4C, 0x41, 0x49, 0x78, 0x36, 0x79, 0x6F, 0x30, 0x65, 0x73, 0x2B, 0x6E, + 0x50, 0x78, 0x64, 0x47, 0x6F, 0x4D, 0x75, 0x4B, 0x38, 0x75, 0x31, 0x38, 0x30, 0x53, 0x64, 0x4F, 0x71, 0x63, 0x58, 0x59, 0x5A, 0x61, 0x69, 0x0A, 0x63, 0x64, 0x4E, 0x77, 0x6C, 0x68, 0x56, 0x4E, 0x74, 0x30, 0x78, 0x7A, 0x37, 0x68, 0x6C, 0x63, + 0x78, 0x56, 0x73, 0x2B, 0x51, 0x66, 0x36, 0x73, 0x64, 0x57, 0x41, 0x37, 0x47, 0x32, 0x50, 0x4F, 0x41, 0x4E, 0x33, 0x61, 0x43, 0x4A, 0x42, 0x69, 0x74, 0x4F, 0x55, 0x74, 0x37, 0x6B, 0x69, 0x6E, 0x61, 0x78, 0x65, 0x5A, 0x56, 0x4C, 0x36, 0x48, + 0x53, 0x75, 0x4F, 0x70, 0x58, 0x67, 0x52, 0x4D, 0x36, 0x78, 0x42, 0x74, 0x56, 0x4E, 0x62, 0x76, 0x38, 0x65, 0x6A, 0x79, 0x0A, 0x59, 0x68, 0x62, 0x4C, 0x67, 0x47, 0x76, 0x74, 0x50, 0x65, 0x33, 0x31, 0x48, 0x7A, 0x43, 0x6C, 0x72, 0x6B, 0x76, + 0x4A, 0x45, 0x2B, 0x32, 0x4B, 0x41, 0x51, 0x48, 0x4A, 0x75, 0x46, 0x46, 0x59, 0x77, 0x47, 0x59, 0x36, 0x73, 0x57, 0x5A, 0x4C, 0x78, 0x4E, 0x55, 0x78, 0x41, 0x6D, 0x4C, 0x70, 0x64, 0x49, 0x51, 0x4D, 0x32, 0x30, 0x31, 0x47, 0x4C, 0x51, 0x49, + 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x30, 0x70, 0x2B, 0x49, 0x33, 0x36, 0x48, 0x4E, 0x4C, 0x4C, 0x33, 0x73, 0x39, 0x54, + 0x73, 0x42, 0x41, 0x5A, 0x4D, 0x7A, 0x4A, 0x37, 0x4C, 0x72, 0x59, 0x45, 0x73, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, + 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x0A, 0x41, 0x66, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, + 0x67, 0x49, 0x42, 0x41, 0x44, 0x4C, 0x4B, 0x67, 0x4C, 0x4F, 0x64, 0x50, 0x56, 0x51, 0x47, 0x33, 0x64, 0x4C, 0x53, 0x4C, 0x76, 0x43, 0x6B, 0x41, 0x53, 0x45, 0x4C, 0x5A, 0x30, 0x6A, 0x4B, 0x62, 0x59, 0x37, 0x67, 0x79, 0x4B, 0x6F, 0x4E, 0x71, + 0x6F, 0x30, 0x68, 0x56, 0x34, 0x2F, 0x47, 0x50, 0x6E, 0x72, 0x4B, 0x0A, 0x32, 0x31, 0x48, 0x55, 0x55, 0x72, 0x50, 0x55, 0x6C, 0x6F, 0x53, 0x6C, 0x57, 0x47, 0x42, 0x2F, 0x35, 0x51, 0x75, 0x4F, 0x48, 0x2F, 0x58, 0x63, 0x43, 0x68, 0x57, 0x42, + 0x35, 0x54, 0x75, 0x32, 0x74, 0x79, 0x49, 0x76, 0x43, 0x5A, 0x77, 0x54, 0x46, 0x72, 0x46, 0x73, 0x44, 0x44, 0x55, 0x49, 0x62, 0x61, 0x74, 0x6A, 0x63, 0x75, 0x33, 0x63, 0x76, 0x75, 0x7A, 0x48, 0x56, 0x2B, 0x59, 0x77, 0x49, 0x48, 0x48, 0x57, + 0x31, 0x78, 0x44, 0x42, 0x45, 0x31, 0x55, 0x42, 0x0A, 0x6A, 0x43, 0x70, 0x44, 0x35, 0x45, 0x48, 0x78, 0x7A, 0x7A, 0x70, 0x36, 0x55, 0x35, 0x4C, 0x4F, 0x6F, 0x67, 0x4D, 0x46, 0x44, 0x54, 0x6A, 0x66, 0x41, 0x72, 0x73, 0x51, 0x4C, 0x74, 0x6B, + 0x37, 0x30, 0x70, 0x74, 0x36, 0x77, 0x4B, 0x47, 0x6D, 0x2B, 0x4C, 0x55, 0x78, 0x35, 0x76, 0x52, 0x31, 0x79, 0x62, 0x6C, 0x54, 0x6D, 0x58, 0x56, 0x48, 0x49, 0x6C, 0x6F, 0x55, 0x46, 0x63, 0x64, 0x34, 0x47, 0x37, 0x61, 0x64, 0x36, 0x51, 0x7A, + 0x34, 0x47, 0x33, 0x62, 0x78, 0x0A, 0x68, 0x59, 0x54, 0x65, 0x6F, 0x64, 0x6F, 0x53, 0x37, 0x36, 0x54, 0x69, 0x45, 0x4A, 0x64, 0x36, 0x65, 0x4E, 0x34, 0x4D, 0x55, 0x5A, 0x65, 0x6F, 0x49, 0x55, 0x43, 0x4C, 0x68, 0x72, 0x30, 0x4E, 0x38, 0x46, + 0x35, 0x4F, 0x53, 0x7A, 0x61, 0x37, 0x4F, 0x79, 0x41, 0x66, 0x69, 0x6B, 0x4A, 0x57, 0x34, 0x51, 0x73, 0x61, 0x76, 0x33, 0x76, 0x51, 0x49, 0x6B, 0x4D, 0x73, 0x52, 0x49, 0x7A, 0x37, 0x35, 0x53, 0x71, 0x30, 0x62, 0x42, 0x77, 0x63, 0x75, 0x70, + 0x54, 0x67, 0x0A, 0x45, 0x33, 0x34, 0x68, 0x35, 0x70, 0x72, 0x43, 0x79, 0x38, 0x56, 0x43, 0x5A, 0x4C, 0x51, 0x65, 0x6C, 0x48, 0x73, 0x49, 0x4A, 0x63, 0x68, 0x78, 0x7A, 0x49, 0x64, 0x46, 0x56, 0x34, 0x58, 0x54, 0x6E, 0x79, 0x6C, 0x69, 0x49, + 0x6F, 0x4E, 0x52, 0x6C, 0x77, 0x41, 0x59, 0x6C, 0x33, 0x64, 0x71, 0x6D, 0x4A, 0x4C, 0x4A, 0x66, 0x47, 0x42, 0x73, 0x33, 0x32, 0x78, 0x39, 0x53, 0x75, 0x52, 0x77, 0x54, 0x4D, 0x4B, 0x65, 0x75, 0x42, 0x33, 0x33, 0x30, 0x44, 0x54, 0x48, 0x0A, + 0x44, 0x38, 0x7A, 0x37, 0x70, 0x2F, 0x38, 0x44, 0x76, 0x71, 0x31, 0x77, 0x6B, 0x4E, 0x6F, 0x4C, 0x33, 0x63, 0x68, 0x74, 0x6C, 0x31, 0x2B, 0x61, 0x66, 0x77, 0x6B, 0x79, 0x51, 0x66, 0x33, 0x4E, 0x6F, 0x73, 0x78, 0x61, 0x62, 0x55, 0x7A, 0x79, + 0x71, 0x6B, 0x6E, 0x2B, 0x5A, 0x76, 0x6A, 0x70, 0x32, 0x44, 0x58, 0x72, 0x44, 0x69, 0x67, 0x65, 0x37, 0x6B, 0x67, 0x76, 0x4F, 0x74, 0x42, 0x35, 0x43, 0x54, 0x68, 0x38, 0x70, 0x69, 0x4B, 0x43, 0x6B, 0x35, 0x58, 0x51, 0x0A, 0x41, 0x37, 0x36, + 0x2B, 0x41, 0x71, 0x41, 0x46, 0x33, 0x53, 0x41, 0x69, 0x34, 0x32, 0x38, 0x64, 0x69, 0x44, 0x52, 0x67, 0x78, 0x75, 0x59, 0x4B, 0x75, 0x51, 0x6C, 0x31, 0x43, 0x2F, 0x41, 0x48, 0x36, 0x47, 0x6D, 0x57, 0x4E, 0x63, 0x66, 0x37, 0x49, 0x34, 0x47, + 0x4F, 0x4F, 0x44, 0x6D, 0x34, 0x52, 0x53, 0x74, 0x44, 0x65, 0x4B, 0x4C, 0x52, 0x4C, 0x42, 0x54, 0x2F, 0x44, 0x53, 0x68, 0x79, 0x63, 0x70, 0x57, 0x62, 0x58, 0x67, 0x6E, 0x62, 0x69, 0x55, 0x53, 0x59, 0x0A, 0x71, 0x71, 0x46, 0x4A, 0x75, 0x33, + 0x46, 0x53, 0x38, 0x72, 0x2F, 0x32, 0x2F, 0x79, 0x65, 0x68, 0x4E, 0x71, 0x2B, 0x34, 0x74, 0x6E, 0x65, 0x49, 0x33, 0x54, 0x71, 0x6B, 0x62, 0x5A, 0x73, 0x30, 0x6B, 0x4E, 0x77, 0x55, 0x58, 0x54, 0x43, 0x2F, 0x74, 0x2B, 0x73, 0x58, 0x35, 0x49, + 0x65, 0x33, 0x63, 0x64, 0x43, 0x68, 0x31, 0x33, 0x63, 0x56, 0x31, 0x45, 0x4C, 0x58, 0x38, 0x76, 0x4D, 0x78, 0x6D, 0x56, 0x32, 0x62, 0x33, 0x52, 0x5A, 0x74, 0x50, 0x2B, 0x6F, 0x47, 0x0A, 0x49, 0x2F, 0x68, 0x47, 0x6F, 0x69, 0x4C, 0x74, 0x6B, + 0x2F, 0x62, 0x64, 0x6D, 0x75, 0x59, 0x71, 0x68, 0x37, 0x47, 0x59, 0x56, 0x50, 0x45, 0x69, 0x39, 0x32, 0x74, 0x46, 0x34, 0x2B, 0x4B, 0x4F, 0x64, 0x68, 0x32, 0x61, 0x6A, 0x63, 0x51, 0x47, 0x6A, 0x54, 0x61, 0x33, 0x46, 0x50, 0x4F, 0x64, 0x56, + 0x47, 0x6D, 0x33, 0x6A, 0x6A, 0x7A, 0x56, 0x70, 0x47, 0x32, 0x54, 0x67, 0x62, 0x65, 0x74, 0x39, 0x72, 0x31, 0x6B, 0x65, 0x38, 0x4C, 0x4A, 0x61, 0x44, 0x6D, 0x67, 0x0A, 0x6B, 0x70, 0x7A, 0x4E, 0x4E, 0x49, 0x61, 0x52, 0x6B, 0x50, 0x70, 0x6B, + 0x55, 0x5A, 0x33, 0x2B, 0x2F, 0x75, 0x75, 0x6C, 0x39, 0x58, 0x58, 0x65, 0x69, 0x66, 0x64, 0x79, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x43, 0x20, 0x52, 0x41, 0x49, 0x5A, 0x20, 0x46, 0x4E, 0x4D, 0x54, 0x2D, 0x52, 0x43, 0x4D, 0x20, 0x53, 0x45, 0x52, 0x56, 0x49, 0x44, 0x4F, 0x52, 0x45, 0x53, 0x20, 0x53, 0x45, 0x47, 0x55, 0x52, 0x4F, 0x53, 0x0A, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x62, 0x6A, 0x43, 0x43, 0x41, 0x66, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, + 0x41, 0x67, 0x49, 0x51, 0x59, 0x76, 0x59, 0x79, 0x62, 0x4F, 0x58, 0x45, 0x34, 0x32, 0x68, 0x63, 0x47, 0x32, 0x4C, 0x64, 0x6E, 0x43, 0x36, 0x64, 0x6C, 0x54, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, + 0x41, 0x7A, 0x42, 0x34, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x46, 0x0A, 0x55, 0x7A, 0x45, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x49, 0x52, 0x6B, 0x35, + 0x4E, 0x56, 0x43, 0x31, 0x53, 0x51, 0x30, 0x30, 0x78, 0x44, 0x6A, 0x41, 0x4D, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x73, 0x4D, 0x42, 0x55, 0x4E, 0x6C, 0x63, 0x6D, 0x56, 0x7A, 0x4D, 0x52, 0x67, 0x77, 0x46, 0x67, 0x59, 0x44, 0x56, 0x51, 0x52, + 0x68, 0x44, 0x41, 0x39, 0x57, 0x51, 0x56, 0x52, 0x46, 0x55, 0x79, 0x31, 0x52, 0x4D, 0x6A, 0x67, 0x79, 0x0A, 0x4E, 0x6A, 0x41, 0x77, 0x4E, 0x45, 0x6F, 0x78, 0x4C, 0x44, 0x41, 0x71, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x49, 0x30, + 0x46, 0x44, 0x49, 0x46, 0x4A, 0x42, 0x53, 0x56, 0x6F, 0x67, 0x52, 0x6B, 0x35, 0x4E, 0x56, 0x43, 0x31, 0x53, 0x51, 0x30, 0x30, 0x67, 0x55, 0x30, 0x56, 0x53, 0x56, 0x6B, 0x6C, 0x45, 0x54, 0x31, 0x4A, 0x46, 0x55, 0x79, 0x42, 0x54, 0x52, 0x55, + 0x64, 0x56, 0x55, 0x6B, 0x39, 0x54, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x34, 0x0A, 0x4D, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x35, 0x4D, 0x7A, 0x63, 0x7A, 0x4D, 0x31, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x7A, 0x4D, 0x54, 0x49, 0x79, 0x4D, + 0x44, 0x41, 0x35, 0x4D, 0x7A, 0x63, 0x7A, 0x4D, 0x31, 0x6F, 0x77, 0x65, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x56, 0x4D, 0x78, 0x45, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x6F, 0x4D, 0x43, 0x45, 0x5A, 0x4F, 0x54, 0x56, 0x51, 0x74, 0x0A, 0x55, 0x6B, 0x4E, 0x4E, 0x4D, 0x51, 0x34, 0x77, 0x44, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x44, 0x41, 0x56, 0x44, 0x5A, 0x58, 0x4A, 0x6C, 0x63, 0x7A, 0x45, 0x59, + 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x59, 0x51, 0x77, 0x50, 0x56, 0x6B, 0x46, 0x55, 0x52, 0x56, 0x4D, 0x74, 0x55, 0x54, 0x49, 0x34, 0x4D, 0x6A, 0x59, 0x77, 0x4D, 0x44, 0x52, 0x4B, 0x4D, 0x53, 0x77, 0x77, 0x4B, 0x67, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x4E, 0x42, 0x0A, 0x51, 0x79, 0x42, 0x53, 0x51, 0x55, 0x6C, 0x61, 0x49, 0x45, 0x5A, 0x4F, 0x54, 0x56, 0x51, 0x74, 0x55, 0x6B, 0x4E, 0x4E, 0x49, 0x46, 0x4E, 0x46, 0x55, 0x6C, 0x5A, 0x4A, 0x52, 0x45, 0x39, + 0x53, 0x52, 0x56, 0x4D, 0x67, 0x55, 0x30, 0x56, 0x48, 0x56, 0x56, 0x4A, 0x50, 0x55, 0x7A, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, + 0x69, 0x41, 0x32, 0x49, 0x41, 0x0A, 0x42, 0x50, 0x61, 0x36, 0x56, 0x31, 0x50, 0x49, 0x79, 0x71, 0x76, 0x66, 0x4E, 0x6B, 0x70, 0x53, 0x49, 0x65, 0x53, 0x58, 0x30, 0x6F, 0x4E, 0x6E, 0x6E, 0x76, 0x42, 0x6C, 0x55, 0x64, 0x42, 0x65, 0x68, 0x38, + 0x64, 0x48, 0x73, 0x56, 0x6E, 0x79, 0x56, 0x30, 0x65, 0x62, 0x41, 0x41, 0x4B, 0x54, 0x52, 0x42, 0x64, 0x70, 0x32, 0x30, 0x4C, 0x48, 0x73, 0x62, 0x49, 0x36, 0x47, 0x41, 0x36, 0x30, 0x58, 0x59, 0x79, 0x7A, 0x5A, 0x6C, 0x32, 0x68, 0x4E, 0x50, + 0x6B, 0x32, 0x0A, 0x4C, 0x45, 0x6E, 0x62, 0x38, 0x30, 0x62, 0x38, 0x73, 0x30, 0x52, 0x70, 0x52, 0x42, 0x4E, 0x6D, 0x2F, 0x64, 0x66, 0x46, 0x2F, 0x61, 0x38, 0x32, 0x54, 0x63, 0x34, 0x44, 0x54, 0x51, 0x64, 0x78, 0x7A, 0x36, 0x39, 0x71, 0x42, + 0x64, 0x4B, 0x69, 0x51, 0x31, 0x6F, 0x4B, 0x55, 0x6D, 0x38, 0x42, 0x41, 0x30, 0x36, 0x4F, 0x69, 0x36, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x0A, + 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, + 0x46, 0x41, 0x47, 0x35, 0x4C, 0x2B, 0x2B, 0x2F, 0x45, 0x59, 0x5A, 0x67, 0x38, 0x6B, 0x2F, 0x51, 0x51, 0x57, 0x36, 0x72, 0x63, 0x78, 0x2F, 0x6E, 0x30, 0x6D, 0x35, 0x4A, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x0A, 0x53, 0x4D, 0x34, + 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x6B, 0x41, 0x4D, 0x47, 0x59, 0x43, 0x4D, 0x51, 0x43, 0x75, 0x53, 0x75, 0x4D, 0x72, 0x51, 0x4D, 0x4E, 0x30, 0x45, 0x66, 0x4B, 0x56, 0x72, 0x52, 0x59, 0x6A, 0x33, 0x6B, 0x34, 0x4D, 0x47, 0x75, 0x5A, + 0x64, 0x70, 0x53, 0x52, 0x65, 0x61, 0x30, 0x52, 0x37, 0x2F, 0x44, 0x6A, 0x69, 0x54, 0x38, 0x75, 0x63, 0x52, 0x52, 0x63, 0x52, 0x54, 0x42, 0x51, 0x6E, 0x4A, 0x6C, 0x55, 0x35, 0x64, 0x55, 0x6F, 0x44, 0x0A, 0x7A, 0x42, 0x4F, 0x51, 0x6E, 0x35, + 0x49, 0x43, 0x4D, 0x51, 0x44, 0x36, 0x53, 0x6D, 0x78, 0x67, 0x69, 0x48, 0x50, 0x7A, 0x37, 0x72, 0x69, 0x59, 0x59, 0x71, 0x6E, 0x4F, 0x4B, 0x38, 0x4C, 0x5A, 0x69, 0x71, 0x5A, 0x77, 0x4D, 0x52, 0x32, 0x76, 0x73, 0x4A, 0x52, 0x4D, 0x36, 0x30, + 0x2F, 0x47, 0x34, 0x39, 0x48, 0x7A, 0x59, 0x71, 0x63, 0x38, 0x2F, 0x35, 0x4D, 0x75, 0x42, 0x31, 0x78, 0x4A, 0x41, 0x57, 0x64, 0x70, 0x45, 0x67, 0x4A, 0x79, 0x76, 0x2B, 0x63, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, + 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x34, 0x36, 0x0A, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x57, 0x6A, 0x43, 0x43, 0x41, 0x30, 0x4B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x53, 0x45, 0x64, 0x4B, 0x37, 0x75, 0x64, 0x63, 0x6A, 0x47, 0x4A, 0x35, 0x41, 0x58, 0x77, + 0x71, 0x64, 0x4C, 0x64, 0x44, 0x66, 0x4A, 0x57, 0x66, 0x52, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x44, 0x41, 0x55, 0x41, 0x4D, 0x45, 0x59, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, + 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4A, 0x46, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, + 0x47, 0x35, 0x32, 0x4C, 0x58, 0x4E, 0x68, 0x4D, 0x52, 0x77, 0x77, 0x47, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4E, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x0A, + 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x51, 0x32, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x35, 0x4D, 0x44, 0x4D, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x32, 0x4D, 0x44, 0x4D, 0x79, + 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x52, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, 0x55, 0x78, 0x47, 0x54, 0x41, 0x58, 0x0A, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x62, 0x6E, 0x59, 0x74, 0x63, 0x32, 0x45, 0x78, 0x48, 0x44, 0x41, 0x61, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, + 0x54, 0x45, 0x30, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x53, 0x4E, 0x44, 0x59, 0x77, 0x67, 0x67, 0x49, 0x69, 0x0A, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, + 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x73, 0x72, 0x48, 0x51, 0x79, 0x36, 0x4C, + 0x4E, 0x6C, 0x35, 0x62, 0x72, 0x74, 0x51, 0x79, 0x59, 0x64, 0x70, 0x6F, 0x6B, 0x4E, 0x52, 0x62, 0x6F, 0x70, 0x69, 0x4C, 0x4B, 0x6B, 0x48, 0x57, 0x50, 0x64, 0x30, 0x38, 0x45, 0x73, 0x0A, 0x43, 0x56, 0x65, 0x4A, 0x4F, 0x61, 0x46, 0x56, 0x36, + 0x57, 0x63, 0x30, 0x64, 0x77, 0x78, 0x75, 0x35, 0x46, 0x55, 0x64, 0x55, 0x69, 0x58, 0x53, 0x45, 0x32, 0x74, 0x65, 0x34, 0x52, 0x32, 0x70, 0x74, 0x33, 0x32, 0x4A, 0x4D, 0x6C, 0x38, 0x4E, 0x6E, 0x70, 0x38, 0x73, 0x65, 0x6D, 0x4E, 0x67, 0x51, + 0x42, 0x2B, 0x6D, 0x73, 0x4C, 0x5A, 0x34, 0x6A, 0x35, 0x6C, 0x55, 0x6C, 0x67, 0x68, 0x59, 0x72, 0x75, 0x51, 0x47, 0x76, 0x47, 0x49, 0x46, 0x41, 0x68, 0x61, 0x2F, 0x0A, 0x72, 0x36, 0x67, 0x6A, 0x41, 0x37, 0x61, 0x55, 0x44, 0x37, 0x78, 0x75, + 0x62, 0x4D, 0x4C, 0x4C, 0x31, 0x61, 0x61, 0x37, 0x44, 0x4F, 0x6E, 0x32, 0x77, 0x51, 0x4C, 0x37, 0x49, 0x64, 0x35, 0x6D, 0x33, 0x52, 0x65, 0x72, 0x64, 0x45, 0x4C, 0x76, 0x38, 0x48, 0x51, 0x76, 0x4A, 0x66, 0x54, 0x71, 0x61, 0x31, 0x56, 0x62, + 0x6B, 0x4E, 0x75, 0x64, 0x33, 0x31, 0x36, 0x48, 0x43, 0x6B, 0x44, 0x37, 0x72, 0x52, 0x6C, 0x72, 0x2B, 0x2F, 0x66, 0x4B, 0x59, 0x49, 0x6A, 0x65, 0x0A, 0x32, 0x73, 0x47, 0x50, 0x31, 0x71, 0x37, 0x56, 0x66, 0x39, 0x51, 0x38, 0x67, 0x2B, 0x37, + 0x58, 0x46, 0x6B, 0x79, 0x44, 0x52, 0x54, 0x4E, 0x72, 0x4A, 0x39, 0x43, 0x47, 0x30, 0x42, 0x77, 0x74, 0x61, 0x2F, 0x4F, 0x72, 0x66, 0x66, 0x47, 0x46, 0x71, 0x66, 0x55, 0x6F, 0x30, 0x71, 0x33, 0x76, 0x38, 0x34, 0x52, 0x4C, 0x48, 0x49, 0x66, + 0x38, 0x45, 0x36, 0x4D, 0x36, 0x63, 0x71, 0x4A, 0x61, 0x45, 0x53, 0x76, 0x57, 0x4A, 0x33, 0x45, 0x6E, 0x37, 0x59, 0x45, 0x74, 0x0A, 0x62, 0x57, 0x61, 0x42, 0x6B, 0x6F, 0x65, 0x30, 0x47, 0x31, 0x68, 0x36, 0x7A, 0x44, 0x38, 0x4B, 0x2B, 0x6B, + 0x5A, 0x50, 0x54, 0x58, 0x68, 0x63, 0x2B, 0x43, 0x74, 0x49, 0x34, 0x77, 0x53, 0x45, 0x79, 0x31, 0x33, 0x32, 0x74, 0x47, 0x71, 0x7A, 0x5A, 0x66, 0x78, 0x43, 0x6E, 0x6C, 0x45, 0x6D, 0x49, 0x79, 0x44, 0x4C, 0x50, 0x52, 0x54, 0x35, 0x67, 0x65, + 0x31, 0x6C, 0x46, 0x67, 0x42, 0x50, 0x47, 0x6D, 0x53, 0x58, 0x5A, 0x67, 0x6A, 0x50, 0x6A, 0x48, 0x76, 0x6A, 0x0A, 0x4B, 0x38, 0x43, 0x64, 0x2B, 0x52, 0x54, 0x79, 0x47, 0x2F, 0x46, 0x57, 0x61, 0x68, 0x61, 0x2F, 0x4C, 0x49, 0x57, 0x46, 0x7A, + 0x58, 0x67, 0x34, 0x6D, 0x75, 0x74, 0x43, 0x61, 0x67, 0x49, 0x30, 0x47, 0x49, 0x4D, 0x58, 0x54, 0x70, 0x52, 0x57, 0x2B, 0x4C, 0x61, 0x43, 0x74, 0x66, 0x4F, 0x57, 0x33, 0x54, 0x33, 0x7A, 0x76, 0x6E, 0x38, 0x67, 0x64, 0x7A, 0x35, 0x37, 0x47, + 0x53, 0x4E, 0x72, 0x4C, 0x4E, 0x52, 0x79, 0x63, 0x30, 0x4E, 0x58, 0x66, 0x65, 0x44, 0x34, 0x0A, 0x31, 0x32, 0x6C, 0x50, 0x46, 0x7A, 0x59, 0x45, 0x2B, 0x63, 0x43, 0x51, 0x59, 0x44, 0x64, 0x46, 0x33, 0x75, 0x59, 0x4D, 0x32, 0x48, 0x53, 0x4E, + 0x72, 0x70, 0x79, 0x69, 0x62, 0x58, 0x52, 0x64, 0x51, 0x72, 0x34, 0x47, 0x39, 0x64, 0x6C, 0x6B, 0x62, 0x67, 0x49, 0x51, 0x72, 0x49, 0x6D, 0x77, 0x54, 0x44, 0x73, 0x48, 0x54, 0x55, 0x42, 0x2B, 0x4A, 0x4D, 0x57, 0x4B, 0x6D, 0x49, 0x4A, 0x35, + 0x6A, 0x71, 0x53, 0x6E, 0x67, 0x69, 0x43, 0x4E, 0x49, 0x2F, 0x6F, 0x6E, 0x0A, 0x63, 0x63, 0x6E, 0x66, 0x78, 0x6B, 0x46, 0x30, 0x6F, 0x45, 0x33, 0x32, 0x6B, 0x52, 0x62, 0x63, 0x52, 0x6F, 0x78, 0x66, 0x4B, 0x57, 0x4D, 0x78, 0x57, 0x58, 0x45, + 0x4D, 0x32, 0x47, 0x2F, 0x43, 0x74, 0x6A, 0x4A, 0x39, 0x2B, 0x2B, 0x5A, 0x64, 0x55, 0x36, 0x5A, 0x2B, 0x46, 0x66, 0x79, 0x37, 0x64, 0x58, 0x78, 0x64, 0x37, 0x50, 0x6A, 0x32, 0x46, 0x78, 0x7A, 0x73, 0x78, 0x32, 0x73, 0x5A, 0x79, 0x2F, 0x4E, + 0x37, 0x38, 0x43, 0x73, 0x48, 0x70, 0x64, 0x6C, 0x73, 0x0A, 0x65, 0x56, 0x52, 0x32, 0x62, 0x4A, 0x30, 0x63, 0x70, 0x6D, 0x34, 0x4F, 0x36, 0x58, 0x6B, 0x4D, 0x71, 0x43, 0x4E, 0x71, 0x6F, 0x39, 0x38, 0x62, 0x4D, 0x44, 0x47, 0x66, 0x73, 0x56, + 0x52, 0x37, 0x2F, 0x6D, 0x72, 0x4C, 0x5A, 0x71, 0x72, 0x63, 0x5A, 0x64, 0x43, 0x69, 0x6E, 0x6B, 0x71, 0x61, 0x42, 0x79, 0x46, 0x72, 0x67, 0x59, 0x2F, 0x62, 0x78, 0x46, 0x6E, 0x36, 0x33, 0x69, 0x4C, 0x41, 0x42, 0x4A, 0x7A, 0x6A, 0x71, 0x6C, + 0x73, 0x32, 0x6B, 0x2B, 0x67, 0x39, 0x0A, 0x76, 0x58, 0x71, 0x68, 0x6E, 0x51, 0x74, 0x32, 0x73, 0x51, 0x76, 0x48, 0x6E, 0x66, 0x33, 0x50, 0x6D, 0x4B, 0x67, 0x47, 0x77, 0x76, 0x67, 0x71, 0x6F, 0x36, 0x47, 0x44, 0x6F, 0x4C, 0x63, 0x6C, 0x63, + 0x71, 0x55, 0x43, 0x34, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, + 0x77, 0x59, 0x44, 0x0A, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x41, 0x31, 0x79, 0x72, + 0x63, 0x34, 0x47, 0x48, 0x71, 0x4D, 0x79, 0x77, 0x70, 0x74, 0x57, 0x55, 0x34, 0x6A, 0x61, 0x57, 0x53, 0x66, 0x38, 0x46, 0x6D, 0x53, 0x77, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, + 0x0A, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x48, 0x78, 0x34, 0x37, 0x50, 0x59, 0x43, 0x4C, 0x4C, 0x74, 0x62, 0x66, 0x70, 0x49, 0x72, 0x58, 0x54, 0x6E, 0x63, 0x76, 0x74, 0x67, 0x64, 0x6F, 0x6B, 0x49, 0x7A, 0x54, 0x66, 0x6E, + 0x76, 0x70, 0x43, 0x6F, 0x37, 0x52, 0x47, 0x6B, 0x65, 0x72, 0x4E, 0x6C, 0x46, 0x6F, 0x30, 0x34, 0x38, 0x70, 0x39, 0x67, 0x6B, 0x55, 0x62, 0x4A, 0x55, 0x48, 0x4A, 0x4E, 0x4F, 0x78, 0x4F, 0x39, 0x37, 0x6B, 0x34, 0x56, 0x67, 0x0A, 0x4A, 0x75, + 0x6F, 0x4A, 0x53, 0x4F, 0x44, 0x31, 0x75, 0x38, 0x66, 0x70, 0x61, 0x4E, 0x4B, 0x37, 0x61, 0x6A, 0x46, 0x78, 0x7A, 0x48, 0x6D, 0x75, 0x45, 0x61, 0x6A, 0x77, 0x6D, 0x66, 0x33, 0x6C, 0x48, 0x37, 0x77, 0x76, 0x71, 0x4D, 0x78, 0x58, 0x36, 0x33, + 0x62, 0x45, 0x49, 0x61, 0x5A, 0x48, 0x55, 0x31, 0x56, 0x4E, 0x61, 0x4C, 0x38, 0x46, 0x70, 0x4F, 0x37, 0x58, 0x4A, 0x71, 0x74, 0x69, 0x32, 0x6B, 0x4D, 0x33, 0x53, 0x2B, 0x4C, 0x47, 0x74, 0x65, 0x57, 0x79, 0x0A, 0x67, 0x78, 0x6B, 0x36, 0x78, + 0x39, 0x50, 0x62, 0x54, 0x5A, 0x34, 0x49, 0x65, 0x76, 0x50, 0x75, 0x7A, 0x7A, 0x35, 0x69, 0x2B, 0x36, 0x7A, 0x6F, 0x59, 0x4D, 0x7A, 0x52, 0x78, 0x36, 0x46, 0x63, 0x67, 0x30, 0x58, 0x45, 0x52, 0x63, 0x7A, 0x7A, 0x46, 0x32, 0x73, 0x55, 0x79, + 0x51, 0x51, 0x43, 0x50, 0x74, 0x49, 0x6B, 0x70, 0x6E, 0x6E, 0x70, 0x48, 0x73, 0x36, 0x69, 0x35, 0x38, 0x46, 0x5A, 0x46, 0x5A, 0x38, 0x64, 0x34, 0x6B, 0x75, 0x61, 0x50, 0x70, 0x39, 0x32, 0x0A, 0x43, 0x43, 0x31, 0x72, 0x32, 0x4C, 0x70, 0x58, + 0x46, 0x4E, 0x71, 0x44, 0x36, 0x76, 0x36, 0x4D, 0x56, 0x65, 0x6E, 0x51, 0x54, 0x71, 0x6E, 0x4D, 0x64, 0x7A, 0x47, 0x78, 0x52, 0x42, 0x46, 0x36, 0x58, 0x4C, 0x45, 0x2B, 0x30, 0x78, 0x52, 0x46, 0x46, 0x52, 0x68, 0x69, 0x4A, 0x42, 0x50, 0x53, + 0x79, 0x30, 0x33, 0x4F, 0x58, 0x49, 0x50, 0x42, 0x4E, 0x76, 0x49, 0x51, 0x74, 0x51, 0x36, 0x49, 0x62, 0x62, 0x6A, 0x68, 0x56, 0x70, 0x2B, 0x4A, 0x33, 0x70, 0x5A, 0x6D, 0x0A, 0x4F, 0x55, 0x64, 0x6B, 0x4C, 0x47, 0x35, 0x4E, 0x72, 0x6D, 0x4A, + 0x37, 0x76, 0x32, 0x42, 0x30, 0x47, 0x62, 0x68, 0x57, 0x72, 0x4A, 0x4B, 0x73, 0x46, 0x6A, 0x4C, 0x74, 0x72, 0x57, 0x68, 0x56, 0x2F, 0x70, 0x69, 0x36, 0x30, 0x7A, 0x54, 0x65, 0x39, 0x4D, 0x6C, 0x68, 0x77, 0x77, 0x36, 0x47, 0x39, 0x6B, 0x75, + 0x45, 0x59, 0x4F, 0x34, 0x4E, 0x65, 0x37, 0x55, 0x79, 0x57, 0x48, 0x6D, 0x52, 0x56, 0x53, 0x79, 0x42, 0x51, 0x37, 0x4E, 0x30, 0x48, 0x33, 0x71, 0x71, 0x0A, 0x4A, 0x5A, 0x34, 0x64, 0x31, 0x36, 0x47, 0x4C, 0x75, 0x63, 0x31, 0x43, 0x4C, 0x67, + 0x53, 0x6B, 0x5A, 0x6F, 0x4E, 0x4E, 0x69, 0x54, 0x57, 0x32, 0x62, 0x4B, 0x67, 0x32, 0x53, 0x6E, 0x6B, 0x68, 0x65, 0x43, 0x4C, 0x51, 0x51, 0x72, 0x7A, 0x52, 0x51, 0x44, 0x47, 0x51, 0x6F, 0x62, 0x34, 0x45, 0x7A, 0x38, 0x70, 0x6E, 0x37, 0x66, + 0x58, 0x77, 0x67, 0x4E, 0x4E, 0x67, 0x79, 0x59, 0x4D, 0x71, 0x49, 0x67, 0x58, 0x51, 0x42, 0x7A, 0x74, 0x53, 0x76, 0x77, 0x79, 0x65, 0x0A, 0x71, 0x69, 0x76, 0x35, 0x75, 0x2B, 0x59, 0x66, 0x6A, 0x79, 0x57, 0x36, 0x68, 0x59, 0x30, 0x58, 0x48, + 0x67, 0x4C, 0x2B, 0x58, 0x56, 0x41, 0x45, 0x56, 0x38, 0x2F, 0x2B, 0x4C, 0x62, 0x7A, 0x76, 0x58, 0x4D, 0x41, 0x61, 0x71, 0x37, 0x61, 0x66, 0x4A, 0x4D, 0x62, 0x66, 0x63, 0x32, 0x68, 0x49, 0x6B, 0x43, 0x77, 0x55, 0x39, 0x44, 0x39, 0x53, 0x47, + 0x75, 0x54, 0x53, 0x79, 0x78, 0x54, 0x44, 0x59, 0x57, 0x6E, 0x50, 0x34, 0x76, 0x6B, 0x59, 0x78, 0x62, 0x6F, 0x7A, 0x0A, 0x6E, 0x78, 0x53, 0x6A, 0x42, 0x46, 0x32, 0x35, 0x63, 0x66, 0x65, 0x31, 0x6C, 0x4E, 0x6A, 0x32, 0x4D, 0x38, 0x46, 0x61, + 0x77, 0x54, 0x53, 0x4C, 0x66, 0x4A, 0x76, 0x64, 0x6B, 0x7A, 0x72, 0x6E, 0x45, 0x36, 0x4A, 0x77, 0x59, 0x5A, 0x2B, 0x76, 0x6A, 0x2B, 0x76, 0x59, 0x78, 0x58, 0x58, 0x34, 0x4D, 0x32, 0x62, 0x55, 0x64, 0x47, 0x63, 0x36, 0x4E, 0x33, 0x65, 0x63, + 0x35, 0x39, 0x32, 0x6B, 0x44, 0x33, 0x5A, 0x44, 0x5A, 0x6F, 0x70, 0x44, 0x38, 0x70, 0x2F, 0x37, 0x0A, 0x44, 0x45, 0x4A, 0x34, 0x59, 0x39, 0x48, 0x69, 0x44, 0x32, 0x39, 0x37, 0x31, 0x4B, 0x45, 0x39, 0x64, 0x4A, 0x65, 0x46, 0x74, 0x30, 0x67, + 0x35, 0x51, 0x64, 0x59, 0x67, 0x2F, 0x4E, 0x41, 0x36, 0x73, 0x2F, 0x72, 0x6F, 0x62, 0x38, 0x53, 0x4B, 0x75, 0x6E, 0x45, 0x33, 0x76, 0x6F, 0x75, 0x58, 0x73, 0x58, 0x67, 0x78, 0x54, 0x37, 0x50, 0x6E, 0x74, 0x67, 0x4D, 0x54, 0x7A, 0x6C, 0x53, + 0x64, 0x72, 0x69, 0x56, 0x5A, 0x7A, 0x48, 0x38, 0x31, 0x58, 0x77, 0x6A, 0x33, 0x0A, 0x51, 0x45, 0x55, 0x78, 0x65, 0x43, 0x70, 0x36, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x45, 0x34, 0x36, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, + 0x49, 0x43, 0x43, 0x7A, 0x43, 0x43, 0x41, 0x5A, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x53, 0x45, 0x64, 0x4B, 0x37, 0x75, 0x6A, 0x4E, 0x75, 0x31, 0x4C, 0x7A, 0x6D, 0x4A, 0x47, 0x6A, 0x46, 0x44, 0x59, 0x51, 0x64, 0x6D, 0x4F, + 0x68, 0x44, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x4D, 0x45, 0x59, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x0A, 0x41, 0x6B, 0x4A, 0x46, 0x4D, + 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x47, 0x35, 0x32, 0x4C, 0x58, 0x4E, 0x68, 0x4D, 0x52, 0x77, 0x77, 0x47, + 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4E, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x0A, 0x52, 0x54, 0x51, 0x32, 0x4D, 0x42, 0x34, 0x58, + 0x44, 0x54, 0x45, 0x35, 0x4D, 0x44, 0x4D, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x32, 0x4D, 0x44, 0x4D, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, + 0x52, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x6B, 0x55, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, + 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x62, 0x6E, 0x59, 0x74, 0x63, 0x32, 0x45, 0x78, 0x48, 0x44, 0x41, 0x61, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x30, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, + 0x70, 0x5A, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x46, 0x4E, 0x44, 0x59, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x0A, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, + 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x53, 0x63, 0x44, 0x72, 0x48, 0x50, 0x74, 0x2B, 0x69, 0x65, 0x55, 0x6E, 0x64, 0x31, 0x4E, 0x50, 0x71, 0x6C, 0x52, 0x71, 0x65, 0x74, 0x4D, 0x68, 0x6B, 0x79, 0x74, 0x41, 0x65, 0x70, 0x4A, 0x38, + 0x71, 0x55, 0x75, 0x77, 0x7A, 0x53, 0x43, 0x68, 0x44, 0x48, 0x32, 0x6F, 0x6D, 0x77, 0x6C, 0x77, 0x78, 0x77, 0x45, 0x77, 0x6B, 0x42, 0x0A, 0x6A, 0x74, 0x6A, 0x71, 0x52, 0x2B, 0x71, 0x2B, 0x73, 0x6F, 0x41, 0x72, 0x7A, 0x66, 0x77, 0x6F, 0x44, + 0x64, 0x75, 0x73, 0x76, 0x4B, 0x53, 0x47, 0x4E, 0x2B, 0x31, 0x77, 0x43, 0x41, 0x42, 0x31, 0x36, 0x70, 0x4D, 0x4C, 0x65, 0x79, 0x35, 0x53, 0x6E, 0x43, 0x4E, 0x6F, 0x49, 0x77, 0x5A, 0x44, 0x37, 0x4A, 0x49, 0x76, 0x55, 0x34, 0x54, 0x62, 0x2B, + 0x30, 0x63, 0x55, 0x42, 0x2B, 0x68, 0x66, 0x6C, 0x47, 0x64, 0x64, 0x79, 0x58, 0x71, 0x42, 0x50, 0x43, 0x43, 0x6A, 0x0A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, + 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, + 0x42, 0x42, 0x51, 0x78, 0x43, 0x70, 0x43, 0x50, 0x74, 0x73, 0x61, 0x64, 0x30, 0x6B, 0x52, 0x4C, 0x0A, 0x67, 0x4C, 0x57, 0x69, 0x35, 0x68, 0x2B, 0x78, 0x45, 0x6B, 0x38, 0x62, 0x6C, 0x54, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, + 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x41, 0x44, 0x42, 0x6C, 0x41, 0x6A, 0x45, 0x41, 0x33, 0x31, 0x53, 0x51, 0x37, 0x5A, 0x76, 0x76, 0x69, 0x35, 0x51, 0x43, 0x6B, 0x78, 0x65, 0x43, 0x6D, 0x62, 0x36, 0x7A, 0x6E, 0x69, 0x7A, + 0x32, 0x43, 0x35, 0x47, 0x4D, 0x6E, 0x30, 0x6F, 0x55, 0x73, 0x66, 0x5A, 0x6B, 0x0A, 0x76, 0x4C, 0x74, 0x6F, 0x55, 0x52, 0x4D, 0x4D, 0x41, 0x2F, 0x63, 0x56, 0x69, 0x34, 0x52, 0x67, 0x75, 0x59, 0x76, 0x2F, 0x55, 0x6F, 0x37, 0x6E, 0x6A, 0x4C, + 0x77, 0x63, 0x41, 0x6A, 0x41, 0x38, 0x2B, 0x52, 0x48, 0x55, 0x6A, 0x45, 0x37, 0x41, 0x77, 0x57, 0x48, 0x43, 0x46, 0x55, 0x79, 0x71, 0x71, 0x78, 0x30, 0x4C, 0x4D, 0x56, 0x38, 0x37, 0x48, 0x4F, 0x49, 0x41, 0x6C, 0x30, 0x51, 0x78, 0x35, 0x76, + 0x35, 0x7A, 0x6C, 0x69, 0x2F, 0x61, 0x6C, 0x74, 0x50, 0x2B, 0x0A, 0x43, 0x41, 0x65, 0x7A, 0x4E, 0x49, 0x6D, 0x38, 0x42, 0x5A, 0x2F, 0x33, 0x48, 0x6F, 0x62, 0x75, 0x69, 0x33, 0x41, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x4C, 0x4F, 0x42, 0x41, 0x4C, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x32, 0x30, 0x32, 0x30, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x67, 0x6A, 0x43, 0x43, 0x41, 0x32, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4C, 0x57, 0x6B, 0x75, 0x39, 0x57, 0x76, 0x74, 0x50, 0x69, 0x6C, 0x76, 0x36, 0x5A, 0x65, 0x55, 0x77, 0x44, 0x51, 0x59, + 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x54, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x56, 0x51, 0x78, 0x0A, 0x49, 0x7A, + 0x41, 0x68, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x6D, 0x55, 0x74, 0x59, 0x32, 0x39, 0x74, 0x62, 0x57, 0x56, 0x79, 0x59, 0x32, 0x55, 0x67, 0x62, 0x57, 0x39, 0x75, 0x61, 0x58, 0x52, 0x76, 0x63, 0x6D, 0x6C, 0x75, 0x5A, 0x79, + 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4D, 0x52, 0x6B, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, 0x48, 0x54, 0x45, 0x39, 0x43, 0x51, 0x55, 0x78, 0x55, 0x55, 0x6C, 0x56, 0x54, 0x0A, 0x56, 0x43, 0x41, 0x79, 0x4D, + 0x44, 0x49, 0x77, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x77, 0x4D, 0x44, 0x59, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, + 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x77, 0x54, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x56, 0x51, 0x78, 0x49, 0x7A, 0x41, 0x68, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, + 0x47, 0x6D, 0x55, 0x74, 0x59, 0x32, 0x39, 0x74, 0x62, 0x57, 0x56, 0x79, 0x59, 0x32, 0x55, 0x67, 0x62, 0x57, 0x39, 0x75, 0x61, 0x58, 0x52, 0x76, 0x63, 0x6D, 0x6C, 0x75, 0x5A, 0x79, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4D, 0x52, 0x6B, 0x77, + 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x42, 0x48, 0x54, 0x45, 0x39, 0x43, 0x51, 0x55, 0x78, 0x55, 0x55, 0x6C, 0x56, 0x54, 0x56, 0x43, 0x41, 0x79, 0x0A, 0x4D, 0x44, 0x49, 0x77, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, + 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x72, 0x69, 0x35, + 0x57, 0x72, 0x52, 0x73, 0x63, 0x37, 0x2F, 0x61, 0x56, 0x6A, 0x36, 0x42, 0x33, 0x47, 0x79, 0x76, 0x54, 0x59, 0x34, 0x2B, 0x45, 0x54, 0x55, 0x57, 0x69, 0x0A, 0x44, 0x35, 0x39, 0x62, 0x52, 0x61, 0x74, 0x5A, 0x65, 0x31, 0x45, 0x30, 0x2B, 0x65, + 0x79, 0x4C, 0x69, 0x6E, 0x6A, 0x46, 0x33, 0x57, 0x75, 0x76, 0x76, 0x63, 0x54, 0x66, 0x6B, 0x30, 0x55, 0x65, 0x76, 0x35, 0x45, 0x34, 0x43, 0x36, 0x34, 0x4F, 0x46, 0x75, 0x64, 0x42, 0x63, 0x2F, 0x6A, 0x62, 0x75, 0x39, 0x47, 0x34, 0x55, 0x65, + 0x44, 0x4C, 0x67, 0x7A, 0x74, 0x7A, 0x4F, 0x47, 0x35, 0x33, 0x69, 0x67, 0x39, 0x5A, 0x59, 0x79, 0x62, 0x4E, 0x70, 0x79, 0x72, 0x4F, 0x0A, 0x56, 0x50, 0x75, 0x34, 0x34, 0x73, 0x42, 0x38, 0x52, 0x38, 0x35, 0x67, 0x66, 0x44, 0x2B, 0x79, 0x63, + 0x2F, 0x4C, 0x41, 0x47, 0x62, 0x61, 0x4B, 0x6B, 0x6F, 0x63, 0x31, 0x44, 0x5A, 0x41, 0x6F, 0x6F, 0x75, 0x51, 0x56, 0x42, 0x47, 0x4D, 0x2B, 0x75, 0x71, 0x2F, 0x75, 0x66, 0x46, 0x37, 0x4D, 0x70, 0x6F, 0x74, 0x51, 0x73, 0x6A, 0x6A, 0x33, 0x51, + 0x57, 0x50, 0x4B, 0x7A, 0x76, 0x39, 0x70, 0x6A, 0x32, 0x67, 0x4F, 0x6C, 0x54, 0x62, 0x6C, 0x7A, 0x4C, 0x6D, 0x4D, 0x0A, 0x43, 0x63, 0x70, 0x4C, 0x33, 0x54, 0x47, 0x51, 0x6C, 0x73, 0x6A, 0x4D, 0x48, 0x2F, 0x31, 0x57, 0x6C, 0x6A, 0x54, 0x62, + 0x6A, 0x68, 0x7A, 0x71, 0x4C, 0x4C, 0x36, 0x46, 0x4C, 0x6D, 0x50, 0x64, 0x71, 0x71, 0x6D, 0x56, 0x30, 0x2F, 0x30, 0x70, 0x6C, 0x52, 0x50, 0x77, 0x79, 0x4A, 0x69, 0x54, 0x32, 0x53, 0x30, 0x57, 0x52, 0x35, 0x41, 0x52, 0x67, 0x36, 0x49, 0x36, + 0x49, 0x71, 0x49, 0x6F, 0x56, 0x36, 0x4C, 0x72, 0x2F, 0x73, 0x43, 0x4D, 0x4B, 0x4B, 0x43, 0x6D, 0x0A, 0x66, 0x65, 0x63, 0x71, 0x51, 0x6A, 0x75, 0x43, 0x67, 0x47, 0x4F, 0x6C, 0x59, 0x78, 0x38, 0x5A, 0x7A, 0x48, 0x79, 0x79, 0x5A, 0x71, 0x6A, + 0x43, 0x30, 0x32, 0x30, 0x33, 0x62, 0x2B, 0x4A, 0x2B, 0x42, 0x6C, 0x48, 0x5A, 0x52, 0x59, 0x51, 0x66, 0x45, 0x73, 0x34, 0x6B, 0x55, 0x6D, 0x53, 0x46, 0x43, 0x30, 0x69, 0x41, 0x54, 0x6F, 0x65, 0x78, 0x49, 0x69, 0x49, 0x77, 0x71, 0x75, 0x75, + 0x75, 0x76, 0x75, 0x41, 0x43, 0x34, 0x45, 0x44, 0x6F, 0x73, 0x45, 0x4B, 0x41, 0x0A, 0x41, 0x31, 0x47, 0x71, 0x74, 0x48, 0x36, 0x71, 0x52, 0x4E, 0x64, 0x44, 0x59, 0x66, 0x4F, 0x69, 0x61, 0x78, 0x61, 0x4A, 0x53, 0x61, 0x53, 0x6A, 0x70, 0x43, + 0x75, 0x4B, 0x41, 0x73, 0x52, 0x34, 0x39, 0x47, 0x69, 0x4B, 0x77, 0x65, 0x52, 0x36, 0x4E, 0x72, 0x46, 0x76, 0x47, 0x35, 0x59, 0x62, 0x64, 0x30, 0x6D, 0x4E, 0x31, 0x4D, 0x6B, 0x47, 0x63, 0x6F, 0x2F, 0x50, 0x55, 0x2B, 0x50, 0x63, 0x46, 0x34, + 0x55, 0x67, 0x53, 0x74, 0x79, 0x59, 0x4A, 0x39, 0x4F, 0x52, 0x0A, 0x4A, 0x69, 0x74, 0x48, 0x48, 0x6D, 0x6B, 0x48, 0x72, 0x39, 0x36, 0x69, 0x35, 0x4F, 0x54, 0x55, 0x61, 0x77, 0x75, 0x7A, 0x58, 0x6E, 0x7A, 0x55, 0x4A, 0x49, 0x42, 0x48, 0x4B, + 0x57, 0x6B, 0x37, 0x62, 0x75, 0x69, 0x73, 0x2F, 0x55, 0x44, 0x72, 0x32, 0x4F, 0x31, 0x78, 0x63, 0x53, 0x76, 0x79, 0x36, 0x46, 0x67, 0x64, 0x36, 0x30, 0x47, 0x58, 0x49, 0x73, 0x55, 0x66, 0x31, 0x44, 0x6E, 0x51, 0x4A, 0x34, 0x2B, 0x48, 0x34, + 0x78, 0x6A, 0x30, 0x34, 0x4B, 0x6C, 0x47, 0x0A, 0x44, 0x66, 0x56, 0x30, 0x4F, 0x6F, 0x49, 0x75, 0x30, 0x47, 0x34, 0x73, 0x6B, 0x61, 0x4D, 0x78, 0x58, 0x44, 0x74, 0x47, 0x36, 0x6E, 0x73, 0x45, 0x45, 0x46, 0x5A, 0x65, 0x67, 0x42, 0x33, 0x31, + 0x70, 0x57, 0x58, 0x6F, 0x67, 0x76, 0x7A, 0x69, 0x42, 0x34, 0x78, 0x69, 0x52, 0x66, 0x55, 0x67, 0x33, 0x6B, 0x5A, 0x77, 0x68, 0x71, 0x47, 0x38, 0x6B, 0x39, 0x4D, 0x65, 0x64, 0x4B, 0x5A, 0x73, 0x73, 0x43, 0x7A, 0x33, 0x41, 0x77, 0x79, 0x49, + 0x44, 0x4D, 0x76, 0x55, 0x0A, 0x63, 0x6C, 0x4F, 0x47, 0x76, 0x47, 0x42, 0x47, 0x38, 0x35, 0x68, 0x71, 0x77, 0x76, 0x47, 0x2F, 0x51, 0x2F, 0x6C, 0x77, 0x49, 0x48, 0x66, 0x4B, 0x4E, 0x30, 0x46, 0x35, 0x56, 0x56, 0x4A, 0x6A, 0x6A, 0x56, 0x73, + 0x53, 0x6E, 0x38, 0x56, 0x6F, 0x78, 0x49, 0x69, 0x64, 0x72, 0x50, 0x49, 0x77, 0x71, 0x37, 0x65, 0x6A, 0x4D, 0x5A, 0x64, 0x6E, 0x72, 0x59, 0x38, 0x58, 0x44, 0x32, 0x7A, 0x48, 0x63, 0x2B, 0x30, 0x6B, 0x6C, 0x47, 0x76, 0x49, 0x67, 0x35, 0x72, + 0x51, 0x0A, 0x6D, 0x6A, 0x64, 0x4A, 0x42, 0x4B, 0x75, 0x78, 0x46, 0x73, 0x68, 0x73, 0x53, 0x55, 0x6B, 0x74, 0x71, 0x36, 0x48, 0x51, 0x6A, 0x4A, 0x4C, 0x79, 0x51, 0x55, 0x70, 0x35, 0x49, 0x53, 0x58, 0x62, 0x59, 0x39, 0x65, 0x32, 0x6E, 0x4B, + 0x64, 0x2B, 0x51, 0x6D, 0x6E, 0x37, 0x4F, 0x6D, 0x4D, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x6A, 0x4D, 0x47, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x0A, 0x41, + 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, + 0x4E, 0x77, 0x75, 0x48, 0x39, 0x46, 0x68, 0x4E, 0x33, 0x6E, 0x6B, 0x71, 0x39, 0x58, 0x56, 0x73, 0x78, 0x4A, 0x78, 0x61, 0x44, 0x31, 0x71, 0x61, 0x4A, 0x77, 0x69, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x49, 0x77, 0x51, 0x59, + 0x4D, 0x42, 0x61, 0x41, 0x46, 0x4E, 0x77, 0x75, 0x48, 0x39, 0x46, 0x68, 0x4E, 0x33, 0x6E, 0x6B, 0x71, 0x39, 0x58, 0x56, 0x73, 0x78, 0x4A, 0x78, 0x61, 0x44, 0x31, 0x71, 0x61, 0x4A, 0x77, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, + 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x43, 0x52, 0x38, 0x45, 0x49, 0x43, 0x61, 0x45, 0x44, 0x75, 0x77, 0x32, 0x6A, 0x41, 0x0A, 0x56, 0x43, 0x2F, 0x66, 0x37, 0x47, 0x4C, + 0x44, 0x77, 0x35, 0x36, 0x4B, 0x6F, 0x44, 0x45, 0x6F, 0x71, 0x6F, 0x4F, 0x4F, 0x70, 0x46, 0x61, 0x57, 0x45, 0x68, 0x43, 0x47, 0x56, 0x72, 0x71, 0x58, 0x63, 0x74, 0x4A, 0x55, 0x4D, 0x48, 0x79, 0x74, 0x47, 0x64, 0x55, 0x64, 0x61, 0x47, 0x2F, + 0x37, 0x46, 0x45, 0x4C, 0x59, 0x6A, 0x51, 0x37, 0x7A, 0x74, 0x64, 0x47, 0x6C, 0x34, 0x77, 0x4A, 0x43, 0x58, 0x74, 0x7A, 0x6F, 0x52, 0x6C, 0x67, 0x48, 0x4E, 0x51, 0x49, 0x77, 0x0A, 0x34, 0x4C, 0x78, 0x30, 0x53, 0x73, 0x46, 0x44, 0x4B, 0x76, + 0x2F, 0x62, 0x47, 0x74, 0x43, 0x77, 0x72, 0x32, 0x7A, 0x44, 0x2F, 0x63, 0x75, 0x7A, 0x39, 0x58, 0x39, 0x74, 0x41, 0x79, 0x35, 0x5A, 0x56, 0x70, 0x30, 0x74, 0x4C, 0x54, 0x57, 0x4D, 0x73, 0x74, 0x5A, 0x44, 0x46, 0x79, 0x79, 0x53, 0x43, 0x73, + 0x74, 0x64, 0x36, 0x49, 0x77, 0x50, 0x53, 0x33, 0x42, 0x44, 0x30, 0x49, 0x4C, 0x2F, 0x71, 0x4D, 0x79, 0x2F, 0x70, 0x4A, 0x54, 0x41, 0x76, 0x6F, 0x65, 0x39, 0x0A, 0x69, 0x75, 0x4F, 0x54, 0x65, 0x38, 0x61, 0x50, 0x6D, 0x78, 0x61, 0x64, 0x4A, + 0x32, 0x57, 0x38, 0x65, 0x73, 0x56, 0x43, 0x67, 0x6D, 0x78, 0x63, 0x42, 0x39, 0x43, 0x70, 0x77, 0x59, 0x68, 0x67, 0x52, 0x4F, 0x6D, 0x59, 0x68, 0x52, 0x5A, 0x66, 0x2B, 0x49, 0x2F, 0x4B, 0x41, 0x52, 0x44, 0x4F, 0x4A, 0x63, 0x50, 0x35, 0x59, + 0x42, 0x75, 0x67, 0x78, 0x5A, 0x66, 0x44, 0x30, 0x79, 0x79, 0x49, 0x4D, 0x61, 0x4B, 0x39, 0x4D, 0x4F, 0x7A, 0x51, 0x30, 0x4D, 0x41, 0x53, 0x0A, 0x38, 0x63, 0x45, 0x35, 0x34, 0x2B, 0x58, 0x31, 0x2B, 0x4E, 0x5A, 0x4B, 0x33, 0x54, 0x54, 0x4E, + 0x2B, 0x32, 0x2F, 0x42, 0x54, 0x2B, 0x4D, 0x41, 0x69, 0x31, 0x62, 0x69, 0x6B, 0x76, 0x63, 0x6F, 0x73, 0x6B, 0x4A, 0x33, 0x63, 0x69, 0x4E, 0x6E, 0x78, 0x7A, 0x38, 0x52, 0x46, 0x62, 0x4C, 0x45, 0x41, 0x77, 0x57, 0x2B, 0x75, 0x78, 0x46, 0x37, + 0x43, 0x72, 0x2B, 0x6F, 0x62, 0x75, 0x66, 0x2F, 0x57, 0x45, 0x50, 0x50, 0x6D, 0x32, 0x65, 0x67, 0x67, 0x41, 0x65, 0x32, 0x0A, 0x48, 0x63, 0x71, 0x74, 0x62, 0x65, 0x70, 0x42, 0x45, 0x58, 0x34, 0x74, 0x64, 0x4A, 0x50, 0x37, 0x77, 0x72, 0x79, + 0x2B, 0x55, 0x55, 0x54, 0x46, 0x37, 0x32, 0x67, 0x6C, 0x4A, 0x34, 0x44, 0x6A, 0x79, 0x4B, 0x44, 0x55, 0x45, 0x75, 0x7A, 0x5A, 0x70, 0x54, 0x63, 0x64, 0x4E, 0x33, 0x79, 0x30, 0x6B, 0x63, 0x72, 0x61, 0x31, 0x4C, 0x47, 0x57, 0x67, 0x65, 0x39, + 0x6F, 0x58, 0x48, 0x59, 0x51, 0x53, 0x61, 0x39, 0x2B, 0x70, 0x54, 0x65, 0x41, 0x73, 0x52, 0x78, 0x53, 0x0A, 0x76, 0x54, 0x4F, 0x42, 0x54, 0x49, 0x2F, 0x35, 0x33, 0x57, 0x58, 0x5A, 0x46, 0x4D, 0x32, 0x4B, 0x4A, 0x56, 0x6A, 0x30, 0x34, 0x73, + 0x57, 0x44, 0x70, 0x51, 0x6D, 0x51, 0x31, 0x47, 0x77, 0x55, 0x59, 0x37, 0x56, 0x41, 0x33, 0x2B, 0x76, 0x41, 0x2F, 0x4D, 0x52, 0x59, 0x66, 0x67, 0x30, 0x55, 0x46, 0x6F, 0x64, 0x55, 0x4A, 0x32, 0x35, 0x57, 0x35, 0x48, 0x43, 0x45, 0x75, 0x47, + 0x77, 0x79, 0x45, 0x6E, 0x36, 0x43, 0x4D, 0x55, 0x4F, 0x2B, 0x31, 0x39, 0x31, 0x38, 0x0A, 0x6F, 0x61, 0x32, 0x75, 0x31, 0x71, 0x73, 0x67, 0x45, 0x75, 0x38, 0x4B, 0x77, 0x78, 0x43, 0x4D, 0x53, 0x5A, 0x59, 0x31, 0x33, 0x41, 0x74, 0x31, 0x58, + 0x72, 0x46, 0x50, 0x31, 0x55, 0x38, 0x30, 0x44, 0x68, 0x45, 0x67, 0x42, 0x33, 0x56, 0x44, 0x52, 0x65, 0x6D, 0x6A, 0x45, 0x64, 0x71, 0x73, 0x6F, 0x35, 0x6E, 0x43, 0x74, 0x6E, 0x6B, 0x6E, 0x34, 0x72, 0x6E, 0x76, 0x79, 0x4F, 0x4C, 0x32, 0x4E, + 0x53, 0x6C, 0x36, 0x64, 0x50, 0x72, 0x46, 0x66, 0x34, 0x49, 0x46, 0x0A, 0x59, 0x71, 0x59, 0x4B, 0x36, 0x6D, 0x69, 0x79, 0x65, 0x55, 0x63, 0x47, 0x62, 0x76, 0x4A, 0x58, 0x71, 0x42, 0x55, 0x7A, 0x78, 0x76, 0x64, 0x34, 0x53, 0x6A, 0x31, 0x43, + 0x65, 0x32, 0x74, 0x2B, 0x2F, 0x76, 0x64, 0x47, 0x36, 0x74, 0x48, 0x72, 0x6A, 0x75, 0x2B, 0x49, 0x61, 0x46, 0x76, 0x6F, 0x77, 0x64, 0x6C, 0x78, 0x66, 0x76, 0x31, 0x6B, 0x37, 0x2F, 0x39, 0x6E, 0x52, 0x34, 0x68, 0x59, 0x4A, 0x53, 0x38, 0x2B, + 0x68, 0x67, 0x65, 0x39, 0x2B, 0x36, 0x6A, 0x6C, 0x0A, 0x67, 0x71, 0x69, 0x73, 0x70, 0x64, 0x4E, 0x70, 0x51, 0x38, 0x30, 0x78, 0x69, 0x45, 0x6D, 0x45, 0x55, 0x35, 0x4C, 0x41, 0x73, 0x54, 0x6B, 0x62, 0x4F, 0x59, 0x4D, 0x42, 0x4D, 0x4D, 0x54, + 0x79, 0x71, 0x66, 0x72, 0x51, 0x41, 0x37, 0x31, 0x79, 0x4E, 0x32, 0x42, 0x57, 0x48, 0x7A, 0x5A, 0x38, 0x76, 0x54, 0x6D, 0x52, 0x39, 0x57, 0x30, 0x4E, 0x76, 0x33, 0x76, 0x58, 0x6B, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, + 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x4E, 0x46, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x52, + 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x37, 0x7A, 0x43, 0x43, 0x41, 0x39, 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x49, 0x44, 0x64, 0x50, 0x6A, 0x76, 0x47, 0x7A, 0x35, 0x61, 0x37, 0x45, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x67, 0x59, 0x51, 0x78, 0x45, 0x6A, 0x41, + 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x55, 0x54, 0x43, 0x55, 0x63, 0x32, 0x4D, 0x7A, 0x49, 0x34, 0x0A, 0x4E, 0x7A, 0x55, 0x78, 0x4D, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x56, + 0x4D, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x6B, 0x46, 0x4F, 0x52, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x76, 0x63, 0x6D, 0x6C, 0x6B, 0x59, 0x57, 0x51, 0x67, 0x5A, 0x47, 0x55, 0x67, 0x51, 0x32, + 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x59, 0x32, 0x6C, 0x76, 0x0A, 0x62, 0x6A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x78, 0x4D, 0x4C, 0x51, 0x55, 0x35, 0x47, 0x49, 0x45, 0x4E, 0x42, 0x49, + 0x46, 0x4A, 0x68, 0x61, 0x58, 0x6F, 0x78, 0x49, 0x6A, 0x41, 0x67, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x47, 0x55, 0x46, 0x4F, 0x52, 0x69, 0x42, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x55, 0x67, 0x55, 0x32, 0x56, 0x79, 0x64, + 0x6D, 0x56, 0x79, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x0A, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x6B, 0x77, 0x4F, 0x54, 0x41, 0x30, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x4D, 0x34, 0x57, 0x68, 0x63, 0x4E, + 0x4D, 0x7A, 0x6B, 0x77, 0x4F, 0x44, 0x4D, 0x77, 0x4D, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x4D, 0x34, 0x57, 0x6A, 0x43, 0x42, 0x68, 0x44, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x52, 0x4D, 0x4A, 0x52, 0x7A, 0x59, 0x7A, + 0x4D, 0x6A, 0x67, 0x33, 0x4E, 0x54, 0x45, 0x77, 0x0A, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x46, 0x55, 0x7A, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, + 0x65, 0x51, 0x55, 0x35, 0x47, 0x49, 0x45, 0x46, 0x31, 0x64, 0x47, 0x39, 0x79, 0x61, 0x57, 0x52, 0x68, 0x5A, 0x43, 0x42, 0x6B, 0x5A, 0x53, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x6A, 0x61, 0x57, 0x39, + 0x75, 0x4D, 0x52, 0x51, 0x77, 0x0A, 0x45, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x77, 0x74, 0x42, 0x54, 0x6B, 0x59, 0x67, 0x51, 0x30, 0x45, 0x67, 0x55, 0x6D, 0x46, 0x70, 0x65, 0x6A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, + 0x55, 0x45, 0x41, 0x78, 0x4D, 0x5A, 0x51, 0x55, 0x35, 0x47, 0x49, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x5A, 0x53, 0x42, 0x54, 0x5A, 0x58, 0x4A, 0x32, 0x5A, 0x58, 0x49, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, + 0x43, 0x43, 0x0A, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, + 0x67, 0x49, 0x42, 0x41, 0x4E, 0x76, 0x72, 0x61, 0x79, 0x76, 0x6D, 0x5A, 0x46, 0x53, 0x56, 0x67, 0x70, 0x43, 0x6A, 0x63, 0x71, 0x51, 0x5A, 0x41, 0x5A, 0x32, 0x63, 0x43, 0x34, 0x46, 0x66, 0x63, 0x30, 0x6D, 0x36, 0x70, 0x36, 0x7A, 0x7A, 0x0A, + 0x42, 0x45, 0x35, 0x37, 0x6C, 0x67, 0x76, 0x73, 0x45, 0x65, 0x42, 0x62, 0x70, 0x68, 0x7A, 0x4F, 0x47, 0x39, 0x49, 0x4E, 0x67, 0x78, 0x77, 0x72, 0x75, 0x4A, 0x34, 0x64, 0x66, 0x6B, 0x55, 0x79, 0x59, 0x41, 0x38, 0x48, 0x36, 0x58, 0x64, 0x59, + 0x66, 0x70, 0x39, 0x71, 0x79, 0x47, 0x46, 0x4F, 0x74, 0x69, 0x62, 0x42, 0x54, 0x49, 0x33, 0x2F, 0x54, 0x4F, 0x38, 0x30, 0x73, 0x68, 0x39, 0x6C, 0x32, 0x4C, 0x6C, 0x34, 0x39, 0x61, 0x32, 0x70, 0x63, 0x62, 0x6E, 0x76, 0x0A, 0x54, 0x31, 0x67, + 0x64, 0x70, 0x64, 0x35, 0x30, 0x49, 0x4A, 0x65, 0x68, 0x37, 0x57, 0x68, 0x4D, 0x33, 0x70, 0x49, 0x58, 0x53, 0x37, 0x79, 0x72, 0x2F, 0x32, 0x57, 0x61, 0x6E, 0x76, 0x74, 0x48, 0x32, 0x56, 0x64, 0x79, 0x38, 0x77, 0x6D, 0x68, 0x72, 0x6E, 0x5A, + 0x45, 0x45, 0x32, 0x36, 0x63, 0x4C, 0x55, 0x51, 0x35, 0x76, 0x50, 0x6E, 0x48, 0x4F, 0x36, 0x52, 0x59, 0x50, 0x55, 0x47, 0x39, 0x74, 0x4D, 0x4A, 0x4A, 0x6F, 0x38, 0x67, 0x4E, 0x30, 0x70, 0x63, 0x76, 0x0A, 0x42, 0x32, 0x56, 0x53, 0x41, 0x4B, + 0x64, 0x75, 0x79, 0x4B, 0x39, 0x6F, 0x37, 0x50, 0x51, 0x55, 0x6C, 0x72, 0x5A, 0x58, 0x48, 0x31, 0x62, 0x44, 0x4F, 0x5A, 0x38, 0x72, 0x62, 0x65, 0x54, 0x7A, 0x50, 0x76, 0x59, 0x31, 0x5A, 0x4E, 0x6F, 0x4D, 0x48, 0x4B, 0x47, 0x45, 0x53, 0x79, + 0x39, 0x4C, 0x53, 0x2B, 0x49, 0x73, 0x4A, 0x4A, 0x31, 0x74, 0x6B, 0x30, 0x44, 0x72, 0x74, 0x53, 0x4F, 0x4F, 0x4D, 0x73, 0x70, 0x76, 0x52, 0x64, 0x4F, 0x6F, 0x69, 0x58, 0x73, 0x65, 0x0A, 0x7A, 0x78, 0x37, 0x36, 0x57, 0x30, 0x4F, 0x4C, 0x7A, + 0x63, 0x32, 0x6F, 0x44, 0x32, 0x72, 0x4B, 0x44, 0x46, 0x36, 0x35, 0x6E, 0x6B, 0x65, 0x50, 0x38, 0x4E, 0x6D, 0x32, 0x43, 0x67, 0x74, 0x59, 0x5A, 0x52, 0x63, 0x7A, 0x75, 0x53, 0x50, 0x6B, 0x64, 0x78, 0x6C, 0x39, 0x79, 0x30, 0x6F, 0x75, 0x6B, + 0x6E, 0x74, 0x50, 0x4C, 0x78, 0x42, 0x33, 0x73, 0x59, 0x30, 0x76, 0x61, 0x4A, 0x78, 0x69, 0x7A, 0x4F, 0x42, 0x51, 0x2B, 0x4F, 0x79, 0x52, 0x70, 0x31, 0x52, 0x4D, 0x0A, 0x56, 0x77, 0x6E, 0x56, 0x64, 0x6D, 0x50, 0x46, 0x36, 0x47, 0x55, 0x65, + 0x37, 0x6D, 0x31, 0x71, 0x7A, 0x77, 0x6D, 0x64, 0x2B, 0x6E, 0x78, 0x50, 0x72, 0x57, 0x41, 0x49, 0x2F, 0x56, 0x61, 0x5A, 0x44, 0x78, 0x55, 0x73, 0x65, 0x36, 0x6D, 0x41, 0x71, 0x34, 0x78, 0x68, 0x6A, 0x30, 0x6F, 0x48, 0x64, 0x6B, 0x4C, 0x65, + 0x50, 0x66, 0x54, 0x64, 0x73, 0x69, 0x51, 0x7A, 0x57, 0x37, 0x69, 0x31, 0x6F, 0x30, 0x54, 0x4A, 0x72, 0x48, 0x39, 0x33, 0x50, 0x42, 0x30, 0x6A, 0x0A, 0x37, 0x49, 0x4B, 0x70, 0x70, 0x75, 0x4C, 0x49, 0x42, 0x6B, 0x77, 0x43, 0x2F, 0x71, 0x78, + 0x63, 0x6D, 0x5A, 0x6B, 0x4C, 0x4C, 0x78, 0x43, 0x4B, 0x70, 0x76, 0x52, 0x2F, 0x31, 0x59, 0x64, 0x30, 0x44, 0x56, 0x6C, 0x4A, 0x52, 0x66, 0x62, 0x77, 0x63, 0x56, 0x77, 0x35, 0x4B, 0x64, 0x61, 0x2F, 0x53, 0x69, 0x4F, 0x4C, 0x39, 0x56, 0x38, + 0x42, 0x59, 0x39, 0x4B, 0x48, 0x63, 0x79, 0x69, 0x31, 0x53, 0x77, 0x72, 0x31, 0x2B, 0x4B, 0x75, 0x43, 0x4C, 0x48, 0x35, 0x7A, 0x0A, 0x4A, 0x54, 0x49, 0x64, 0x43, 0x32, 0x4D, 0x4B, 0x46, 0x34, 0x45, 0x41, 0x2F, 0x37, 0x5A, 0x32, 0x58, 0x75, + 0x65, 0x30, 0x73, 0x55, 0x44, 0x4B, 0x49, 0x62, 0x76, 0x56, 0x67, 0x46, 0x48, 0x6C, 0x53, 0x46, 0x4A, 0x6E, 0x4C, 0x4E, 0x4A, 0x68, 0x69, 0x51, 0x63, 0x4E, 0x44, 0x38, 0x35, 0x43, 0x64, 0x38, 0x42, 0x45, 0x63, 0x35, 0x78, 0x45, 0x55, 0x4B, + 0x44, 0x62, 0x45, 0x41, 0x6F, 0x74, 0x6C, 0x52, 0x79, 0x42, 0x72, 0x2B, 0x51, 0x63, 0x35, 0x52, 0x51, 0x65, 0x0A, 0x38, 0x54, 0x5A, 0x42, 0x41, 0x51, 0x49, 0x76, 0x66, 0x58, 0x4F, 0x6E, 0x33, 0x6B, 0x4C, 0x4D, 0x54, 0x4F, 0x6D, 0x4A, 0x44, + 0x56, 0x62, 0x33, 0x6E, 0x35, 0x48, 0x55, 0x41, 0x38, 0x5A, 0x73, 0x79, 0x59, 0x2F, 0x62, 0x32, 0x42, 0x7A, 0x67, 0x51, 0x4A, 0x68, 0x64, 0x5A, 0x70, 0x6D, 0x59, 0x67, 0x47, 0x34, 0x74, 0x2F, 0x77, 0x48, 0x46, 0x7A, 0x73, 0x74, 0x47, 0x48, + 0x36, 0x77, 0x43, 0x78, 0x6B, 0x50, 0x6D, 0x72, 0x71, 0x4B, 0x45, 0x50, 0x4D, 0x56, 0x4F, 0x0A, 0x48, 0x6A, 0x31, 0x74, 0x79, 0x52, 0x52, 0x4D, 0x34, 0x79, 0x35, 0x42, 0x75, 0x38, 0x6F, 0x35, 0x76, 0x7A, 0x59, 0x38, 0x4B, 0x68, 0x6D, 0x71, + 0x51, 0x59, 0x64, 0x4F, 0x70, 0x63, 0x35, 0x4C, 0x4D, 0x6E, 0x6E, 0x64, 0x6B, 0x45, 0x6C, 0x2F, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, + 0x4D, 0x42, 0x61, 0x41, 0x46, 0x4A, 0x78, 0x66, 0x30, 0x47, 0x78, 0x6A, 0x0A, 0x6F, 0x31, 0x2B, 0x54, 0x79, 0x70, 0x4F, 0x59, 0x43, 0x4B, 0x32, 0x4D, 0x68, 0x36, 0x55, 0x73, 0x58, 0x4D, 0x45, 0x33, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, + 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x63, 0x58, 0x39, 0x42, 0x73, 0x59, 0x36, 0x4E, 0x66, 0x6B, 0x38, 0x71, 0x54, 0x6D, 0x41, 0x69, 0x74, 0x6A, 0x49, 0x65, 0x6C, 0x4C, 0x46, 0x7A, 0x42, 0x4E, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, + 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x0A, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, + 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x54, 0x68, 0x36, 0x35, 0x69, 0x73, 0x61, 0x67, 0x6D, 0x44, 0x39, 0x75, 0x77, 0x32, + 0x6E, 0x41, 0x61, 0x6C, 0x78, 0x4A, 0x0A, 0x55, 0x71, 0x7A, 0x4C, 0x4B, 0x31, 0x31, 0x34, 0x4F, 0x4D, 0x48, 0x56, 0x56, 0x49, 0x53, 0x66, 0x6B, 0x2F, 0x43, 0x48, 0x47, 0x54, 0x30, 0x73, 0x5A, 0x6F, 0x6E, 0x72, 0x44, 0x55, 0x4C, 0x38, 0x7A, + 0x50, 0x42, 0x31, 0x68, 0x54, 0x2B, 0x4C, 0x39, 0x49, 0x42, 0x64, 0x65, 0x65, 0x55, 0x58, 0x5A, 0x37, 0x30, 0x31, 0x67, 0x75, 0x4C, 0x79, 0x50, 0x49, 0x35, 0x39, 0x57, 0x7A, 0x62, 0x4C, 0x57, 0x6F, 0x41, 0x41, 0x4B, 0x66, 0x4C, 0x4F, 0x4B, + 0x79, 0x7A, 0x78, 0x0A, 0x6A, 0x36, 0x70, 0x74, 0x42, 0x5A, 0x4E, 0x73, 0x63, 0x73, 0x64, 0x57, 0x36, 0x39, 0x39, 0x51, 0x49, 0x79, 0x6A, 0x6C, 0x52, 0x52, 0x41, 0x39, 0x36, 0x47, 0x65, 0x6A, 0x72, 0x77, 0x35, 0x56, 0x44, 0x35, 0x41, 0x4A, + 0x59, 0x75, 0x39, 0x4C, 0x57, 0x61, 0x4C, 0x32, 0x55, 0x2F, 0x48, 0x41, 0x4E, 0x65, 0x51, 0x76, 0x77, 0x53, 0x53, 0x39, 0x65, 0x53, 0x39, 0x4F, 0x49, 0x43, 0x49, 0x37, 0x2F, 0x52, 0x6F, 0x67, 0x73, 0x4B, 0x51, 0x4F, 0x4C, 0x48, 0x44, 0x74, + 0x0A, 0x64, 0x44, 0x2B, 0x34, 0x45, 0x35, 0x55, 0x47, 0x55, 0x63, 0x6A, 0x6F, 0x68, 0x79, 0x62, 0x4B, 0x70, 0x46, 0x74, 0x71, 0x46, 0x69, 0x47, 0x53, 0x33, 0x58, 0x4E, 0x67, 0x6E, 0x68, 0x41, 0x59, 0x33, 0x6A, 0x79, 0x42, 0x36, 0x75, 0x67, + 0x59, 0x77, 0x33, 0x79, 0x4A, 0x38, 0x6F, 0x74, 0x51, 0x50, 0x72, 0x30, 0x52, 0x34, 0x68, 0x55, 0x44, 0x71, 0x44, 0x5A, 0x39, 0x4D, 0x77, 0x46, 0x73, 0x53, 0x42, 0x58, 0x58, 0x69, 0x4A, 0x43, 0x5A, 0x42, 0x4D, 0x58, 0x4D, 0x0A, 0x35, 0x67, + 0x66, 0x30, 0x76, 0x50, 0x53, 0x51, 0x37, 0x52, 0x50, 0x69, 0x36, 0x6F, 0x76, 0x44, 0x6A, 0x36, 0x4D, 0x7A, 0x44, 0x38, 0x45, 0x70, 0x54, 0x42, 0x4E, 0x4F, 0x32, 0x68, 0x56, 0x57, 0x63, 0x58, 0x4E, 0x79, 0x67, 0x6C, 0x44, 0x32, 0x6D, 0x6A, + 0x4E, 0x38, 0x6F, 0x72, 0x47, 0x6F, 0x47, 0x6A, 0x52, 0x30, 0x5A, 0x56, 0x7A, 0x4F, 0x30, 0x65, 0x75, 0x72, 0x55, 0x2B, 0x41, 0x61, 0x67, 0x4E, 0x6A, 0x71, 0x4F, 0x6B, 0x6E, 0x6B, 0x4A, 0x6A, 0x43, 0x62, 0x0A, 0x35, 0x52, 0x79, 0x4B, 0x71, + 0x4B, 0x6B, 0x56, 0x4D, 0x6F, 0x61, 0x5A, 0x6B, 0x67, 0x6F, 0x51, 0x49, 0x31, 0x59, 0x53, 0x34, 0x50, 0x62, 0x4F, 0x54, 0x4F, 0x4B, 0x37, 0x76, 0x74, 0x75, 0x4E, 0x6B, 0x6E, 0x4D, 0x42, 0x5A, 0x69, 0x39, 0x69, 0x50, 0x72, 0x4A, 0x79, 0x4A, + 0x30, 0x55, 0x32, 0x37, 0x55, 0x31, 0x57, 0x34, 0x35, 0x65, 0x5A, 0x2F, 0x7A, 0x6F, 0x31, 0x50, 0x71, 0x56, 0x55, 0x53, 0x6C, 0x4A, 0x5A, 0x53, 0x32, 0x44, 0x62, 0x37, 0x76, 0x35, 0x34, 0x0A, 0x45, 0x58, 0x39, 0x4B, 0x33, 0x42, 0x52, 0x35, + 0x59, 0x4C, 0x5A, 0x72, 0x5A, 0x41, 0x50, 0x62, 0x46, 0x59, 0x50, 0x68, 0x6F, 0x72, 0x37, 0x32, 0x49, 0x35, 0x64, 0x51, 0x38, 0x41, 0x6B, 0x7A, 0x4E, 0x71, 0x64, 0x78, 0x6C, 0x69, 0x58, 0x7A, 0x75, 0x55, 0x4A, 0x39, 0x32, 0x7A, 0x67, 0x2F, + 0x4C, 0x46, 0x69, 0x73, 0x36, 0x45, 0x4C, 0x68, 0x44, 0x74, 0x6A, 0x54, 0x4F, 0x30, 0x77, 0x75, 0x67, 0x75, 0x6D, 0x44, 0x4C, 0x6D, 0x73, 0x78, 0x32, 0x64, 0x31, 0x48, 0x0A, 0x68, 0x6B, 0x39, 0x74, 0x6C, 0x35, 0x45, 0x75, 0x54, 0x2B, 0x49, + 0x6F, 0x63, 0x54, 0x55, 0x57, 0x30, 0x66, 0x4A, 0x7A, 0x2F, 0x69, 0x55, 0x72, 0x42, 0x30, 0x63, 0x6B, 0x59, 0x79, 0x66, 0x49, 0x2B, 0x50, 0x62, 0x5A, 0x61, 0x2F, 0x77, 0x53, 0x4D, 0x56, 0x59, 0x49, 0x77, 0x46, 0x4E, 0x43, 0x72, 0x35, 0x7A, + 0x51, 0x4D, 0x33, 0x37, 0x38, 0x42, 0x76, 0x41, 0x78, 0x52, 0x41, 0x4D, 0x55, 0x38, 0x56, 0x6A, 0x71, 0x38, 0x6D, 0x6F, 0x4E, 0x71, 0x52, 0x47, 0x79, 0x0A, 0x67, 0x37, 0x37, 0x46, 0x47, 0x72, 0x38, 0x48, 0x36, 0x6C, 0x6E, 0x63, 0x6F, 0x34, + 0x67, 0x31, 0x37, 0x35, 0x78, 0x32, 0x4D, 0x6A, 0x78, 0x4E, 0x42, 0x69, 0x4C, 0x4F, 0x46, 0x65, 0x58, 0x64, 0x6E, 0x74, 0x69, 0x50, 0x32, 0x74, 0x37, 0x53, 0x78, 0x44, 0x6E, 0x6C, 0x46, 0x34, 0x48, 0x50, 0x4F, 0x45, 0x66, 0x72, 0x66, 0x34, + 0x68, 0x74, 0x57, 0x52, 0x76, 0x66, 0x6E, 0x30, 0x49, 0x55, 0x72, 0x6E, 0x37, 0x50, 0x71, 0x4C, 0x42, 0x6D, 0x5A, 0x64, 0x6F, 0x33, 0x0A, 0x72, 0x35, 0x2B, 0x71, 0x50, 0x65, 0x6F, 0x6F, 0x74, 0x74, 0x37, 0x56, 0x4D, 0x56, 0x67, 0x57, 0x67, + 0x6C, 0x76, 0x71, 0x75, 0x78, 0x6C, 0x31, 0x41, 0x6E, 0x4D, 0x61, 0x79, 0x6B, 0x67, 0x61, 0x49, 0x5A, 0x4F, 0x51, 0x43, 0x6F, 0x36, 0x54, 0x68, 0x4B, 0x64, 0x39, 0x4F, 0x79, 0x4D, 0x59, 0x6B, 0x6F, 0x6D, 0x67, 0x6A, 0x61, 0x77, 0x3D, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x75, 0x6D, 0x20, 0x45, 0x43, 0x2D, 0x33, 0x38, 0x34, + 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x5A, 0x54, 0x43, 0x43, 0x41, 0x65, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x65, 0x49, 0x38, 0x6E, 0x58, 0x49, 0x45, 0x53, 0x55, 0x69, 0x43, + 0x6C, 0x42, 0x4E, 0x41, 0x74, 0x33, 0x62, 0x70, 0x7A, 0x39, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x30, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x47, 0x45, 0x77, 0x4A, 0x51, 0x0A, 0x54, 0x44, 0x45, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x59, 0x51, 0x58, 0x4E, 0x7A, 0x5A, 0x57, 0x4E, 0x76, 0x49, 0x45, 0x52, 0x68, 0x64, 0x47, 0x45, 0x67, 0x55, 0x33, + 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x54, 0x4C, 0x6B, 0x45, 0x75, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x35, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, 0x30, 0x67, 0x51, 0x32, + 0x56, 0x79, 0x0A, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x4D, 0x54, 0x45, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x31, 0x62, 0x53, 0x42, 0x46, 0x51, 0x79, 0x30, 0x7A, 0x4F, 0x44, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x67, 0x77, 0x4D, 0x7A, 0x49, 0x32, 0x0A, + 0x4D, 0x44, 0x63, 0x79, 0x4E, 0x44, 0x55, 0x30, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x4D, 0x77, 0x4D, 0x7A, 0x49, 0x32, 0x4D, 0x44, 0x63, 0x79, 0x4E, 0x44, 0x55, 0x30, 0x57, 0x6A, 0x42, 0x30, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x51, 0x54, 0x44, 0x45, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x59, 0x51, 0x58, 0x4E, 0x7A, 0x5A, 0x57, 0x4E, 0x76, 0x49, 0x45, 0x52, 0x68, 0x0A, 0x64, 0x47, 0x45, + 0x67, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x54, 0x4C, 0x6B, 0x45, 0x75, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x35, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, 0x30, + 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x0A, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x45, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x31, 0x62, 0x53, 0x42, 0x46, 0x51, 0x79, 0x30, 0x7A, 0x4F, 0x44, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, + 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x54, 0x45, 0x4B, 0x49, 0x36, 0x72, 0x47, 0x46, 0x74, 0x71, 0x0A, 0x76, 0x6D, 0x35, 0x6B, 0x4E, 0x32, 0x50, 0x6B, 0x7A, + 0x65, 0x79, 0x72, 0x4F, 0x76, 0x66, 0x4D, 0x6F, 0x62, 0x67, 0x4F, 0x67, 0x6B, 0x6E, 0x58, 0x68, 0x69, 0x6D, 0x66, 0x6F, 0x5A, 0x54, 0x79, 0x34, 0x32, 0x42, 0x34, 0x6D, 0x49, 0x46, 0x34, 0x42, 0x6B, 0x33, 0x79, 0x37, 0x4A, 0x6F, 0x4F, 0x56, + 0x32, 0x43, 0x44, 0x6E, 0x37, 0x54, 0x6D, 0x46, 0x79, 0x38, 0x61, 0x73, 0x31, 0x30, 0x43, 0x57, 0x34, 0x6B, 0x6A, 0x50, 0x4D, 0x49, 0x52, 0x42, 0x53, 0x71, 0x6E, 0x0A, 0x69, 0x42, 0x4D, 0x59, 0x38, 0x31, 0x43, 0x45, 0x31, 0x37, 0x30, 0x30, + 0x4C, 0x43, 0x65, 0x4A, 0x56, 0x66, 0x2F, 0x4F, 0x54, 0x4F, 0x66, 0x66, 0x70, 0x68, 0x38, 0x6F, 0x78, 0x50, 0x42, 0x55, 0x77, 0x37, 0x6C, 0x38, 0x74, 0x31, 0x4F, 0x74, 0x36, 0x38, 0x4B, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x49, 0x30, 0x47, 0x5A, 0x6E, 0x51, + 0x6B, 0x64, 0x6A, 0x72, 0x7A, 0x69, 0x66, 0x65, 0x38, 0x31, 0x72, 0x31, 0x48, 0x66, 0x53, 0x2B, 0x38, 0x45, 0x46, 0x39, 0x4C, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, + 0x42, 0x42, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x0A, 0x41, 0x44, 0x42, 0x6C, 0x41, 0x6A, 0x41, 0x44, 0x56, 0x53, 0x32, 0x6D, 0x35, 0x68, 0x6A, 0x45, 0x66, 0x4F, + 0x2F, 0x4A, 0x55, 0x47, 0x37, 0x42, 0x4A, 0x77, 0x2B, 0x63, 0x68, 0x36, 0x39, 0x75, 0x31, 0x52, 0x73, 0x49, 0x47, 0x4C, 0x32, 0x53, 0x4B, 0x63, 0x48, 0x76, 0x6C, 0x4A, 0x46, 0x34, 0x30, 0x6A, 0x6F, 0x63, 0x56, 0x59, 0x6C, 0x69, 0x35, 0x52, + 0x73, 0x4A, 0x48, 0x72, 0x70, 0x6B, 0x61, 0x2F, 0x46, 0x32, 0x74, 0x4E, 0x51, 0x43, 0x4D, 0x51, 0x43, 0x30, 0x0A, 0x51, 0x6F, 0x53, 0x5A, 0x2F, 0x36, 0x76, 0x6E, 0x6E, 0x76, 0x75, 0x52, 0x6C, 0x79, 0x64, 0x64, 0x33, 0x4C, 0x42, 0x62, 0x4D, + 0x48, 0x48, 0x4F, 0x58, 0x6A, 0x67, 0x61, 0x61, 0x74, 0x6B, 0x6C, 0x35, 0x2B, 0x72, 0x33, 0x59, 0x5A, 0x4A, 0x57, 0x2B, 0x4F, 0x72, 0x61, 0x4E, 0x73, 0x4B, 0x48, 0x5A, 0x5A, 0x59, 0x75, 0x63, 0x69, 0x55, 0x76, 0x66, 0x39, 0x2F, 0x44, 0x45, + 0x38, 0x6B, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x75, 0x6D, 0x20, 0x54, 0x72, + 0x75, 0x73, 0x74, 0x65, 0x64, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x77, 0x44, 0x43, 0x43, 0x41, 0x36, 0x69, 0x67, 0x41, 0x77, 0x49, + 0x42, 0x41, 0x67, 0x49, 0x51, 0x48, 0x72, 0x39, 0x5A, 0x55, 0x4C, 0x6A, 0x4A, 0x67, 0x44, 0x64, 0x4D, 0x42, 0x76, 0x66, 0x72, 0x56, 0x55, 0x2B, 0x31, 0x37, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, + 0x42, 0x41, 0x51, 0x30, 0x46, 0x41, 0x44, 0x42, 0x36, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x51, 0x54, 0x44, 0x45, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, + 0x4D, 0x59, 0x51, 0x58, 0x4E, 0x7A, 0x5A, 0x57, 0x4E, 0x76, 0x49, 0x45, 0x52, 0x68, 0x64, 0x47, 0x45, 0x67, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, 0x54, 0x4C, 0x6B, 0x45, 0x75, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x35, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, 0x30, 0x67, 0x0A, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x64, + 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x6B, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x31, 0x62, 0x53, 0x42, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, + 0x47, 0x56, 0x6B, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x0A, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x67, 0x77, 0x4D, 0x7A, 0x45, 0x32, 0x4D, 0x54, 0x49, 0x78, 0x4D, 0x44, 0x45, 0x7A, 0x57, 0x68, 0x63, 0x4E, + 0x4E, 0x44, 0x4D, 0x77, 0x4D, 0x7A, 0x45, 0x32, 0x4D, 0x54, 0x49, 0x78, 0x4D, 0x44, 0x45, 0x7A, 0x57, 0x6A, 0x42, 0x36, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x51, 0x54, 0x44, 0x45, 0x68, + 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x59, 0x0A, 0x51, 0x58, 0x4E, 0x7A, 0x5A, 0x57, 0x4E, 0x76, 0x49, 0x45, 0x52, 0x68, 0x64, 0x47, 0x45, 0x67, 0x55, 0x33, 0x6C, 0x7A, 0x64, 0x47, 0x56, 0x74, 0x63, 0x79, 0x42, + 0x54, 0x4C, 0x6B, 0x45, 0x75, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x35, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x64, 0x57, 0x30, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, + 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x42, 0x0A, 0x64, 0x58, 0x52, 0x6F, 0x62, 0x33, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x78, 0x48, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x46, 0x6B, 0x4E, 0x6C, 0x63, 0x6E, + 0x52, 0x31, 0x62, 0x53, 0x42, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x47, 0x56, 0x6B, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, + 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x0A, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x52, 0x4C, 0x59, 0x36, 0x37, 0x74, 0x7A, 0x62, 0x71, 0x62, + 0x54, 0x65, 0x52, 0x6E, 0x30, 0x36, 0x54, 0x70, 0x77, 0x58, 0x6B, 0x4B, 0x51, 0x4D, 0x6C, 0x7A, 0x68, 0x79, 0x43, 0x39, 0x33, 0x79, 0x5A, 0x6E, 0x30, 0x45, 0x47, 0x7A, 0x65, 0x32, 0x6A, 0x75, 0x73, 0x44, 0x62, 0x43, 0x53, 0x7A, 0x42, 0x66, + 0x4E, 0x38, 0x70, 0x0A, 0x66, 0x6B, 0x74, 0x6C, 0x4C, 0x35, 0x4F, 0x6E, 0x31, 0x41, 0x46, 0x72, 0x41, 0x79, 0x67, 0x59, 0x6F, 0x39, 0x69, 0x64, 0x42, 0x63, 0x45, 0x71, 0x32, 0x45, 0x58, 0x78, 0x6B, 0x64, 0x37, 0x66, 0x4F, 0x39, 0x43, 0x41, + 0x41, 0x6F, 0x7A, 0x50, 0x4F, 0x41, 0x2F, 0x71, 0x70, 0x31, 0x78, 0x34, 0x45, 0x61, 0x54, 0x42, 0x79, 0x49, 0x56, 0x63, 0x4A, 0x64, 0x50, 0x54, 0x73, 0x75, 0x63, 0x6C, 0x7A, 0x78, 0x46, 0x55, 0x6C, 0x36, 0x73, 0x31, 0x77, 0x42, 0x35, 0x32, + 0x0A, 0x48, 0x4F, 0x38, 0x41, 0x55, 0x35, 0x38, 0x35, 0x33, 0x42, 0x53, 0x6C, 0x4C, 0x43, 0x49, 0x6C, 0x73, 0x33, 0x4A, 0x79, 0x2F, 0x49, 0x32, 0x7A, 0x35, 0x54, 0x34, 0x49, 0x48, 0x68, 0x51, 0x71, 0x4E, 0x77, 0x75, 0x49, 0x50, 0x4D, 0x71, + 0x77, 0x39, 0x4D, 0x6A, 0x43, 0x6F, 0x61, 0x36, 0x38, 0x77, 0x62, 0x34, 0x70, 0x5A, 0x31, 0x58, 0x69, 0x2F, 0x4B, 0x31, 0x5A, 0x58, 0x50, 0x36, 0x39, 0x56, 0x79, 0x79, 0x77, 0x6B, 0x49, 0x33, 0x43, 0x37, 0x54, 0x65, 0x32, 0x0A, 0x66, 0x4A, + 0x6D, 0x49, 0x74, 0x64, 0x55, 0x44, 0x6D, 0x6A, 0x30, 0x56, 0x44, 0x54, 0x30, 0x36, 0x71, 0x4B, 0x68, 0x46, 0x38, 0x4A, 0x56, 0x4F, 0x4A, 0x56, 0x6B, 0x64, 0x7A, 0x5A, 0x68, 0x70, 0x75, 0x39, 0x50, 0x4D, 0x4D, 0x73, 0x6D, 0x4E, 0x37, 0x34, + 0x48, 0x2B, 0x72, 0x58, 0x32, 0x4A, 0x75, 0x37, 0x70, 0x67, 0x45, 0x38, 0x70, 0x6C, 0x6C, 0x57, 0x65, 0x67, 0x38, 0x78, 0x6E, 0x32, 0x41, 0x31, 0x62, 0x55, 0x61, 0x74, 0x4D, 0x6E, 0x34, 0x71, 0x47, 0x74, 0x0A, 0x67, 0x2F, 0x42, 0x4B, 0x45, + 0x69, 0x4A, 0x33, 0x48, 0x41, 0x56, 0x7A, 0x34, 0x68, 0x6C, 0x78, 0x51, 0x73, 0x44, 0x73, 0x64, 0x55, 0x61, 0x61, 0x6B, 0x46, 0x6A, 0x67, 0x61, 0x6F, 0x34, 0x72, 0x70, 0x55, 0x59, 0x77, 0x42, 0x49, 0x34, 0x5A, 0x73, 0x68, 0x66, 0x6A, 0x76, + 0x71, 0x6D, 0x36, 0x66, 0x31, 0x62, 0x78, 0x4A, 0x41, 0x50, 0x58, 0x73, 0x69, 0x45, 0x6F, 0x64, 0x67, 0x34, 0x32, 0x4D, 0x45, 0x78, 0x35, 0x31, 0x55, 0x47, 0x61, 0x6D, 0x71, 0x69, 0x34, 0x0A, 0x4E, 0x62, 0x6F, 0x4D, 0x4F, 0x76, 0x4A, 0x45, + 0x47, 0x79, 0x43, 0x49, 0x39, 0x38, 0x55, 0x6C, 0x31, 0x7A, 0x33, 0x47, 0x34, 0x7A, 0x35, 0x44, 0x33, 0x59, 0x66, 0x2B, 0x78, 0x4F, 0x72, 0x31, 0x55, 0x7A, 0x35, 0x4D, 0x5A, 0x66, 0x38, 0x37, 0x53, 0x73, 0x74, 0x34, 0x57, 0x6D, 0x73, 0x58, + 0x58, 0x77, 0x33, 0x48, 0x77, 0x30, 0x39, 0x4F, 0x6D, 0x69, 0x71, 0x69, 0x37, 0x56, 0x64, 0x4E, 0x49, 0x75, 0x4A, 0x47, 0x6D, 0x6A, 0x38, 0x50, 0x6B, 0x54, 0x51, 0x6B, 0x0A, 0x66, 0x56, 0x58, 0x6A, 0x6A, 0x4A, 0x55, 0x33, 0x30, 0x78, 0x72, + 0x77, 0x43, 0x53, 0x73, 0x73, 0x30, 0x73, 0x6D, 0x4E, 0x74, 0x41, 0x30, 0x41, 0x71, 0x32, 0x63, 0x70, 0x4B, 0x4E, 0x67, 0x42, 0x39, 0x52, 0x6B, 0x45, 0x74, 0x68, 0x32, 0x2B, 0x64, 0x76, 0x35, 0x79, 0x58, 0x4D, 0x53, 0x46, 0x79, 0x74, 0x4B, + 0x41, 0x51, 0x64, 0x38, 0x46, 0x71, 0x4B, 0x50, 0x56, 0x68, 0x4A, 0x42, 0x50, 0x43, 0x2F, 0x50, 0x67, 0x50, 0x35, 0x73, 0x5A, 0x30, 0x6A, 0x65, 0x4A, 0x0A, 0x50, 0x2F, 0x4A, 0x37, 0x55, 0x68, 0x79, 0x4D, 0x39, 0x75, 0x48, 0x33, 0x50, 0x41, + 0x65, 0x58, 0x6A, 0x41, 0x36, 0x69, 0x57, 0x59, 0x45, 0x4D, 0x73, 0x70, 0x41, 0x39, 0x30, 0x2B, 0x4E, 0x5A, 0x52, 0x75, 0x30, 0x50, 0x71, 0x61, 0x66, 0x65, 0x67, 0x47, 0x74, 0x61, 0x71, 0x67, 0x65, 0x32, 0x47, 0x63, 0x75, 0x38, 0x56, 0x2F, + 0x4F, 0x58, 0x49, 0x58, 0x6F, 0x4D, 0x73, 0x53, 0x74, 0x30, 0x50, 0x75, 0x76, 0x61, 0x70, 0x32, 0x63, 0x74, 0x54, 0x4D, 0x53, 0x59, 0x0A, 0x6E, 0x6A, 0x59, 0x4A, 0x64, 0x6D, 0x5A, 0x6D, 0x2F, 0x42, 0x6F, 0x2F, 0x36, 0x6B, 0x68, 0x55, 0x48, + 0x4C, 0x34, 0x77, 0x76, 0x59, 0x42, 0x51, 0x76, 0x33, 0x79, 0x31, 0x7A, 0x67, 0x44, 0x32, 0x44, 0x47, 0x48, 0x5A, 0x35, 0x79, 0x51, 0x44, 0x34, 0x4F, 0x4D, 0x42, 0x67, 0x51, 0x36, 0x39, 0x32, 0x49, 0x55, 0x30, 0x69, 0x4C, 0x32, 0x79, 0x4E, + 0x71, 0x68, 0x37, 0x58, 0x41, 0x6A, 0x6C, 0x52, 0x49, 0x43, 0x4D, 0x62, 0x2F, 0x67, 0x76, 0x31, 0x53, 0x48, 0x4B, 0x0A, 0x48, 0x52, 0x7A, 0x51, 0x2B, 0x38, 0x53, 0x31, 0x68, 0x39, 0x45, 0x36, 0x54, 0x73, 0x64, 0x32, 0x74, 0x54, 0x56, 0x49, + 0x74, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x4D, 0x2B, 0x78, 0x78, 0x31, 0x0A, 0x76, 0x41, 0x4C, 0x54, 0x6E, 0x30, 0x34, 0x75, 0x53, 0x4E, 0x6E, 0x35, 0x59, 0x46, 0x53, 0x71, 0x78, 0x4C, 0x4E, 0x50, 0x2B, 0x6A, 0x41, + 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4E, 0x42, 0x51, 0x41, + 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x45, 0x69, 0x69, 0x31, 0x51, 0x41, 0x4C, 0x0A, 0x4C, 0x74, 0x41, 0x2F, 0x76, 0x42, 0x7A, 0x56, 0x74, 0x56, 0x52, 0x4A, 0x48, 0x6C, 0x70, 0x72, 0x39, 0x4F, 0x54, 0x79, 0x34, 0x45, 0x41, 0x33, 0x34, 0x4D, + 0x77, 0x55, 0x65, 0x37, 0x6E, 0x4A, 0x2B, 0x6A, 0x57, 0x31, 0x64, 0x52, 0x65, 0x54, 0x61, 0x67, 0x56, 0x70, 0x68, 0x5A, 0x7A, 0x4E, 0x54, 0x78, 0x6C, 0x34, 0x57, 0x78, 0x6D, 0x42, 0x38, 0x32, 0x4D, 0x2B, 0x77, 0x38, 0x35, 0x62, 0x6A, 0x2F, + 0x55, 0x76, 0x58, 0x67, 0x46, 0x32, 0x45, 0x7A, 0x38, 0x73, 0x0A, 0x41, 0x4C, 0x6E, 0x4E, 0x6C, 0x6C, 0x49, 0x35, 0x53, 0x57, 0x30, 0x45, 0x54, 0x73, 0x58, 0x70, 0x44, 0x34, 0x59, 0x4E, 0x34, 0x66, 0x71, 0x7A, 0x58, 0x34, 0x49, 0x53, 0x38, + 0x54, 0x72, 0x4F, 0x5A, 0x67, 0x59, 0x6B, 0x4E, 0x43, 0x76, 0x6F, 0x7A, 0x4D, 0x72, 0x6E, 0x61, 0x64, 0x79, 0x48, 0x6E, 0x63, 0x49, 0x30, 0x31, 0x33, 0x6E, 0x52, 0x30, 0x33, 0x65, 0x34, 0x71, 0x6C, 0x6C, 0x59, 0x2F, 0x70, 0x30, 0x6D, 0x2B, + 0x6A, 0x69, 0x47, 0x50, 0x70, 0x32, 0x4B, 0x0A, 0x68, 0x32, 0x52, 0x58, 0x35, 0x52, 0x63, 0x36, 0x34, 0x76, 0x6D, 0x4E, 0x75, 0x65, 0x4D, 0x7A, 0x65, 0x4D, 0x47, 0x51, 0x32, 0x4C, 0x6A, 0x64, 0x74, 0x34, 0x4E, 0x52, 0x35, 0x4D, 0x54, 0x4D, + 0x49, 0x39, 0x55, 0x47, 0x66, 0x4F, 0x5A, 0x52, 0x30, 0x38, 0x30, 0x30, 0x4D, 0x63, 0x44, 0x32, 0x52, 0x72, 0x73, 0x4C, 0x72, 0x66, 0x77, 0x39, 0x45, 0x41, 0x55, 0x71, 0x4F, 0x30, 0x71, 0x52, 0x4A, 0x65, 0x36, 0x4D, 0x31, 0x49, 0x53, 0x48, + 0x67, 0x43, 0x71, 0x38, 0x0A, 0x43, 0x59, 0x79, 0x71, 0x4F, 0x68, 0x4E, 0x66, 0x36, 0x44, 0x52, 0x35, 0x55, 0x4D, 0x45, 0x51, 0x47, 0x66, 0x6E, 0x54, 0x4B, 0x42, 0x37, 0x55, 0x30, 0x56, 0x45, 0x77, 0x4B, 0x62, 0x4F, 0x75, 0x6B, 0x47, 0x66, + 0x57, 0x48, 0x77, 0x70, 0x6A, 0x73, 0x63, 0x57, 0x70, 0x78, 0x6B, 0x49, 0x78, 0x59, 0x78, 0x65, 0x55, 0x37, 0x32, 0x6E, 0x4C, 0x4C, 0x2F, 0x71, 0x4D, 0x46, 0x48, 0x33, 0x45, 0x51, 0x78, 0x69, 0x4A, 0x32, 0x66, 0x41, 0x79, 0x51, 0x4F, 0x61, + 0x41, 0x0A, 0x34, 0x6B, 0x5A, 0x66, 0x35, 0x65, 0x50, 0x42, 0x41, 0x46, 0x6D, 0x6F, 0x2B, 0x65, 0x67, 0x67, 0x76, 0x49, 0x6B, 0x73, 0x44, 0x6B, 0x63, 0x30, 0x43, 0x2B, 0x70, 0x58, 0x77, 0x6C, 0x4D, 0x32, 0x2F, 0x4B, 0x66, 0x55, 0x72, 0x7A, + 0x48, 0x4E, 0x2F, 0x67, 0x4C, 0x6C, 0x64, 0x66, 0x71, 0x35, 0x4A, 0x77, 0x6E, 0x35, 0x38, 0x2F, 0x55, 0x37, 0x79, 0x6E, 0x32, 0x66, 0x71, 0x53, 0x4C, 0x4C, 0x69, 0x4D, 0x6D, 0x71, 0x30, 0x55, 0x63, 0x39, 0x4E, 0x6E, 0x65, 0x6F, 0x0A, 0x57, + 0x57, 0x52, 0x72, 0x4A, 0x38, 0x2F, 0x76, 0x4A, 0x38, 0x48, 0x6A, 0x4A, 0x4C, 0x57, 0x47, 0x39, 0x36, 0x35, 0x2B, 0x4D, 0x6B, 0x32, 0x77, 0x65, 0x57, 0x6A, 0x52, 0x4F, 0x65, 0x69, 0x51, 0x57, 0x4D, 0x4F, 0x44, 0x76, 0x41, 0x38, 0x73, 0x31, + 0x70, 0x66, 0x72, 0x7A, 0x67, 0x7A, 0x68, 0x49, 0x4D, 0x66, 0x61, 0x74, 0x7A, 0x37, 0x44, 0x50, 0x37, 0x38, 0x76, 0x33, 0x44, 0x53, 0x6B, 0x2B, 0x79, 0x73, 0x68, 0x7A, 0x57, 0x65, 0x50, 0x53, 0x2F, 0x54, 0x6A, 0x0A, 0x36, 0x74, 0x51, 0x2F, + 0x35, 0x30, 0x2B, 0x36, 0x75, 0x61, 0x57, 0x54, 0x52, 0x52, 0x78, 0x6D, 0x48, 0x79, 0x48, 0x36, 0x5A, 0x46, 0x35, 0x76, 0x34, 0x48, 0x61, 0x55, 0x4D, 0x73, 0x74, 0x31, 0x39, 0x57, 0x37, 0x6C, 0x39, 0x6F, 0x2F, 0x48, 0x75, 0x4B, 0x54, 0x4D, + 0x71, 0x4A, 0x5A, 0x39, 0x5A, 0x50, 0x73, 0x6B, 0x57, 0x6B, 0x6F, 0x44, 0x62, 0x47, 0x73, 0x34, 0x78, 0x75, 0x67, 0x44, 0x51, 0x35, 0x72, 0x33, 0x56, 0x37, 0x6D, 0x7A, 0x4B, 0x57, 0x6D, 0x54, 0x0A, 0x4F, 0x50, 0x51, 0x44, 0x38, 0x72, 0x76, + 0x37, 0x67, 0x6D, 0x73, 0x48, 0x49, 0x4E, 0x46, 0x53, 0x48, 0x35, 0x70, 0x6B, 0x41, 0x6E, 0x75, 0x59, 0x5A, 0x74, 0x74, 0x63, 0x54, 0x56, 0x6F, 0x50, 0x30, 0x49, 0x53, 0x56, 0x6F, 0x44, 0x77, 0x55, 0x51, 0x77, 0x62, 0x4B, 0x79, 0x74, 0x75, + 0x34, 0x51, 0x54, 0x62, 0x61, 0x61, 0x6B, 0x52, 0x6E, 0x68, 0x36, 0x2B, 0x76, 0x34, 0x30, 0x55, 0x52, 0x46, 0x57, 0x6B, 0x49, 0x73, 0x72, 0x34, 0x57, 0x4F, 0x5A, 0x63, 0x6B, 0x0A, 0x62, 0x78, 0x4A, 0x46, 0x30, 0x57, 0x64, 0x64, 0x43, 0x61, + 0x6A, 0x4A, 0x46, 0x64, 0x72, 0x36, 0x30, 0x71, 0x5A, 0x66, 0x45, 0x32, 0x45, 0x66, 0x76, 0x34, 0x57, 0x73, 0x74, 0x4B, 0x32, 0x74, 0x42, 0x5A, 0x51, 0x49, 0x67, 0x78, 0x35, 0x31, 0x46, 0x39, 0x4E, 0x78, 0x4F, 0x35, 0x4E, 0x51, 0x49, 0x31, + 0x6D, 0x67, 0x37, 0x54, 0x79, 0x52, 0x56, 0x4A, 0x31, 0x32, 0x41, 0x4D, 0x58, 0x44, 0x75, 0x44, 0x6A, 0x62, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x75, 0x6E, 0x54, 0x72, 0x75, 0x73, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x73, 0x7A, 0x43, 0x43, 0x41, 0x35, 0x75, 0x67, + 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x45, 0x77, 0x4C, 0x56, 0x34, 0x6B, 0x42, 0x4D, 0x6B, 0x6B, 0x61, 0x47, 0x46, 0x6D, 0x64, 0x64, 0x74, 0x4C, 0x75, 0x37, 0x73, 0x6D, 0x73, 0x2B, 0x2F, 0x42, 0x4D, 0x77, 0x44, 0x51, 0x59, 0x4A, + 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x59, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x45, 0x34, 0x78, 0x4E, 0x7A, 0x41, + 0x31, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x4C, 0x6B, 0x46, 0x6E, 0x5A, 0x57, 0x35, 0x6A, 0x5A, 0x53, 0x42, 0x4F, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x35, 0x68, 0x62, 0x47, 0x55, 0x67, 0x5A, 0x47, 0x55, 0x67, 0x51, 0x32, 0x56, + 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x46, 0x62, 0x47, 0x56, 0x6A, 0x0A, 0x64, 0x48, 0x4A, 0x76, 0x62, 0x6D, 0x6C, 0x78, 0x64, 0x57, 0x55, 0x78, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x45, 0x46, 0x52, 0x31, 0x62, 0x6C, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x6B, 0x77, 0x4E, 0x44, + 0x49, 0x32, 0x4D, 0x44, 0x67, 0x31, 0x4E, 0x7A, 0x55, 0x32, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x51, 0x77, 0x0A, 0x4E, 0x44, 0x49, 0x32, 0x4D, 0x44, 0x67, 0x31, 0x4E, 0x7A, 0x55, 0x32, 0x57, 0x6A, 0x42, 0x68, 0x4D, 0x51, 0x73, 0x77, 0x43, + 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x55, 0x54, 0x6A, 0x45, 0x33, 0x4D, 0x44, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x75, 0x51, 0x57, 0x64, 0x6C, 0x62, 0x6D, 0x4E, 0x6C, 0x49, 0x45, 0x35, 0x68, 0x64, + 0x47, 0x6C, 0x76, 0x62, 0x6D, 0x46, 0x73, 0x5A, 0x53, 0x42, 0x6B, 0x5A, 0x53, 0x42, 0x44, 0x0A, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x56, 0x73, 0x5A, 0x57, 0x4E, 0x30, + 0x63, 0x6D, 0x39, 0x75, 0x61, 0x58, 0x46, 0x31, 0x5A, 0x54, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x51, 0x56, 0x48, 0x56, 0x75, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, + 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, + 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4D, 0x50, 0x4E, 0x30, 0x2F, 0x79, 0x39, 0x42, 0x46, 0x50, 0x64, 0x44, 0x43, 0x41, 0x36, 0x31, 0x59, 0x67, 0x75, 0x42, 0x55, 0x74, 0x42, 0x39, 0x59, 0x4F, 0x43, 0x66, 0x76, 0x64, + 0x5A, 0x6E, 0x35, 0x36, 0x65, 0x59, 0x2B, 0x68, 0x7A, 0x0A, 0x32, 0x76, 0x59, 0x47, 0x71, 0x55, 0x38, 0x66, 0x74, 0x50, 0x6B, 0x4C, 0x48, 0x7A, 0x6D, 0x4D, 0x6D, 0x69, 0x44, 0x51, 0x66, 0x67, 0x62, 0x55, 0x37, 0x44, 0x54, 0x5A, 0x68, 0x72, + 0x78, 0x31, 0x57, 0x34, 0x65, 0x49, 0x38, 0x4E, 0x4C, 0x5A, 0x31, 0x4B, 0x4D, 0x4B, 0x73, 0x6D, 0x77, 0x62, 0x36, 0x30, 0x6B, 0x73, 0x50, 0x71, 0x78, 0x64, 0x32, 0x4A, 0x51, 0x44, 0x6F, 0x4F, 0x77, 0x30, 0x35, 0x54, 0x44, 0x45, 0x4E, 0x58, + 0x33, 0x37, 0x4A, 0x6B, 0x30, 0x62, 0x0A, 0x62, 0x6A, 0x42, 0x55, 0x32, 0x50, 0x57, 0x41, 0x52, 0x5A, 0x77, 0x35, 0x72, 0x5A, 0x7A, 0x4A, 0x4A, 0x51, 0x52, 0x4E, 0x6D, 0x70, 0x41, 0x2B, 0x54, 0x6B, 0x42, 0x75, 0x69, 0x6D, 0x76, 0x4E, 0x4B, + 0x57, 0x66, 0x47, 0x7A, 0x43, 0x33, 0x67, 0x64, 0x4F, 0x67, 0x46, 0x56, 0x77, 0x70, 0x49, 0x55, 0x50, 0x70, 0x36, 0x51, 0x39, 0x70, 0x2B, 0x37, 0x46, 0x75, 0x61, 0x44, 0x6D, 0x4A, 0x32, 0x2F, 0x75, 0x71, 0x64, 0x48, 0x59, 0x56, 0x79, 0x37, + 0x42, 0x47, 0x37, 0x0A, 0x4E, 0x65, 0x67, 0x66, 0x4A, 0x37, 0x2F, 0x42, 0x6F, 0x63, 0x65, 0x37, 0x53, 0x42, 0x62, 0x64, 0x56, 0x74, 0x66, 0x4D, 0x54, 0x71, 0x44, 0x68, 0x75, 0x61, 0x7A, 0x62, 0x31, 0x59, 0x4D, 0x5A, 0x47, 0x6F, 0x58, 0x52, + 0x6C, 0x4A, 0x66, 0x58, 0x79, 0x71, 0x4E, 0x6C, 0x43, 0x2F, 0x4D, 0x34, 0x2B, 0x51, 0x4B, 0x75, 0x33, 0x66, 0x5A, 0x6E, 0x7A, 0x38, 0x6B, 0x2F, 0x39, 0x59, 0x6F, 0x73, 0x52, 0x78, 0x71, 0x5A, 0x62, 0x77, 0x55, 0x4E, 0x2F, 0x64, 0x41, 0x64, + 0x0A, 0x67, 0x6A, 0x48, 0x38, 0x4B, 0x63, 0x77, 0x41, 0x57, 0x4A, 0x65, 0x52, 0x54, 0x49, 0x41, 0x41, 0x48, 0x44, 0x4F, 0x46, 0x6C, 0x69, 0x2F, 0x4C, 0x51, 0x63, 0x4B, 0x4C, 0x45, 0x49, 0x54, 0x44, 0x43, 0x53, 0x53, 0x4A, 0x48, 0x37, 0x55, + 0x50, 0x32, 0x64, 0x6C, 0x33, 0x52, 0x78, 0x69, 0x53, 0x6C, 0x47, 0x42, 0x63, 0x78, 0x35, 0x6B, 0x44, 0x50, 0x50, 0x37, 0x33, 0x6C, 0x61, 0x64, 0x39, 0x55, 0x4B, 0x47, 0x41, 0x77, 0x71, 0x6D, 0x44, 0x72, 0x56, 0x69, 0x57, 0x0A, 0x56, 0x53, + 0x48, 0x62, 0x68, 0x6C, 0x6E, 0x55, 0x72, 0x38, 0x61, 0x38, 0x33, 0x59, 0x46, 0x75, 0x42, 0x39, 0x74, 0x67, 0x59, 0x76, 0x37, 0x73, 0x45, 0x47, 0x37, 0x61, 0x61, 0x41, 0x48, 0x30, 0x67, 0x78, 0x75, 0x70, 0x50, 0x71, 0x4A, 0x62, 0x49, 0x39, + 0x64, 0x6B, 0x78, 0x74, 0x2F, 0x63, 0x6F, 0x6E, 0x33, 0x59, 0x53, 0x37, 0x71, 0x43, 0x30, 0x6C, 0x48, 0x34, 0x5A, 0x72, 0x38, 0x47, 0x52, 0x75, 0x52, 0x35, 0x4B, 0x69, 0x59, 0x32, 0x65, 0x59, 0x38, 0x66, 0x0A, 0x54, 0x70, 0x6B, 0x64, 0x73, + 0x6F, 0x38, 0x4D, 0x44, 0x68, 0x7A, 0x2F, 0x79, 0x56, 0x33, 0x41, 0x2F, 0x5A, 0x41, 0x51, 0x70, 0x72, 0x45, 0x33, 0x38, 0x38, 0x30, 0x36, 0x4A, 0x47, 0x36, 0x30, 0x68, 0x5A, 0x43, 0x2F, 0x67, 0x4C, 0x6B, 0x4D, 0x6A, 0x4E, 0x57, 0x62, 0x31, + 0x73, 0x6A, 0x78, 0x56, 0x6A, 0x38, 0x61, 0x67, 0x49, 0x6C, 0x36, 0x71, 0x65, 0x49, 0x62, 0x4D, 0x6C, 0x45, 0x73, 0x50, 0x76, 0x4C, 0x66, 0x65, 0x2F, 0x5A, 0x64, 0x65, 0x69, 0x6B, 0x5A, 0x0A, 0x6A, 0x75, 0x58, 0x49, 0x76, 0x54, 0x5A, 0x78, + 0x69, 0x31, 0x31, 0x4D, 0x77, 0x68, 0x30, 0x2F, 0x72, 0x56, 0x69, 0x69, 0x7A, 0x7A, 0x31, 0x77, 0x54, 0x61, 0x5A, 0x51, 0x6D, 0x43, 0x58, 0x63, 0x49, 0x2F, 0x6D, 0x34, 0x57, 0x45, 0x45, 0x49, 0x63, 0x62, 0x39, 0x50, 0x75, 0x49, 0x53, 0x67, + 0x6A, 0x77, 0x42, 0x55, 0x46, 0x66, 0x79, 0x52, 0x62, 0x56, 0x69, 0x6E, 0x6C, 0x6A, 0x76, 0x72, 0x53, 0x35, 0x59, 0x6E, 0x7A, 0x57, 0x75, 0x69, 0x6F, 0x59, 0x61, 0x73, 0x0A, 0x44, 0x58, 0x78, 0x55, 0x35, 0x6D, 0x5A, 0x4D, 0x5A, 0x6C, 0x2B, + 0x51, 0x76, 0x69, 0x47, 0x61, 0x41, 0x6B, 0x59, 0x74, 0x35, 0x49, 0x50, 0x43, 0x67, 0x4C, 0x6E, 0x50, 0x53, 0x7A, 0x37, 0x6F, 0x66, 0x7A, 0x77, 0x42, 0x37, 0x49, 0x39, 0x65, 0x7A, 0x58, 0x2F, 0x53, 0x4B, 0x45, 0x49, 0x42, 0x6C, 0x59, 0x72, + 0x69, 0x6C, 0x7A, 0x30, 0x51, 0x49, 0x58, 0x33, 0x32, 0x6E, 0x52, 0x7A, 0x46, 0x4E, 0x4B, 0x48, 0x73, 0x4C, 0x41, 0x34, 0x4B, 0x55, 0x69, 0x77, 0x53, 0x0A, 0x56, 0x58, 0x41, 0x6B, 0x50, 0x63, 0x76, 0x43, 0x46, 0x44, 0x56, 0x44, 0x58, 0x53, + 0x64, 0x4F, 0x76, 0x73, 0x43, 0x39, 0x71, 0x6E, 0x79, 0x57, 0x35, 0x2F, 0x79, 0x65, 0x59, 0x61, 0x31, 0x45, 0x30, 0x77, 0x43, 0x58, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, + 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x51, 0x47, 0x6D, 0x70, 0x73, 0x66, 0x55, 0x33, 0x33, 0x78, 0x39, 0x61, 0x54, 0x49, 0x0A, 0x30, 0x34, 0x59, 0x2B, 0x6F, 0x58, 0x4E, 0x5A, 0x74, 0x50, 0x64, 0x45, 0x49, 0x54, 0x41, 0x50, 0x42, + 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x41, 0x61, 0x61, 0x6D, + 0x78, 0x39, 0x54, 0x66, 0x66, 0x48, 0x31, 0x70, 0x4D, 0x6A, 0x54, 0x68, 0x6A, 0x36, 0x68, 0x63, 0x31, 0x6D, 0x30, 0x0A, 0x39, 0x30, 0x51, 0x68, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, + 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x71, 0x67, 0x56, 0x75, 0x74, 0x74, 0x30, 0x56, + 0x79, 0x62, 0x2B, 0x7A, 0x78, 0x69, 0x44, 0x32, 0x42, 0x6B, 0x65, 0x77, 0x68, 0x70, 0x4D, 0x6C, 0x0A, 0x30, 0x34, 0x32, 0x35, 0x79, 0x41, 0x41, 0x2F, 0x6C, 0x2F, 0x56, 0x53, 0x4A, 0x34, 0x68, 0x78, 0x79, 0x58, 0x54, 0x39, 0x36, 0x38, 0x70, + 0x6B, 0x32, 0x31, 0x76, 0x76, 0x48, 0x6C, 0x32, 0x36, 0x76, 0x39, 0x48, 0x72, 0x37, 0x6C, 0x78, 0x70, 0x75, 0x68, 0x62, 0x49, 0x38, 0x37, 0x6D, 0x50, 0x30, 0x7A, 0x59, 0x75, 0x51, 0x45, 0x6B, 0x48, 0x44, 0x56, 0x6E, 0x65, 0x69, 0x78, 0x43, + 0x77, 0x53, 0x51, 0x58, 0x69, 0x2F, 0x35, 0x45, 0x2F, 0x53, 0x37, 0x66, 0x64, 0x0A, 0x41, 0x6F, 0x37, 0x34, 0x67, 0x53, 0x68, 0x63, 0x7A, 0x4E, 0x78, 0x74, 0x72, 0x31, 0x38, 0x55, 0x6E, 0x48, 0x31, 0x59, 0x65, 0x41, 0x33, 0x32, 0x67, 0x41, + 0x6D, 0x35, 0x36, 0x51, 0x36, 0x58, 0x4B, 0x52, 0x6D, 0x34, 0x74, 0x2B, 0x76, 0x34, 0x46, 0x73, 0x74, 0x56, 0x45, 0x75, 0x54, 0x47, 0x66, 0x62, 0x76, 0x45, 0x37, 0x50, 0x69, 0x31, 0x48, 0x45, 0x34, 0x2B, 0x5A, 0x37, 0x2F, 0x46, 0x58, 0x78, + 0x74, 0x74, 0x62, 0x55, 0x63, 0x6F, 0x71, 0x67, 0x52, 0x59, 0x0A, 0x59, 0x64, 0x5A, 0x32, 0x76, 0x79, 0x4A, 0x2F, 0x30, 0x41, 0x64, 0x71, 0x70, 0x32, 0x52, 0x54, 0x38, 0x4A, 0x65, 0x4E, 0x6E, 0x59, 0x41, 0x2F, 0x75, 0x38, 0x45, 0x48, 0x32, + 0x32, 0x57, 0x76, 0x35, 0x70, 0x73, 0x79, 0x6D, 0x73, 0x4E, 0x55, 0x6B, 0x38, 0x51, 0x63, 0x43, 0x4D, 0x4E, 0x45, 0x2B, 0x33, 0x74, 0x6A, 0x45, 0x55, 0x50, 0x52, 0x61, 0x68, 0x70, 0x68, 0x61, 0x6E, 0x6C, 0x74, 0x6B, 0x45, 0x38, 0x70, 0x6A, + 0x6B, 0x63, 0x46, 0x77, 0x52, 0x4A, 0x70, 0x0A, 0x61, 0x64, 0x62, 0x47, 0x4E, 0x6A, 0x48, 0x68, 0x2F, 0x50, 0x71, 0x41, 0x75, 0x6C, 0x78, 0x50, 0x78, 0x4F, 0x75, 0x33, 0x4D, 0x71, 0x7A, 0x34, 0x64, 0x57, 0x45, 0x58, 0x31, 0x78, 0x41, 0x5A, + 0x75, 0x66, 0x48, 0x53, 0x43, 0x65, 0x39, 0x36, 0x51, 0x70, 0x31, 0x62, 0x57, 0x67, 0x76, 0x55, 0x78, 0x70, 0x56, 0x4F, 0x4B, 0x73, 0x37, 0x2F, 0x42, 0x39, 0x64, 0x50, 0x66, 0x68, 0x67, 0x47, 0x69, 0x50, 0x45, 0x5A, 0x74, 0x64, 0x6D, 0x59, + 0x75, 0x36, 0x35, 0x78, 0x0A, 0x78, 0x42, 0x7A, 0x6E, 0x64, 0x46, 0x6C, 0x59, 0x37, 0x77, 0x79, 0x4A, 0x7A, 0x34, 0x73, 0x66, 0x64, 0x5A, 0x4D, 0x61, 0x42, 0x42, 0x53, 0x53, 0x53, 0x46, 0x43, 0x70, 0x36, 0x31, 0x63, 0x70, 0x41, 0x42, 0x62, + 0x6A, 0x4E, 0x68, 0x7A, 0x49, 0x2B, 0x4C, 0x2F, 0x77, 0x4D, 0x39, 0x56, 0x42, 0x44, 0x38, 0x54, 0x4D, 0x50, 0x4E, 0x33, 0x70, 0x4D, 0x30, 0x4D, 0x42, 0x6B, 0x52, 0x41, 0x72, 0x48, 0x74, 0x47, 0x35, 0x58, 0x63, 0x30, 0x79, 0x47, 0x59, 0x75, + 0x50, 0x0A, 0x6A, 0x43, 0x42, 0x33, 0x31, 0x79, 0x4C, 0x45, 0x51, 0x74, 0x79, 0x45, 0x46, 0x70, 0x73, 0x6C, 0x62, 0x65, 0x69, 0x30, 0x56, 0x58, 0x46, 0x2F, 0x73, 0x48, 0x79, 0x7A, 0x30, 0x33, 0x46, 0x4A, 0x75, 0x63, 0x39, 0x53, 0x70, 0x41, + 0x51, 0x2F, 0x33, 0x44, 0x32, 0x67, 0x75, 0x36, 0x38, 0x7A, 0x6E, 0x67, 0x6F, 0x77, 0x59, 0x49, 0x37, 0x62, 0x6E, 0x56, 0x32, 0x55, 0x71, 0x4C, 0x31, 0x67, 0x35, 0x32, 0x4B, 0x41, 0x64, 0x6F, 0x47, 0x44, 0x44, 0x49, 0x7A, 0x4D, 0x0A, 0x4D, + 0x45, 0x5A, 0x4A, 0x34, 0x67, 0x7A, 0x53, 0x71, 0x4B, 0x2F, 0x72, 0x59, 0x58, 0x48, 0x76, 0x35, 0x79, 0x4A, 0x69, 0x71, 0x66, 0x64, 0x63, 0x5A, 0x47, 0x79, 0x66, 0x46, 0x6F, 0x78, 0x6E, 0x4E, 0x69, 0x64, 0x46, 0x39, 0x51, 0x6C, 0x37, 0x76, + 0x2F, 0x59, 0x51, 0x43, 0x76, 0x47, 0x77, 0x6A, 0x56, 0x52, 0x44, 0x6A, 0x41, 0x53, 0x36, 0x6F, 0x7A, 0x2F, 0x76, 0x34, 0x6A, 0x58, 0x48, 0x2B, 0x58, 0x54, 0x67, 0x62, 0x7A, 0x52, 0x42, 0x30, 0x4C, 0x39, 0x7A, 0x0A, 0x5A, 0x56, 0x63, 0x67, + 0x2B, 0x5A, 0x74, 0x6E, 0x65, 0x6D, 0x5A, 0x6F, 0x4A, 0x45, 0x36, 0x41, 0x5A, 0x62, 0x30, 0x51, 0x6D, 0x51, 0x5A, 0x5A, 0x38, 0x6D, 0x57, 0x76, 0x75, 0x4D, 0x5A, 0x48, 0x75, 0x2F, 0x32, 0x51, 0x65, 0x49, 0x74, 0x42, 0x63, 0x79, 0x36, 0x76, + 0x56, 0x52, 0x2F, 0x63, 0x4F, 0x35, 0x4A, 0x79, 0x62, 0x6F, 0x54, 0x54, 0x30, 0x47, 0x46, 0x4D, 0x44, 0x63, 0x78, 0x32, 0x56, 0x2B, 0x49, 0x74, 0x68, 0x53, 0x49, 0x56, 0x4E, 0x67, 0x33, 0x72, 0x0A, 0x41, 0x5A, 0x33, 0x72, 0x32, 0x4F, 0x76, + 0x45, 0x68, 0x4A, 0x6E, 0x37, 0x77, 0x41, 0x7A, 0x4D, 0x4D, 0x75, 0x6A, 0x6A, 0x64, 0x39, 0x71, 0x44, 0x52, 0x49, 0x75, 0x65, 0x56, 0x53, 0x6A, 0x41, 0x69, 0x31, 0x6A, 0x54, 0x6B, 0x44, 0x35, 0x4F, 0x47, 0x77, 0x44, 0x78, 0x46, 0x61, 0x32, + 0x44, 0x4B, 0x35, 0x6F, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x48, 0x41, 0x52, 0x49, 0x43, 0x41, 0x20, + 0x54, 0x4C, 0x53, 0x20, 0x52, 0x53, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x32, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, + 0x46, 0x70, 0x44, 0x43, 0x43, 0x41, 0x34, 0x79, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x4F, 0x63, 0x71, 0x54, 0x48, 0x4F, 0x39, 0x44, 0x38, 0x38, 0x61, 0x4F, 0x6B, 0x38, 0x66, 0x30, 0x5A, 0x49, 0x6B, 0x34, 0x66, 0x6A, 0x41, + 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x73, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x48, 0x55, 0x6A, + 0x45, 0x33, 0x4D, 0x44, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x75, 0x53, 0x47, 0x56, 0x73, 0x62, 0x47, 0x56, 0x75, 0x61, 0x57, 0x4D, 0x67, 0x51, 0x57, 0x4E, 0x68, 0x5A, 0x47, 0x56, 0x74, 0x61, 0x57, 0x4D, 0x67, 0x59, 0x57, + 0x35, 0x6B, 0x49, 0x46, 0x4A, 0x6C, 0x63, 0x32, 0x56, 0x68, 0x63, 0x6D, 0x4E, 0x6F, 0x49, 0x45, 0x6C, 0x75, 0x63, 0x33, 0x52, 0x70, 0x64, 0x48, 0x56, 0x30, 0x61, 0x57, 0x39, 0x75, 0x0A, 0x63, 0x79, 0x42, 0x44, 0x51, 0x54, 0x45, 0x6B, 0x4D, + 0x43, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x62, 0x53, 0x45, 0x46, 0x53, 0x53, 0x55, 0x4E, 0x42, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, + 0x53, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x78, 0x4D, 0x44, 0x49, 0x78, 0x4F, 0x54, 0x45, 0x77, 0x4E, 0x54, 0x55, 0x7A, 0x0A, 0x4F, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x31, 0x4D, 0x44, 0x49, 0x78, + 0x4D, 0x7A, 0x45, 0x77, 0x4E, 0x54, 0x55, 0x7A, 0x4E, 0x31, 0x6F, 0x77, 0x62, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x31, 0x49, 0x78, 0x4E, 0x7A, 0x41, 0x31, 0x42, 0x67, 0x4E, 0x56, + 0x42, 0x41, 0x6F, 0x4D, 0x4C, 0x6B, 0x68, 0x6C, 0x62, 0x47, 0x78, 0x6C, 0x62, 0x6D, 0x6C, 0x6A, 0x49, 0x45, 0x46, 0x6A, 0x59, 0x57, 0x52, 0x6C, 0x0A, 0x62, 0x57, 0x6C, 0x6A, 0x49, 0x47, 0x46, 0x75, 0x5A, 0x43, 0x42, 0x53, 0x5A, 0x58, 0x4E, + 0x6C, 0x59, 0x58, 0x4A, 0x6A, 0x61, 0x43, 0x42, 0x4A, 0x62, 0x6E, 0x4E, 0x30, 0x61, 0x58, 0x52, 0x31, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6E, 0x4D, 0x67, 0x51, 0x30, 0x45, 0x78, 0x4A, 0x44, 0x41, 0x69, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, + 0x4D, 0x47, 0x30, 0x68, 0x42, 0x55, 0x6B, 0x6C, 0x44, 0x51, 0x53, 0x42, 0x55, 0x54, 0x46, 0x4D, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x0A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x54, + 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, + 0x49, 0x42, 0x41, 0x49, 0x76, 0x43, 0x35, 0x36, 0x39, 0x6C, 0x6D, 0x77, 0x56, 0x6E, 0x6C, 0x73, 0x6B, 0x4E, 0x0A, 0x4A, 0x4C, 0x6E, 0x51, 0x44, 0x6D, 0x54, 0x38, 0x7A, 0x75, 0x49, 0x6B, 0x47, 0x43, 0x79, 0x45, 0x66, 0x33, 0x64, 0x52, 0x79, + 0x77, 0x51, 0x52, 0x4E, 0x72, 0x68, 0x65, 0x37, 0x57, 0x6C, 0x78, 0x70, 0x35, 0x37, 0x6B, 0x4A, 0x51, 0x6D, 0x58, 0x5A, 0x38, 0x46, 0x48, 0x77, 0x73, 0x2B, 0x52, 0x46, 0x6A, 0x5A, 0x69, 0x50, 0x54, 0x67, 0x45, 0x34, 0x56, 0x47, 0x43, 0x2F, + 0x36, 0x7A, 0x53, 0x74, 0x47, 0x6E, 0x64, 0x4C, 0x75, 0x77, 0x52, 0x6F, 0x30, 0x58, 0x75, 0x0A, 0x61, 0x32, 0x73, 0x37, 0x54, 0x4C, 0x2B, 0x4D, 0x6A, 0x61, 0x51, 0x65, 0x6E, 0x52, 0x47, 0x35, 0x36, 0x54, 0x6A, 0x35, 0x65, 0x67, 0x34, 0x4D, + 0x6D, 0x4F, 0x49, 0x6A, 0x48, 0x64, 0x46, 0x4F, 0x59, 0x39, 0x54, 0x6E, 0x75, 0x45, 0x46, 0x45, 0x2B, 0x32, 0x75, 0x76, 0x61, 0x39, 0x6F, 0x66, 0x30, 0x38, 0x57, 0x52, 0x69, 0x46, 0x75, 0x6B, 0x69, 0x5A, 0x4C, 0x52, 0x67, 0x65, 0x61, 0x4D, + 0x4F, 0x56, 0x69, 0x67, 0x31, 0x6D, 0x6C, 0x44, 0x71, 0x61, 0x32, 0x59, 0x0A, 0x55, 0x6C, 0x68, 0x75, 0x32, 0x77, 0x72, 0x37, 0x61, 0x38, 0x39, 0x6F, 0x2B, 0x75, 0x4F, 0x6B, 0x58, 0x6A, 0x70, 0x46, 0x63, 0x35, 0x67, 0x48, 0x36, 0x6C, 0x38, + 0x43, 0x63, 0x74, 0x34, 0x4D, 0x70, 0x62, 0x4F, 0x66, 0x72, 0x71, 0x6B, 0x64, 0x74, 0x78, 0x32, 0x7A, 0x2F, 0x49, 0x70, 0x5A, 0x35, 0x32, 0x35, 0x79, 0x5A, 0x61, 0x33, 0x31, 0x4D, 0x4A, 0x51, 0x6A, 0x42, 0x2F, 0x4F, 0x43, 0x46, 0x6B, 0x73, + 0x31, 0x6D, 0x4A, 0x78, 0x54, 0x75, 0x79, 0x2F, 0x4B, 0x0A, 0x35, 0x46, 0x72, 0x5A, 0x78, 0x34, 0x30, 0x64, 0x2F, 0x4A, 0x69, 0x5A, 0x2B, 0x79, 0x79, 0x6B, 0x67, 0x6D, 0x76, 0x77, 0x4B, 0x68, 0x2B, 0x4F, 0x43, 0x31, 0x39, 0x78, 0x58, 0x46, + 0x79, 0x75, 0x51, 0x6E, 0x73, 0x70, 0x69, 0x59, 0x48, 0x4C, 0x41, 0x36, 0x4F, 0x5A, 0x79, 0x6F, 0x69, 0x65, 0x43, 0x30, 0x41, 0x4A, 0x51, 0x54, 0x50, 0x62, 0x35, 0x6C, 0x68, 0x36, 0x2F, 0x61, 0x36, 0x5A, 0x63, 0x4D, 0x42, 0x61, 0x44, 0x39, + 0x59, 0x54, 0x68, 0x6E, 0x45, 0x76, 0x0A, 0x64, 0x6D, 0x6E, 0x38, 0x6B, 0x4E, 0x33, 0x62, 0x4C, 0x57, 0x37, 0x52, 0x38, 0x70, 0x76, 0x31, 0x47, 0x6D, 0x75, 0x65, 0x62, 0x78, 0x57, 0x4D, 0x65, 0x76, 0x42, 0x4C, 0x4B, 0x4B, 0x41, 0x69, 0x4F, + 0x49, 0x41, 0x6B, 0x62, 0x44, 0x61, 0x6B, 0x4F, 0x2F, 0x49, 0x77, 0x6B, 0x66, 0x4E, 0x34, 0x45, 0x38, 0x2F, 0x42, 0x50, 0x7A, 0x57, 0x72, 0x38, 0x52, 0x30, 0x52, 0x49, 0x37, 0x56, 0x44, 0x49, 0x70, 0x34, 0x42, 0x6B, 0x72, 0x63, 0x59, 0x41, + 0x75, 0x55, 0x52, 0x0A, 0x30, 0x59, 0x4C, 0x62, 0x46, 0x51, 0x44, 0x4D, 0x59, 0x54, 0x66, 0x42, 0x4B, 0x6E, 0x79, 0x61, 0x34, 0x64, 0x43, 0x36, 0x73, 0x31, 0x42, 0x47, 0x37, 0x6F, 0x4B, 0x73, 0x6E, 0x54, 0x48, 0x34, 0x2B, 0x79, 0x50, 0x69, + 0x41, 0x77, 0x42, 0x49, 0x63, 0x4B, 0x4D, 0x4A, 0x4A, 0x6E, 0x6B, 0x56, 0x55, 0x32, 0x44, 0x7A, 0x4F, 0x46, 0x79, 0x74, 0x4F, 0x4F, 0x71, 0x42, 0x41, 0x47, 0x4D, 0x55, 0x75, 0x54, 0x4E, 0x65, 0x33, 0x51, 0x76, 0x62, 0x6F, 0x45, 0x55, 0x48, + 0x0A, 0x47, 0x6A, 0x4D, 0x4A, 0x2B, 0x45, 0x32, 0x30, 0x70, 0x77, 0x4B, 0x6D, 0x61, 0x66, 0x54, 0x43, 0x57, 0x51, 0x57, 0x49, 0x5A, 0x59, 0x56, 0x57, 0x72, 0x6B, 0x76, 0x4C, 0x34, 0x4E, 0x34, 0x38, 0x66, 0x53, 0x30, 0x61, 0x79, 0x4F, 0x6E, + 0x37, 0x48, 0x36, 0x4E, 0x68, 0x53, 0x74, 0x59, 0x71, 0x45, 0x36, 0x31, 0x33, 0x54, 0x42, 0x6F, 0x59, 0x6D, 0x35, 0x45, 0x50, 0x57, 0x4E, 0x67, 0x47, 0x56, 0x4D, 0x57, 0x58, 0x2B, 0x4B, 0x6F, 0x2F, 0x49, 0x49, 0x71, 0x6D, 0x0A, 0x68, 0x61, + 0x5A, 0x33, 0x39, 0x71, 0x62, 0x38, 0x48, 0x4F, 0x4C, 0x75, 0x62, 0x70, 0x51, 0x7A, 0x4B, 0x6F, 0x4E, 0x51, 0x68, 0x41, 0x72, 0x6C, 0x54, 0x34, 0x62, 0x34, 0x55, 0x45, 0x56, 0x34, 0x41, 0x49, 0x48, 0x72, 0x57, 0x32, 0x6A, 0x6A, 0x4A, 0x6F, + 0x33, 0x4D, 0x65, 0x31, 0x78, 0x52, 0x39, 0x42, 0x51, 0x73, 0x51, 0x4C, 0x34, 0x61, 0x59, 0x42, 0x31, 0x36, 0x63, 0x6D, 0x45, 0x64, 0x48, 0x32, 0x4D, 0x74, 0x69, 0x4B, 0x72, 0x4F, 0x6F, 0x6B, 0x57, 0x51, 0x0A, 0x43, 0x50, 0x78, 0x72, 0x76, + 0x72, 0x4E, 0x51, 0x4B, 0x6C, 0x72, 0x39, 0x71, 0x45, 0x67, 0x59, 0x52, 0x74, 0x61, 0x51, 0x51, 0x4A, 0x4B, 0x51, 0x43, 0x6F, 0x52, 0x65, 0x61, 0x44, 0x48, 0x34, 0x36, 0x2B, 0x30, 0x4E, 0x30, 0x78, 0x33, 0x47, 0x66, 0x5A, 0x6B, 0x59, 0x56, + 0x56, 0x59, 0x6E, 0x5A, 0x53, 0x36, 0x4E, 0x52, 0x63, 0x55, 0x6B, 0x37, 0x4D, 0x37, 0x6A, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, + 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x41, 0x70, 0x49, 0x49, 0x36, 0x5A, 0x67, 0x70, 0x4A, 0x49, 0x4B, 0x4D, 0x2B, 0x71, 0x54, + 0x57, 0x38, 0x56, 0x58, 0x36, 0x69, 0x56, 0x4E, 0x76, 0x52, 0x4C, 0x75, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x0A, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, + 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x50, 0x70, 0x42, 0x49, 0x71, 0x6D, 0x35, 0x69, 0x46, 0x53, 0x56, 0x6D, 0x65, 0x77, 0x7A, 0x56, 0x6A, 0x49, 0x75, + 0x4A, 0x6E, 0x64, 0x66, 0x74, 0x54, 0x67, 0x66, 0x76, 0x6E, 0x4E, 0x41, 0x55, 0x58, 0x31, 0x35, 0x51, 0x76, 0x57, 0x69, 0x57, 0x6B, 0x4B, 0x51, 0x55, 0x0A, 0x45, 0x61, 0x70, 0x6F, 0x62, 0x51, 0x6B, 0x31, 0x4F, 0x55, 0x41, 0x4A, 0x32, 0x76, + 0x51, 0x4A, 0x4C, 0x44, 0x53, 0x6C, 0x65, 0x31, 0x6D, 0x45, 0x53, 0x53, 0x6D, 0x58, 0x64, 0x4D, 0x67, 0x48, 0x48, 0x6B, 0x64, 0x74, 0x38, 0x73, 0x34, 0x63, 0x55, 0x43, 0x62, 0x6A, 0x6E, 0x6A, 0x31, 0x41, 0x55, 0x7A, 0x2F, 0x33, 0x66, 0x35, + 0x5A, 0x32, 0x45, 0x4D, 0x56, 0x47, 0x70, 0x64, 0x41, 0x67, 0x53, 0x31, 0x44, 0x30, 0x4E, 0x54, 0x73, 0x59, 0x39, 0x46, 0x56, 0x71, 0x0A, 0x51, 0x52, 0x74, 0x48, 0x42, 0x6D, 0x67, 0x38, 0x75, 0x77, 0x6B, 0x49, 0x59, 0x74, 0x6C, 0x66, 0x56, + 0x55, 0x4B, 0x71, 0x72, 0x46, 0x4F, 0x46, 0x72, 0x4A, 0x56, 0x57, 0x4E, 0x6C, 0x61, 0x72, 0x35, 0x41, 0x57, 0x4D, 0x78, 0x61, 0x6A, 0x61, 0x48, 0x36, 0x4E, 0x70, 0x76, 0x56, 0x4D, 0x50, 0x78, 0x50, 0x2F, 0x63, 0x79, 0x75, 0x4E, 0x2B, 0x38, + 0x6B, 0x79, 0x49, 0x68, 0x6B, 0x64, 0x47, 0x47, 0x76, 0x4D, 0x41, 0x39, 0x59, 0x43, 0x52, 0x6F, 0x74, 0x78, 0x44, 0x0A, 0x51, 0x70, 0x53, 0x62, 0x49, 0x50, 0x44, 0x52, 0x7A, 0x62, 0x4C, 0x72, 0x4C, 0x46, 0x50, 0x43, 0x55, 0x33, 0x68, 0x4B, + 0x54, 0x77, 0x53, 0x55, 0x51, 0x5A, 0x71, 0x50, 0x4A, 0x7A, 0x4C, 0x42, 0x35, 0x55, 0x6B, 0x5A, 0x76, 0x2F, 0x48, 0x79, 0x77, 0x6F, 0x75, 0x6F, 0x43, 0x6A, 0x6B, 0x78, 0x4B, 0x4C, 0x52, 0x39, 0x59, 0x6A, 0x59, 0x73, 0x54, 0x65, 0x77, 0x66, + 0x4D, 0x37, 0x5A, 0x2B, 0x64, 0x32, 0x31, 0x2B, 0x55, 0x50, 0x43, 0x66, 0x44, 0x74, 0x63, 0x52, 0x0A, 0x6A, 0x38, 0x38, 0x59, 0x78, 0x65, 0x4D, 0x6E, 0x2F, 0x69, 0x62, 0x76, 0x42, 0x5A, 0x33, 0x50, 0x7A, 0x7A, 0x66, 0x46, 0x30, 0x48, 0x76, + 0x61, 0x4F, 0x37, 0x41, 0x57, 0x68, 0x41, 0x77, 0x36, 0x6B, 0x39, 0x61, 0x2B, 0x46, 0x39, 0x73, 0x50, 0x50, 0x67, 0x34, 0x5A, 0x65, 0x41, 0x6E, 0x48, 0x71, 0x51, 0x4A, 0x79, 0x49, 0x6B, 0x76, 0x33, 0x4E, 0x33, 0x61, 0x36, 0x64, 0x63, 0x53, + 0x46, 0x41, 0x31, 0x70, 0x6A, 0x31, 0x62, 0x46, 0x31, 0x42, 0x63, 0x4B, 0x35, 0x0A, 0x76, 0x5A, 0x53, 0x74, 0x6A, 0x42, 0x57, 0x5A, 0x70, 0x35, 0x4E, 0x39, 0x39, 0x73, 0x58, 0x7A, 0x71, 0x6E, 0x54, 0x50, 0x42, 0x49, 0x57, 0x55, 0x6D, 0x41, + 0x44, 0x30, 0x34, 0x76, 0x6E, 0x4B, 0x4A, 0x47, 0x57, 0x2F, 0x34, 0x47, 0x4B, 0x76, 0x79, 0x4D, 0x58, 0x36, 0x73, 0x73, 0x6D, 0x65, 0x56, 0x6B, 0x6A, 0x61, 0x65, 0x66, 0x32, 0x57, 0x64, 0x68, 0x57, 0x2B, 0x6F, 0x34, 0x35, 0x57, 0x78, 0x4C, + 0x4D, 0x30, 0x2F, 0x4C, 0x35, 0x48, 0x39, 0x4D, 0x47, 0x30, 0x0A, 0x71, 0x50, 0x7A, 0x56, 0x4D, 0x49, 0x68, 0x6F, 0x37, 0x73, 0x75, 0x75, 0x79, 0x57, 0x50, 0x45, 0x64, 0x72, 0x36, 0x73, 0x4F, 0x42, 0x6A, 0x68, 0x58, 0x6C, 0x7A, 0x50, 0x72, + 0x6A, 0x6F, 0x69, 0x55, 0x65, 0x76, 0x52, 0x69, 0x37, 0x50, 0x7A, 0x4B, 0x7A, 0x4D, 0x48, 0x56, 0x49, 0x66, 0x36, 0x74, 0x4C, 0x49, 0x54, 0x65, 0x37, 0x70, 0x54, 0x42, 0x47, 0x49, 0x42, 0x6E, 0x66, 0x48, 0x41, 0x54, 0x2B, 0x37, 0x68, 0x4F, + 0x74, 0x53, 0x4C, 0x49, 0x42, 0x44, 0x36, 0x0A, 0x41, 0x6C, 0x66, 0x6D, 0x37, 0x38, 0x45, 0x4C, 0x74, 0x35, 0x42, 0x47, 0x6E, 0x42, 0x6B, 0x70, 0x6A, 0x4E, 0x78, 0x76, 0x6F, 0x45, 0x70, 0x70, 0x61, 0x5A, 0x53, 0x33, 0x4A, 0x47, 0x57, 0x67, + 0x2F, 0x36, 0x77, 0x2F, 0x7A, 0x67, 0x48, 0x37, 0x49, 0x53, 0x37, 0x39, 0x61, 0x50, 0x69, 0x62, 0x38, 0x71, 0x58, 0x50, 0x4D, 0x54, 0x68, 0x63, 0x46, 0x61, 0x72, 0x6D, 0x6C, 0x77, 0x44, 0x42, 0x33, 0x31, 0x71, 0x6C, 0x70, 0x7A, 0x6D, 0x71, + 0x36, 0x59, 0x52, 0x2F, 0x0A, 0x50, 0x46, 0x47, 0x6F, 0x4F, 0x74, 0x6D, 0x55, 0x57, 0x34, 0x79, 0x2F, 0x54, 0x77, 0x68, 0x78, 0x35, 0x64, 0x75, 0x6F, 0x58, 0x4E, 0x54, 0x53, 0x70, 0x76, 0x34, 0x41, 0x6F, 0x38, 0x59, 0x57, 0x78, 0x77, 0x2F, + 0x6F, 0x67, 0x4D, 0x34, 0x63, 0x4B, 0x47, 0x52, 0x30, 0x47, 0x51, 0x6A, 0x54, 0x51, 0x75, 0x50, 0x4F, 0x41, 0x46, 0x31, 0x2F, 0x73, 0x64, 0x77, 0x54, 0x73, 0x4F, 0x45, 0x46, 0x79, 0x39, 0x45, 0x67, 0x71, 0x6F, 0x5A, 0x30, 0x6E, 0x6A, 0x6E, + 0x6E, 0x0A, 0x6B, 0x66, 0x33, 0x2F, 0x57, 0x39, 0x62, 0x33, 0x72, 0x61, 0x59, 0x76, 0x41, 0x77, 0x74, 0x74, 0x34, 0x31, 0x64, 0x55, 0x36, 0x33, 0x5A, 0x54, 0x47, 0x49, 0x30, 0x52, 0x6D, 0x4C, 0x6F, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x48, 0x41, 0x52, 0x49, 0x43, 0x41, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, + 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x32, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x56, 0x44, 0x43, 0x43, 0x41, 0x64, 0x75, 0x67, 0x41, 0x77, + 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x5A, 0x33, 0x53, 0x64, 0x6A, 0x58, 0x66, 0x59, 0x4F, 0x32, 0x72, 0x62, 0x49, 0x76, 0x54, 0x2F, 0x57, 0x65, 0x4B, 0x2F, 0x7A, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, + 0x51, 0x44, 0x41, 0x7A, 0x42, 0x73, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x48, 0x0A, 0x55, 0x6A, 0x45, 0x33, 0x4D, 0x44, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x75, 0x53, + 0x47, 0x56, 0x73, 0x62, 0x47, 0x56, 0x75, 0x61, 0x57, 0x4D, 0x67, 0x51, 0x57, 0x4E, 0x68, 0x5A, 0x47, 0x56, 0x74, 0x61, 0x57, 0x4D, 0x67, 0x59, 0x57, 0x35, 0x6B, 0x49, 0x46, 0x4A, 0x6C, 0x63, 0x32, 0x56, 0x68, 0x63, 0x6D, 0x4E, 0x6F, 0x49, + 0x45, 0x6C, 0x75, 0x63, 0x33, 0x52, 0x70, 0x64, 0x48, 0x56, 0x30, 0x61, 0x57, 0x39, 0x75, 0x63, 0x79, 0x42, 0x44, 0x0A, 0x51, 0x54, 0x45, 0x6B, 0x4D, 0x43, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x62, 0x53, 0x45, 0x46, 0x53, + 0x53, 0x55, 0x4E, 0x42, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x78, + 0x4D, 0x44, 0x49, 0x78, 0x4F, 0x54, 0x45, 0x78, 0x4D, 0x44, 0x45, 0x78, 0x4D, 0x46, 0x6F, 0x58, 0x0A, 0x44, 0x54, 0x51, 0x31, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x7A, 0x45, 0x78, 0x4D, 0x44, 0x45, 0x77, 0x4F, 0x56, 0x6F, 0x77, 0x62, 0x44, 0x45, + 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x31, 0x49, 0x78, 0x4E, 0x7A, 0x41, 0x31, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x4C, 0x6B, 0x68, 0x6C, 0x62, 0x47, 0x78, 0x6C, 0x62, 0x6D, 0x6C, + 0x6A, 0x49, 0x45, 0x46, 0x6A, 0x59, 0x57, 0x52, 0x6C, 0x62, 0x57, 0x6C, 0x6A, 0x0A, 0x49, 0x47, 0x46, 0x75, 0x5A, 0x43, 0x42, 0x53, 0x5A, 0x58, 0x4E, 0x6C, 0x59, 0x58, 0x4A, 0x6A, 0x61, 0x43, 0x42, 0x4A, 0x62, 0x6E, 0x4E, 0x30, 0x61, 0x58, + 0x52, 0x31, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6E, 0x4D, 0x67, 0x51, 0x30, 0x45, 0x78, 0x4A, 0x44, 0x41, 0x69, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x47, 0x30, 0x68, 0x42, 0x55, 0x6B, 0x6C, 0x44, 0x51, 0x53, 0x42, 0x55, 0x54, 0x46, + 0x4D, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x0A, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, + 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x44, 0x67, 0x49, 0x2F, 0x72, 0x47, 0x67, 0x6C, 0x74, 0x4A, 0x36, 0x72, 0x4B, 0x39, 0x4A, 0x4F, 0x74, 0x44, 0x41, 0x34, 0x4D, 0x4D, 0x37, 0x4B, + 0x4B, 0x72, 0x78, 0x63, 0x6D, 0x31, 0x6C, 0x0A, 0x41, 0x45, 0x65, 0x49, 0x68, 0x50, 0x79, 0x61, 0x4A, 0x6D, 0x75, 0x71, 0x53, 0x37, 0x70, 0x73, 0x42, 0x41, 0x71, 0x49, 0x58, 0x68, 0x66, 0x79, 0x56, 0x59, 0x66, 0x38, 0x4D, 0x4C, 0x41, 0x30, + 0x34, 0x6A, 0x52, 0x59, 0x56, 0x78, 0x71, 0x45, 0x55, 0x2B, 0x6B, 0x77, 0x32, 0x61, 0x6E, 0x79, 0x6C, 0x6E, 0x54, 0x44, 0x55, 0x52, 0x39, 0x59, 0x53, 0x54, 0x48, 0x4D, 0x6D, 0x45, 0x35, 0x67, 0x45, 0x59, 0x64, 0x31, 0x30, 0x33, 0x4B, 0x55, + 0x6B, 0x45, 0x2B, 0x62, 0x0A, 0x45, 0x43, 0x55, 0x71, 0x71, 0x48, 0x67, 0x74, 0x76, 0x70, 0x42, 0x42, 0x57, 0x4A, 0x41, 0x56, 0x63, 0x71, 0x65, 0x68, 0x74, 0x36, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, + 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x79, 0x52, 0x74, 0x54, 0x67, 0x52, 0x4C, 0x2B, 0x42, 0x4E, 0x55, + 0x57, 0x0A, 0x30, 0x61, 0x71, 0x38, 0x6D, 0x6D, 0x2B, 0x33, 0x6F, 0x4A, 0x55, 0x5A, 0x62, 0x73, 0x6F, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, + 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x63, 0x41, 0x4D, 0x47, 0x51, 0x43, 0x4D, 0x42, 0x48, 0x65, 0x72, 0x76, 0x6A, 0x63, 0x54, 0x6F, 0x69, 0x77, 0x71, 0x66, 0x41, 0x69, 0x0A, 0x72, + 0x63, 0x4A, 0x52, 0x51, 0x4F, 0x39, 0x67, 0x63, 0x53, 0x33, 0x75, 0x6A, 0x77, 0x4C, 0x45, 0x58, 0x51, 0x4E, 0x77, 0x53, 0x61, 0x53, 0x53, 0x36, 0x73, 0x55, 0x55, 0x69, 0x48, 0x43, 0x6D, 0x30, 0x77, 0x32, 0x77, 0x71, 0x73, 0x6F, 0x73, 0x51, + 0x4A, 0x7A, 0x37, 0x36, 0x59, 0x4A, 0x75, 0x6D, 0x67, 0x49, 0x77, 0x4B, 0x30, 0x65, 0x61, 0x42, 0x38, 0x62, 0x52, 0x77, 0x6F, 0x46, 0x38, 0x79, 0x67, 0x75, 0x57, 0x47, 0x45, 0x45, 0x62, 0x6F, 0x2F, 0x51, 0x77, 0x0A, 0x43, 0x5A, 0x36, 0x31, + 0x49, 0x79, 0x67, 0x4E, 0x6E, 0x78, 0x53, 0x32, 0x50, 0x46, 0x4F, 0x69, 0x54, 0x41, 0x5A, 0x70, 0x66, 0x66, 0x70, 0x73, 0x6B, 0x63, 0x59, 0x71, 0x53, 0x55, 0x58, 0x6D, 0x37, 0x4C, 0x63, 0x54, 0x34, 0x54, 0x70, 0x73, 0x0A, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x75, 0x74, 0x6F, 0x72, 0x69, 0x64, 0x61, 0x64, 0x20, 0x64, 0x65, 0x20, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x63, 0x69, 0x6F, 0x6E, 0x20, 0x46, 0x69, 0x72, 0x6D, 0x61, 0x70, 0x72, 0x6F, 0x66, 0x65, 0x73, 0x69, 0x6F, 0x6E, 0x61, 0x6C, 0x20, 0x43, 0x49, 0x46, 0x20, 0x41, 0x36, 0x32, 0x36, 0x33, 0x34, 0x30, 0x36, + 0x38, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x47, 0x46, 0x44, 0x43, 0x43, 0x41, 0x2F, 0x79, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x49, 0x47, 0x33, 0x44, 0x70, 0x30, 0x76, 0x2B, 0x75, 0x62, 0x48, 0x45, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x55, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x56, 0x4D, 0x78, + 0x51, 0x6A, 0x42, 0x41, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x4F, 0x55, 0x46, 0x31, 0x64, 0x47, 0x39, 0x79, 0x61, 0x57, 0x52, 0x68, 0x5A, 0x43, 0x42, 0x6B, 0x5A, 0x53, 0x42, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x61, 0x57, 0x5A, + 0x70, 0x59, 0x32, 0x46, 0x6A, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x5A, 0x70, 0x63, 0x6D, 0x31, 0x68, 0x63, 0x48, 0x4A, 0x76, 0x5A, 0x6D, 0x56, 0x7A, 0x61, 0x57, 0x39, 0x75, 0x59, 0x57, 0x77, 0x67, 0x51, 0x30, 0x6C, 0x47, 0x49, 0x45, 0x45, + 0x32, 0x0A, 0x4D, 0x6A, 0x59, 0x7A, 0x4E, 0x44, 0x41, 0x32, 0x4F, 0x44, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4E, 0x44, 0x41, 0x35, 0x4D, 0x6A, 0x4D, 0x78, 0x4E, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x64, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4E, 0x6A, + 0x41, 0x31, 0x4D, 0x44, 0x55, 0x78, 0x4E, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x64, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x56, 0x54, 0x4D, 0x55, 0x49, 0x77, 0x0A, 0x51, + 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x44, 0x6C, 0x42, 0x64, 0x58, 0x52, 0x76, 0x63, 0x6D, 0x6C, 0x6B, 0x59, 0x57, 0x51, 0x67, 0x5A, 0x47, 0x55, 0x67, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x59, + 0x32, 0x6C, 0x76, 0x62, 0x69, 0x42, 0x47, 0x61, 0x58, 0x4A, 0x74, 0x59, 0x58, 0x42, 0x79, 0x62, 0x32, 0x5A, 0x6C, 0x63, 0x32, 0x6C, 0x76, 0x62, 0x6D, 0x46, 0x73, 0x49, 0x45, 0x4E, 0x4A, 0x52, 0x69, 0x42, 0x42, 0x0A, 0x4E, 0x6A, 0x49, 0x32, + 0x4D, 0x7A, 0x51, 0x77, 0x4E, 0x6A, 0x67, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, + 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x4B, 0x6C, 0x6D, 0x75, 0x4F, 0x36, 0x76, 0x6A, 0x37, 0x38, 0x61, 0x49, 0x31, 0x34, 0x48, 0x39, 0x4D, 0x32, 0x75, 0x44, 0x44, 0x0A, 0x55, 0x74, 0x64, 0x39, 0x74, 0x68, 0x44, + 0x49, 0x41, 0x6C, 0x36, 0x7A, 0x51, 0x79, 0x72, 0x45, 0x54, 0x32, 0x71, 0x79, 0x79, 0x68, 0x78, 0x64, 0x4B, 0x4A, 0x70, 0x34, 0x45, 0x52, 0x70, 0x70, 0x57, 0x56, 0x65, 0x76, 0x74, 0x53, 0x42, 0x43, 0x35, 0x49, 0x73, 0x50, 0x35, 0x74, 0x39, + 0x62, 0x70, 0x67, 0x4F, 0x53, 0x4C, 0x2F, 0x55, 0x52, 0x35, 0x47, 0x4C, 0x58, 0x4D, 0x6E, 0x45, 0x34, 0x32, 0x51, 0x51, 0x4D, 0x63, 0x61, 0x73, 0x39, 0x55, 0x58, 0x34, 0x50, 0x0A, 0x42, 0x39, 0x39, 0x6A, 0x42, 0x56, 0x7A, 0x70, 0x76, 0x35, + 0x52, 0x76, 0x77, 0x53, 0x6D, 0x43, 0x77, 0x4C, 0x54, 0x61, 0x55, 0x62, 0x44, 0x42, 0x50, 0x4C, 0x75, 0x74, 0x4E, 0x30, 0x70, 0x63, 0x79, 0x76, 0x46, 0x4C, 0x4E, 0x67, 0x34, 0x6B, 0x71, 0x37, 0x2F, 0x44, 0x68, 0x48, 0x66, 0x39, 0x71, 0x46, + 0x44, 0x30, 0x73, 0x65, 0x66, 0x47, 0x4C, 0x39, 0x49, 0x74, 0x57, 0x59, 0x31, 0x36, 0x43, 0x6B, 0x36, 0x57, 0x61, 0x56, 0x49, 0x43, 0x71, 0x6A, 0x61, 0x59, 0x0A, 0x37, 0x50, 0x7A, 0x36, 0x46, 0x49, 0x4D, 0x4D, 0x4E, 0x78, 0x2F, 0x4A, 0x6B, + 0x6A, 0x64, 0x2F, 0x31, 0x34, 0x45, 0x74, 0x35, 0x63, 0x53, 0x35, 0x34, 0x44, 0x34, 0x30, 0x2F, 0x6D, 0x66, 0x30, 0x50, 0x6D, 0x62, 0x52, 0x30, 0x2F, 0x52, 0x41, 0x7A, 0x31, 0x35, 0x69, 0x4E, 0x41, 0x39, 0x77, 0x42, 0x6A, 0x34, 0x67, 0x47, + 0x46, 0x72, 0x4F, 0x39, 0x33, 0x49, 0x62, 0x4A, 0x57, 0x79, 0x54, 0x64, 0x42, 0x53, 0x54, 0x6F, 0x33, 0x4F, 0x78, 0x44, 0x71, 0x71, 0x48, 0x0A, 0x45, 0x43, 0x4E, 0x5A, 0x58, 0x79, 0x41, 0x46, 0x47, 0x55, 0x66, 0x74, 0x61, 0x49, 0x36, 0x53, + 0x45, 0x73, 0x70, 0x64, 0x2F, 0x4E, 0x59, 0x72, 0x73, 0x70, 0x49, 0x38, 0x49, 0x4D, 0x2F, 0x68, 0x58, 0x36, 0x38, 0x67, 0x76, 0x71, 0x42, 0x32, 0x66, 0x33, 0x62, 0x6C, 0x37, 0x42, 0x71, 0x47, 0x59, 0x54, 0x4D, 0x2B, 0x35, 0x33, 0x75, 0x30, + 0x50, 0x36, 0x41, 0x50, 0x6A, 0x71, 0x4B, 0x35, 0x61, 0x6D, 0x2B, 0x35, 0x68, 0x79, 0x5A, 0x76, 0x51, 0x57, 0x79, 0x49, 0x0A, 0x70, 0x6C, 0x44, 0x39, 0x61, 0x6D, 0x4D, 0x4C, 0x39, 0x5A, 0x4D, 0x57, 0x47, 0x78, 0x6D, 0x50, 0x73, 0x75, 0x32, + 0x62, 0x6D, 0x38, 0x6D, 0x51, 0x39, 0x51, 0x45, 0x4D, 0x33, 0x78, 0x6B, 0x39, 0x44, 0x7A, 0x34, 0x34, 0x49, 0x38, 0x6B, 0x76, 0x6A, 0x77, 0x7A, 0x52, 0x41, 0x76, 0x34, 0x62, 0x56, 0x64, 0x5A, 0x4F, 0x30, 0x49, 0x30, 0x38, 0x72, 0x30, 0x2B, + 0x6B, 0x38, 0x2F, 0x36, 0x76, 0x4B, 0x74, 0x4D, 0x46, 0x6E, 0x58, 0x6B, 0x49, 0x6F, 0x63, 0x74, 0x58, 0x0A, 0x4D, 0x62, 0x53, 0x63, 0x79, 0x4A, 0x43, 0x79, 0x5A, 0x2F, 0x51, 0x59, 0x46, 0x70, 0x4D, 0x36, 0x2F, 0x45, 0x66, 0x59, 0x30, 0x58, + 0x69, 0x57, 0x4D, 0x52, 0x2B, 0x36, 0x4B, 0x77, 0x78, 0x66, 0x58, 0x5A, 0x6D, 0x74, 0x59, 0x34, 0x6C, 0x61, 0x4A, 0x43, 0x42, 0x32, 0x32, 0x4E, 0x2F, 0x39, 0x71, 0x30, 0x36, 0x6D, 0x49, 0x71, 0x71, 0x64, 0x58, 0x75, 0x59, 0x6E, 0x69, 0x6E, + 0x31, 0x6F, 0x4B, 0x61, 0x50, 0x6E, 0x69, 0x72, 0x6A, 0x61, 0x45, 0x62, 0x73, 0x58, 0x0A, 0x4C, 0x5A, 0x6D, 0x64, 0x45, 0x79, 0x52, 0x47, 0x39, 0x38, 0x58, 0x69, 0x32, 0x4A, 0x2B, 0x4F, 0x66, 0x38, 0x65, 0x50, 0x64, 0x47, 0x31, 0x61, 0x73, + 0x75, 0x68, 0x79, 0x39, 0x61, 0x7A, 0x75, 0x4A, 0x42, 0x43, 0x74, 0x4C, 0x78, 0x54, 0x61, 0x2F, 0x79, 0x32, 0x61, 0x52, 0x6E, 0x46, 0x48, 0x76, 0x6B, 0x4C, 0x66, 0x75, 0x77, 0x48, 0x62, 0x39, 0x48, 0x2F, 0x54, 0x4B, 0x49, 0x38, 0x78, 0x57, + 0x56, 0x76, 0x54, 0x79, 0x51, 0x4B, 0x6D, 0x74, 0x46, 0x4C, 0x4B, 0x0A, 0x62, 0x70, 0x66, 0x37, 0x51, 0x38, 0x55, 0x49, 0x4A, 0x6D, 0x2B, 0x4B, 0x39, 0x4C, 0x76, 0x39, 0x6E, 0x79, 0x69, 0x71, 0x44, 0x64, 0x56, 0x46, 0x38, 0x78, 0x4D, 0x36, + 0x48, 0x64, 0x6A, 0x41, 0x65, 0x49, 0x39, 0x42, 0x5A, 0x7A, 0x77, 0x65, 0x6C, 0x47, 0x53, 0x75, 0x65, 0x77, 0x76, 0x46, 0x36, 0x4E, 0x6B, 0x42, 0x69, 0x44, 0x6B, 0x61, 0x6C, 0x34, 0x5A, 0x6B, 0x51, 0x64, 0x55, 0x37, 0x68, 0x77, 0x78, 0x75, + 0x2B, 0x67, 0x2F, 0x47, 0x76, 0x55, 0x67, 0x55, 0x0A, 0x76, 0x7A, 0x6C, 0x4E, 0x31, 0x4A, 0x35, 0x42, 0x74, 0x6F, 0x2B, 0x57, 0x48, 0x57, 0x4F, 0x57, 0x6B, 0x39, 0x6D, 0x56, 0x42, 0x6E, 0x67, 0x78, 0x61, 0x4A, 0x34, 0x33, 0x42, 0x6A, 0x75, + 0x41, 0x69, 0x55, 0x56, 0x68, 0x4F, 0x53, 0x50, 0x48, 0x47, 0x30, 0x53, 0x6A, 0x46, 0x65, 0x55, 0x63, 0x2B, 0x4A, 0x49, 0x77, 0x75, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x34, 0x48, 0x76, 0x4D, 0x49, 0x48, 0x73, 0x4D, 0x42, 0x30, + 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x6C, 0x7A, 0x65, 0x75, 0x72, 0x4E, 0x52, 0x34, 0x41, 0x50, 0x6E, 0x37, 0x56, 0x64, 0x4D, 0x41, 0x63, 0x74, 0x48, 0x4E, 0x48, 0x44, 0x68, 0x70, 0x6B, 0x4C, 0x7A, + 0x41, 0x53, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x43, 0x44, 0x41, 0x47, 0x41, 0x51, 0x48, 0x2F, 0x41, 0x67, 0x45, 0x42, 0x4D, 0x49, 0x47, 0x6D, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x41, 0x45, 0x67, 0x5A, + 0x34, 0x77, 0x0A, 0x67, 0x5A, 0x73, 0x77, 0x67, 0x5A, 0x67, 0x47, 0x42, 0x46, 0x55, 0x64, 0x49, 0x41, 0x41, 0x77, 0x67, 0x59, 0x38, 0x77, 0x4C, 0x77, 0x59, 0x49, 0x4B, 0x77, 0x59, 0x42, 0x42, 0x51, 0x55, 0x48, 0x41, 0x67, 0x45, 0x57, 0x49, + 0x32, 0x68, 0x30, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x33, 0x64, 0x33, 0x63, 0x75, 0x5A, 0x6D, 0x6C, 0x79, 0x62, 0x57, 0x46, 0x77, 0x63, 0x6D, 0x39, 0x6D, 0x5A, 0x58, 0x4E, 0x70, 0x62, 0x32, 0x35, 0x68, 0x62, 0x43, 0x35, 0x6A, 0x0A, + 0x62, 0x32, 0x30, 0x76, 0x59, 0x33, 0x42, 0x7A, 0x4D, 0x46, 0x77, 0x47, 0x43, 0x43, 0x73, 0x47, 0x41, 0x51, 0x55, 0x46, 0x42, 0x77, 0x49, 0x43, 0x4D, 0x46, 0x41, 0x65, 0x54, 0x67, 0x42, 0x51, 0x41, 0x47, 0x45, 0x41, 0x63, 0x77, 0x42, 0x6C, + 0x41, 0x47, 0x38, 0x41, 0x49, 0x41, 0x42, 0x6B, 0x41, 0x47, 0x55, 0x41, 0x49, 0x41, 0x42, 0x73, 0x41, 0x47, 0x45, 0x41, 0x49, 0x41, 0x42, 0x43, 0x41, 0x47, 0x38, 0x41, 0x62, 0x67, 0x42, 0x68, 0x41, 0x47, 0x34, 0x41, 0x0A, 0x62, 0x77, 0x42, + 0x32, 0x41, 0x47, 0x45, 0x41, 0x49, 0x41, 0x41, 0x30, 0x41, 0x44, 0x63, 0x41, 0x49, 0x41, 0x42, 0x43, 0x41, 0x47, 0x45, 0x41, 0x63, 0x67, 0x42, 0x6A, 0x41, 0x47, 0x55, 0x41, 0x62, 0x41, 0x42, 0x76, 0x41, 0x47, 0x34, 0x41, 0x59, 0x51, 0x41, + 0x67, 0x41, 0x44, 0x41, 0x41, 0x4F, 0x41, 0x41, 0x77, 0x41, 0x44, 0x45, 0x41, 0x4E, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x0A, 0x41, 0x51, 0x59, 0x77, 0x44, 0x51, + 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x48, 0x53, 0x48, 0x4B, 0x41, 0x49, 0x72, 0x64, 0x78, 0x39, 0x6D, 0x69, 0x57, 0x54, 0x74, 0x6A, 0x33, + 0x51, 0x75, 0x52, 0x68, 0x79, 0x37, 0x71, 0x50, 0x6A, 0x34, 0x43, 0x78, 0x32, 0x44, 0x74, 0x6A, 0x71, 0x6E, 0x36, 0x45, 0x57, 0x4B, 0x42, 0x37, 0x66, 0x67, 0x50, 0x69, 0x44, 0x4C, 0x0A, 0x34, 0x51, 0x6A, 0x62, 0x45, 0x77, 0x6A, 0x34, 0x4B, + 0x4B, 0x45, 0x31, 0x73, 0x6F, 0x43, 0x7A, 0x43, 0x31, 0x48, 0x41, 0x30, 0x31, 0x61, 0x61, 0x6A, 0x54, 0x4E, 0x46, 0x53, 0x61, 0x39, 0x4A, 0x38, 0x4F, 0x41, 0x39, 0x42, 0x33, 0x70, 0x46, 0x45, 0x31, 0x72, 0x2F, 0x79, 0x4A, 0x66, 0x59, 0x30, + 0x78, 0x67, 0x73, 0x66, 0x5A, 0x62, 0x34, 0x33, 0x61, 0x4A, 0x6C, 0x51, 0x33, 0x43, 0x54, 0x6B, 0x42, 0x57, 0x36, 0x6B, 0x4E, 0x2F, 0x6F, 0x47, 0x62, 0x44, 0x62, 0x0A, 0x4C, 0x49, 0x70, 0x67, 0x44, 0x37, 0x64, 0x76, 0x6C, 0x41, 0x63, 0x65, + 0x48, 0x61, 0x62, 0x4A, 0x68, 0x66, 0x61, 0x39, 0x4E, 0x50, 0x68, 0x41, 0x65, 0x47, 0x49, 0x51, 0x63, 0x44, 0x71, 0x2B, 0x66, 0x55, 0x73, 0x35, 0x67, 0x61, 0x6B, 0x51, 0x31, 0x4A, 0x5A, 0x42, 0x75, 0x2F, 0x68, 0x66, 0x48, 0x41, 0x73, 0x64, + 0x43, 0x50, 0x4B, 0x78, 0x73, 0x49, 0x6C, 0x36, 0x38, 0x76, 0x65, 0x67, 0x34, 0x4D, 0x53, 0x50, 0x69, 0x33, 0x69, 0x31, 0x4F, 0x31, 0x69, 0x6C, 0x0A, 0x49, 0x34, 0x35, 0x50, 0x56, 0x66, 0x34, 0x32, 0x4F, 0x2B, 0x41, 0x4D, 0x74, 0x38, 0x6F, + 0x71, 0x4D, 0x45, 0x45, 0x67, 0x74, 0x49, 0x44, 0x4E, 0x72, 0x76, 0x78, 0x32, 0x5A, 0x6E, 0x4F, 0x6F, 0x72, 0x6D, 0x37, 0x68, 0x66, 0x4E, 0x6F, 0x44, 0x36, 0x4A, 0x51, 0x67, 0x35, 0x69, 0x4B, 0x6A, 0x30, 0x42, 0x2B, 0x51, 0x58, 0x53, 0x42, + 0x54, 0x46, 0x43, 0x5A, 0x58, 0x32, 0x6C, 0x53, 0x58, 0x33, 0x78, 0x5A, 0x45, 0x45, 0x41, 0x45, 0x65, 0x69, 0x47, 0x61, 0x50, 0x0A, 0x63, 0x6A, 0x69, 0x54, 0x33, 0x53, 0x43, 0x33, 0x4E, 0x4C, 0x37, 0x58, 0x38, 0x65, 0x35, 0x6A, 0x6A, 0x6B, + 0x64, 0x35, 0x4B, 0x41, 0x62, 0x38, 0x38, 0x31, 0x6C, 0x46, 0x4A, 0x57, 0x41, 0x69, 0x4D, 0x78, 0x75, 0x6A, 0x58, 0x36, 0x69, 0x36, 0x4B, 0x74, 0x6F, 0x61, 0x50, 0x63, 0x31, 0x41, 0x36, 0x6F, 0x7A, 0x75, 0x42, 0x52, 0x57, 0x56, 0x31, 0x61, + 0x55, 0x73, 0x49, 0x43, 0x2B, 0x6E, 0x6D, 0x43, 0x6A, 0x75, 0x52, 0x66, 0x7A, 0x78, 0x75, 0x49, 0x67, 0x41, 0x0A, 0x4C, 0x49, 0x39, 0x43, 0x32, 0x6C, 0x48, 0x56, 0x6E, 0x4F, 0x55, 0x54, 0x61, 0x48, 0x46, 0x46, 0x51, 0x34, 0x75, 0x65, 0x43, + 0x79, 0x45, 0x38, 0x53, 0x31, 0x77, 0x46, 0x33, 0x42, 0x71, 0x66, 0x6D, 0x49, 0x37, 0x61, 0x76, 0x53, 0x4B, 0x65, 0x63, 0x73, 0x32, 0x74, 0x43, 0x73, 0x76, 0x4D, 0x6F, 0x32, 0x65, 0x62, 0x4B, 0x48, 0x54, 0x45, 0x6D, 0x39, 0x63, 0x61, 0x50, + 0x41, 0x52, 0x59, 0x70, 0x6F, 0x4B, 0x64, 0x72, 0x63, 0x64, 0x37, 0x62, 0x2F, 0x2B, 0x41, 0x0A, 0x6C, 0x75, 0x6E, 0x34, 0x6A, 0x57, 0x71, 0x39, 0x47, 0x4A, 0x41, 0x64, 0x2F, 0x30, 0x6B, 0x61, 0x6B, 0x46, 0x49, 0x33, 0x6B, 0x79, 0x38, 0x38, + 0x41, 0x6C, 0x32, 0x43, 0x64, 0x67, 0x74, 0x52, 0x35, 0x78, 0x62, 0x48, 0x56, 0x2F, 0x67, 0x34, 0x2B, 0x61, 0x66, 0x4E, 0x6D, 0x79, 0x4A, 0x55, 0x37, 0x32, 0x4F, 0x77, 0x46, 0x57, 0x31, 0x54, 0x5A, 0x51, 0x4E, 0x4B, 0x58, 0x6B, 0x71, 0x67, + 0x73, 0x71, 0x65, 0x4F, 0x53, 0x51, 0x42, 0x5A, 0x4F, 0x4E, 0x58, 0x48, 0x0A, 0x39, 0x49, 0x42, 0x6B, 0x39, 0x57, 0x36, 0x56, 0x55, 0x4C, 0x67, 0x52, 0x66, 0x68, 0x56, 0x77, 0x4F, 0x45, 0x71, 0x77, 0x66, 0x39, 0x44, 0x45, 0x4D, 0x6E, 0x44, + 0x41, 0x47, 0x66, 0x2F, 0x4A, 0x4F, 0x43, 0x30, 0x55, 0x4C, 0x47, 0x62, 0x30, 0x51, 0x6B, 0x54, 0x6D, 0x56, 0x58, 0x59, 0x62, 0x67, 0x42, 0x56, 0x58, 0x2F, 0x38, 0x43, 0x6E, 0x70, 0x36, 0x6F, 0x35, 0x71, 0x74, 0x6A, 0x54, 0x63, 0x4E, 0x41, + 0x75, 0x75, 0x75, 0x75, 0x55, 0x61, 0x76, 0x70, 0x66, 0x0A, 0x4E, 0x49, 0x62, 0x6E, 0x59, 0x72, 0x58, 0x39, 0x69, 0x76, 0x41, 0x77, 0x68, 0x5A, 0x54, 0x4A, 0x72, 0x79, 0x51, 0x43, 0x4C, 0x32, 0x2F, 0x57, 0x33, 0x57, 0x66, 0x2B, 0x34, 0x37, + 0x42, 0x56, 0x54, 0x77, 0x53, 0x59, 0x54, 0x36, 0x52, 0x42, 0x56, 0x75, 0x4B, 0x54, 0x30, 0x47, 0x72, 0x6F, 0x31, 0x76, 0x50, 0x37, 0x5A, 0x65, 0x44, 0x4F, 0x64, 0x63, 0x51, 0x78, 0x57, 0x51, 0x7A, 0x75, 0x67, 0x73, 0x67, 0x4D, 0x59, 0x44, + 0x4E, 0x4B, 0x47, 0x62, 0x71, 0x45, 0x0A, 0x5A, 0x79, 0x63, 0x50, 0x76, 0x45, 0x4A, 0x64, 0x76, 0x53, 0x52, 0x55, 0x44, 0x65, 0x77, 0x64, 0x63, 0x41, 0x5A, 0x66, 0x70, 0x4C, 0x7A, 0x36, 0x49, 0x48, 0x78, 0x56, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x76, 0x54, 0x72, 0x75, 0x73, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, + 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x44, 0x7A, 0x43, 0x43, 0x41, 0x5A, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x62, 0x6D, 0x71, 0x38, 0x57, 0x61, 0x70, 0x54, 0x76, 0x70, 0x67, 0x35, 0x5A, + 0x36, 0x4C, 0x53, 0x61, 0x36, 0x51, 0x37, 0x35, 0x6D, 0x30, 0x63, 0x31, 0x74, 0x6F, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x77, 0x52, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x48, 0x44, 0x41, 0x61, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x32, 0x6C, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x51, 0x32, 0x68, 0x70, 0x62, 0x6D, 0x45, 0x67, + 0x51, 0x32, 0x38, 0x75, 0x4C, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x47, 0x6A, 0x41, 0x59, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x58, 0x5A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, + 0x0A, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x34, 0x4D, 0x44, 0x63, 0x7A, 0x4D, 0x54, 0x41, 0x33, 0x4D, 0x6A, 0x59, 0x30, 0x4E, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x7A, 0x4D, 0x44, 0x63, + 0x7A, 0x4D, 0x54, 0x41, 0x33, 0x4D, 0x6A, 0x59, 0x30, 0x4E, 0x46, 0x6F, 0x77, 0x52, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x48, 0x44, 0x41, 0x61, 0x0A, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x32, 0x6C, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x51, 0x32, 0x68, 0x70, 0x62, 0x6D, 0x45, 0x67, 0x51, 0x32, 0x38, 0x75, 0x4C, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x47, 0x6A, 0x41, 0x59, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x58, 0x5A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x48, 0x59, 0x77, 0x0A, 0x45, 0x41, 0x59, 0x48, 0x4B, + 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x43, 0x41, 0x51, 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x5A, 0x56, 0x42, 0x4B, 0x72, 0x6F, 0x78, 0x35, 0x6C, 0x6B, 0x71, 0x71, 0x48, 0x41, 0x6A, 0x44, 0x6F, + 0x36, 0x4C, 0x4E, 0x2F, 0x6C, 0x6C, 0x57, 0x51, 0x58, 0x66, 0x39, 0x4A, 0x70, 0x52, 0x43, 0x75, 0x78, 0x33, 0x4E, 0x43, 0x4E, 0x74, 0x7A, 0x73, 0x6C, 0x74, 0x31, 0x38, 0x38, 0x2B, 0x63, 0x0A, 0x54, 0x6F, 0x4C, 0x30, 0x76, 0x2F, 0x68, 0x68, + 0x4A, 0x6F, 0x56, 0x73, 0x31, 0x6F, 0x56, 0x62, 0x63, 0x6E, 0x44, 0x53, 0x2F, 0x64, 0x74, 0x69, 0x74, 0x4E, 0x39, 0x54, 0x69, 0x37, 0x32, 0x78, 0x52, 0x46, 0x68, 0x69, 0x51, 0x67, 0x6E, 0x48, 0x2B, 0x6E, 0x39, 0x62, 0x45, 0x4F, 0x66, 0x2B, + 0x51, 0x50, 0x33, 0x41, 0x32, 0x4D, 0x4D, 0x72, 0x4D, 0x75, 0x64, 0x77, 0x70, 0x72, 0x65, 0x6D, 0x49, 0x46, 0x55, 0x64, 0x65, 0x34, 0x42, 0x64, 0x53, 0x34, 0x39, 0x6E, 0x0A, 0x54, 0x50, 0x45, 0x51, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, + 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x6D, 0x44, 0x6E, 0x4E, 0x76, 0x74, 0x69, 0x79, 0x6A, 0x50, 0x65, 0x79, 0x71, 0x2B, 0x47, 0x74, 0x4A, 0x4B, 0x39, 0x37, 0x66, 0x4B, 0x48, 0x62, 0x48, 0x38, 0x38, + 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4F, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, + 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, 0x77, 0x5A, 0x51, 0x49, 0x77, 0x56, 0x35, 0x33, 0x64, 0x56, 0x76, 0x48, 0x48, 0x34, 0x2B, + 0x6D, 0x34, 0x53, 0x56, 0x42, 0x72, 0x6D, 0x32, 0x6E, 0x44, 0x62, 0x2B, 0x7A, 0x44, 0x66, 0x53, 0x58, 0x6B, 0x56, 0x35, 0x55, 0x54, 0x0A, 0x51, 0x4A, 0x74, 0x53, 0x30, 0x7A, 0x76, 0x7A, 0x51, 0x42, 0x6D, 0x38, 0x4A, 0x73, 0x63, 0x74, 0x42, + 0x70, 0x36, 0x31, 0x65, 0x7A, 0x61, 0x66, 0x39, 0x53, 0x58, 0x55, 0x59, 0x32, 0x73, 0x41, 0x41, 0x6A, 0x45, 0x41, 0x36, 0x64, 0x50, 0x47, 0x6E, 0x6C, 0x61, 0x61, 0x4B, 0x73, 0x79, 0x68, 0x32, 0x6A, 0x2F, 0x49, 0x5A, 0x69, 0x76, 0x54, 0x57, + 0x4A, 0x77, 0x67, 0x68, 0x66, 0x71, 0x72, 0x6B, 0x59, 0x70, 0x77, 0x63, 0x42, 0x45, 0x34, 0x59, 0x47, 0x51, 0x4C, 0x0A, 0x59, 0x67, 0x6D, 0x52, 0x57, 0x41, 0x44, 0x35, 0x54, 0x66, 0x73, 0x30, 0x61, 0x4E, 0x6F, 0x4A, 0x72, 0x53, 0x45, 0x47, + 0x47, 0x4A, 0x54, 0x4F, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x76, 0x54, 0x72, 0x75, 0x73, 0x20, 0x52, 0x6F, + 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x56, 0x6A, 0x43, 0x43, 0x41, 0x7A, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x51, 0x2B, 0x4E, 0x78, 0x45, 0x39, 0x69, 0x7A, 0x57, 0x52, 0x52, 0x64, + 0x74, 0x38, 0x36, 0x4D, 0x2F, 0x54, 0x58, 0x39, 0x62, 0x37, 0x77, 0x46, 0x6A, 0x55, 0x55, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x51, 0x7A, 0x45, 0x4C, + 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x48, 0x44, 0x41, 0x61, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x45, 0x32, 0x6C, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x51, 0x32, 0x68, + 0x70, 0x62, 0x6D, 0x45, 0x67, 0x51, 0x32, 0x38, 0x75, 0x4C, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x46, 0x6A, 0x41, 0x55, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x44, 0x58, 0x5A, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x49, 0x46, 0x4A, + 0x76, 0x0A, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x67, 0x77, 0x4E, 0x7A, 0x4D, 0x78, 0x4D, 0x44, 0x63, 0x79, 0x4E, 0x44, 0x41, 0x31, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x4D, 0x77, 0x4E, 0x7A, + 0x4D, 0x78, 0x4D, 0x44, 0x63, 0x79, 0x4E, 0x44, 0x41, 0x31, 0x57, 0x6A, 0x42, 0x44, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x0A, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x54, 0x61, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x44, 0x61, 0x47, 0x6C, 0x75, 0x59, 0x53, 0x42, 0x44, 0x62, 0x79, 0x34, 0x73, 0x54, 0x48, 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x57, 0x4D, 0x42, 0x51, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4E, 0x64, 0x6C, 0x52, 0x79, 0x64, 0x58, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x0A, 0x4B, 0x6F, 0x5A, 0x49, + 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4C, 0x31, 0x56, 0x66, 0x47, 0x48, 0x54, 0x75, 0x42, 0x30, 0x45, + 0x59, 0x67, 0x57, 0x67, 0x72, 0x6D, 0x79, 0x33, 0x63, 0x4C, 0x52, 0x42, 0x36, 0x6B, 0x73, 0x44, 0x58, 0x68, 0x41, 0x2F, 0x6B, 0x46, 0x6F, 0x63, 0x69, 0x7A, 0x75, 0x77, 0x5A, 0x6F, 0x74, 0x73, 0x0A, 0x53, 0x4B, 0x59, 0x63, 0x49, 0x72, 0x72, + 0x56, 0x51, 0x4A, 0x4C, 0x75, 0x4D, 0x37, 0x49, 0x6A, 0x57, 0x63, 0x6D, 0x4F, 0x76, 0x46, 0x6A, 0x61, 0x69, 0x35, 0x37, 0x51, 0x47, 0x66, 0x49, 0x76, 0x57, 0x63, 0x61, 0x4D, 0x59, 0x31, 0x71, 0x36, 0x6E, 0x36, 0x4D, 0x4C, 0x73, 0x4C, 0x4F, + 0x61, 0x58, 0x4C, 0x6F, 0x52, 0x75, 0x42, 0x4C, 0x70, 0x44, 0x4C, 0x76, 0x50, 0x62, 0x6D, 0x79, 0x41, 0x68, 0x79, 0x6B, 0x55, 0x41, 0x79, 0x79, 0x4E, 0x4A, 0x4A, 0x72, 0x49, 0x0A, 0x5A, 0x49, 0x4F, 0x31, 0x61, 0x71, 0x77, 0x54, 0x4C, 0x44, + 0x50, 0x78, 0x6E, 0x39, 0x77, 0x73, 0x59, 0x54, 0x77, 0x61, 0x50, 0x33, 0x42, 0x56, 0x6D, 0x36, 0x30, 0x41, 0x55, 0x6E, 0x2F, 0x50, 0x42, 0x4C, 0x6E, 0x2B, 0x4E, 0x76, 0x71, 0x63, 0x77, 0x42, 0x61, 0x75, 0x59, 0x76, 0x36, 0x57, 0x54, 0x45, + 0x4E, 0x2B, 0x56, 0x52, 0x53, 0x2B, 0x47, 0x72, 0x50, 0x53, 0x62, 0x63, 0x4B, 0x76, 0x64, 0x6D, 0x61, 0x56, 0x61, 0x79, 0x71, 0x77, 0x6C, 0x48, 0x65, 0x46, 0x0A, 0x58, 0x67, 0x51, 0x50, 0x59, 0x68, 0x31, 0x6A, 0x64, 0x66, 0x64, 0x72, 0x35, + 0x38, 0x74, 0x62, 0x6D, 0x6E, 0x44, 0x73, 0x50, 0x6D, 0x63, 0x46, 0x38, 0x50, 0x34, 0x48, 0x43, 0x49, 0x44, 0x50, 0x4B, 0x4E, 0x73, 0x46, 0x78, 0x68, 0x51, 0x6E, 0x4C, 0x34, 0x5A, 0x39, 0x38, 0x43, 0x66, 0x65, 0x2F, 0x2B, 0x5A, 0x2B, 0x4D, + 0x30, 0x6A, 0x6E, 0x43, 0x78, 0x35, 0x59, 0x30, 0x53, 0x63, 0x72, 0x55, 0x77, 0x35, 0x58, 0x53, 0x6D, 0x58, 0x58, 0x2B, 0x36, 0x4B, 0x41, 0x0A, 0x59, 0x50, 0x78, 0x4D, 0x76, 0x44, 0x56, 0x54, 0x41, 0x57, 0x71, 0x58, 0x63, 0x6F, 0x4B, 0x76, + 0x38, 0x52, 0x31, 0x77, 0x36, 0x4A, 0x7A, 0x31, 0x37, 0x31, 0x37, 0x43, 0x62, 0x4D, 0x64, 0x48, 0x66, 0x6C, 0x71, 0x55, 0x68, 0x53, 0x5A, 0x4E, 0x4F, 0x37, 0x72, 0x72, 0x54, 0x4F, 0x69, 0x77, 0x43, 0x63, 0x4A, 0x6C, 0x77, 0x70, 0x32, 0x64, + 0x43, 0x5A, 0x74, 0x4F, 0x74, 0x5A, 0x63, 0x46, 0x72, 0x50, 0x55, 0x47, 0x6F, 0x50, 0x63, 0x32, 0x42, 0x58, 0x37, 0x30, 0x0A, 0x6B, 0x4C, 0x4A, 0x72, 0x78, 0x4C, 0x54, 0x35, 0x5A, 0x4F, 0x72, 0x70, 0x47, 0x67, 0x72, 0x49, 0x44, 0x61, 0x6A, + 0x74, 0x4A, 0x38, 0x6E, 0x55, 0x35, 0x37, 0x4F, 0x35, 0x71, 0x34, 0x49, 0x69, 0x6B, 0x43, 0x63, 0x39, 0x4B, 0x75, 0x68, 0x38, 0x6B, 0x4F, 0x2B, 0x38, 0x54, 0x2F, 0x33, 0x69, 0x43, 0x69, 0x53, 0x6E, 0x33, 0x6D, 0x55, 0x6B, 0x70, 0x46, 0x33, + 0x71, 0x77, 0x48, 0x59, 0x77, 0x30, 0x33, 0x64, 0x51, 0x2B, 0x41, 0x30, 0x45, 0x6D, 0x35, 0x51, 0x32, 0x0A, 0x41, 0x58, 0x50, 0x4B, 0x42, 0x6C, 0x69, 0x6D, 0x30, 0x7A, 0x76, 0x63, 0x2B, 0x67, 0x52, 0x47, 0x45, 0x31, 0x57, 0x4B, 0x79, 0x55, + 0x52, 0x48, 0x75, 0x46, 0x45, 0x35, 0x47, 0x69, 0x37, 0x6F, 0x4E, 0x4F, 0x4A, 0x35, 0x79, 0x31, 0x6C, 0x4B, 0x43, 0x6E, 0x2B, 0x38, 0x70, 0x75, 0x38, 0x66, 0x41, 0x32, 0x64, 0x71, 0x57, 0x53, 0x73, 0x6C, 0x59, 0x70, 0x50, 0x5A, 0x55, 0x78, + 0x6C, 0x6D, 0x50, 0x43, 0x64, 0x69, 0x4B, 0x59, 0x5A, 0x4E, 0x70, 0x47, 0x76, 0x75, 0x0A, 0x2F, 0x39, 0x52, 0x4F, 0x75, 0x74, 0x57, 0x30, 0x34, 0x6F, 0x35, 0x49, 0x57, 0x67, 0x41, 0x5A, 0x43, 0x66, 0x45, 0x46, 0x32, 0x63, 0x36, 0x52, 0x73, + 0x66, 0x66, 0x72, 0x36, 0x54, 0x6C, 0x50, 0x39, 0x6D, 0x38, 0x45, 0x51, 0x35, 0x70, 0x56, 0x39, 0x54, 0x34, 0x46, 0x46, 0x4C, 0x32, 0x2F, 0x73, 0x31, 0x6D, 0x30, 0x32, 0x49, 0x34, 0x7A, 0x68, 0x4B, 0x4F, 0x51, 0x55, 0x71, 0x71, 0x7A, 0x41, + 0x70, 0x56, 0x67, 0x2B, 0x51, 0x78, 0x4D, 0x61, 0x50, 0x6E, 0x75, 0x0A, 0x31, 0x52, 0x63, 0x4E, 0x2B, 0x48, 0x46, 0x58, 0x74, 0x53, 0x58, 0x6B, 0x4B, 0x65, 0x35, 0x6C, 0x58, 0x61, 0x2F, 0x52, 0x37, 0x6A, 0x77, 0x58, 0x43, 0x31, 0x70, 0x44, + 0x78, 0x61, 0x57, 0x47, 0x36, 0x69, 0x53, 0x65, 0x34, 0x67, 0x55, 0x48, 0x33, 0x44, 0x52, 0x43, 0x45, 0x70, 0x48, 0x57, 0x4F, 0x58, 0x53, 0x75, 0x54, 0x45, 0x47, 0x43, 0x32, 0x2F, 0x4B, 0x6D, 0x53, 0x4E, 0x47, 0x7A, 0x6D, 0x2F, 0x4D, 0x7A, + 0x71, 0x76, 0x4F, 0x6D, 0x77, 0x4D, 0x56, 0x4F, 0x0A, 0x39, 0x66, 0x53, 0x64, 0x64, 0x6D, 0x50, 0x6D, 0x41, 0x73, 0x59, 0x69, 0x53, 0x38, 0x47, 0x56, 0x50, 0x31, 0x42, 0x6B, 0x4C, 0x46, 0x54, 0x6C, 0x74, 0x76, 0x41, 0x38, 0x4B, 0x63, 0x39, + 0x58, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x55, 0x59, 0x6E, 0x42, 0x6A, 0x38, 0x58, 0x57, 0x45, 0x51, 0x31, 0x69, + 0x4F, 0x30, 0x52, 0x59, 0x67, 0x0A, 0x73, 0x63, 0x61, 0x73, 0x47, 0x72, 0x7A, 0x32, 0x69, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, + 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, + 0x4F, 0x43, 0x0A, 0x41, 0x67, 0x45, 0x41, 0x4B, 0x62, 0x71, 0x53, 0x53, 0x61, 0x65, 0x74, 0x38, 0x50, 0x46, 0x77, 0x77, 0x2B, 0x53, 0x58, 0x38, 0x4A, 0x2B, 0x70, 0x4A, 0x64, 0x56, 0x72, 0x6E, 0x6A, 0x54, 0x2B, 0x35, 0x68, 0x70, 0x6B, 0x39, + 0x6A, 0x70, 0x72, 0x55, 0x72, 0x49, 0x51, 0x65, 0x42, 0x71, 0x66, 0x54, 0x4E, 0x71, 0x4B, 0x32, 0x75, 0x77, 0x63, 0x4E, 0x31, 0x4C, 0x67, 0x51, 0x6B, 0x76, 0x37, 0x62, 0x48, 0x62, 0x4B, 0x4A, 0x41, 0x73, 0x35, 0x45, 0x68, 0x57, 0x64, 0x0A, + 0x6E, 0x78, 0x45, 0x74, 0x2F, 0x48, 0x6C, 0x6B, 0x33, 0x4F, 0x44, 0x67, 0x39, 0x64, 0x33, 0x67, 0x56, 0x38, 0x6D, 0x6C, 0x73, 0x6E, 0x5A, 0x77, 0x55, 0x4B, 0x54, 0x2B, 0x74, 0x77, 0x70, 0x77, 0x31, 0x61, 0x41, 0x30, 0x38, 0x58, 0x58, 0x58, + 0x54, 0x55, 0x6D, 0x36, 0x45, 0x64, 0x47, 0x7A, 0x32, 0x4F, 0x79, 0x43, 0x2F, 0x2B, 0x73, 0x4F, 0x78, 0x4C, 0x39, 0x6B, 0x4C, 0x58, 0x31, 0x6A, 0x62, 0x68, 0x64, 0x34, 0x37, 0x46, 0x31, 0x38, 0x69, 0x4D, 0x6A, 0x72, 0x0A, 0x6A, 0x6C, 0x64, + 0x32, 0x32, 0x56, 0x6B, 0x45, 0x2B, 0x72, 0x78, 0x53, 0x48, 0x30, 0x57, 0x73, 0x38, 0x48, 0x71, 0x41, 0x37, 0x4F, 0x78, 0x76, 0x64, 0x71, 0x36, 0x52, 0x32, 0x78, 0x43, 0x4F, 0x42, 0x4E, 0x79, 0x53, 0x33, 0x36, 0x44, 0x32, 0x35, 0x71, 0x35, + 0x4A, 0x30, 0x38, 0x46, 0x73, 0x45, 0x68, 0x76, 0x4D, 0x4B, 0x61, 0x72, 0x35, 0x43, 0x4B, 0x58, 0x69, 0x4E, 0x78, 0x54, 0x4B, 0x73, 0x62, 0x68, 0x6D, 0x37, 0x78, 0x71, 0x43, 0x35, 0x50, 0x44, 0x34, 0x0A, 0x38, 0x61, 0x63, 0x57, 0x61, 0x62, + 0x66, 0x62, 0x71, 0x57, 0x45, 0x38, 0x6E, 0x2F, 0x55, 0x78, 0x79, 0x2B, 0x51, 0x41, 0x52, 0x73, 0x49, 0x76, 0x64, 0x4C, 0x47, 0x78, 0x31, 0x34, 0x48, 0x75, 0x71, 0x43, 0x61, 0x56, 0x76, 0x49, 0x69, 0x76, 0x54, 0x44, 0x55, 0x48, 0x4B, 0x67, + 0x4C, 0x4B, 0x65, 0x42, 0x52, 0x74, 0x52, 0x79, 0x74, 0x41, 0x56, 0x75, 0x6E, 0x4C, 0x4B, 0x6D, 0x43, 0x68, 0x5A, 0x77, 0x4F, 0x67, 0x7A, 0x6F, 0x79, 0x38, 0x73, 0x48, 0x4A, 0x6E, 0x0A, 0x78, 0x44, 0x48, 0x4F, 0x32, 0x7A, 0x54, 0x6C, 0x4A, + 0x51, 0x4E, 0x67, 0x4A, 0x58, 0x74, 0x78, 0x6D, 0x4F, 0x54, 0x41, 0x47, 0x79, 0x74, 0x66, 0x64, 0x45, 0x4C, 0x53, 0x53, 0x38, 0x56, 0x5A, 0x43, 0x41, 0x65, 0x48, 0x76, 0x73, 0x58, 0x44, 0x66, 0x2B, 0x65, 0x57, 0x32, 0x65, 0x48, 0x63, 0x4B, + 0x4A, 0x66, 0x57, 0x6A, 0x77, 0x58, 0x6A, 0x39, 0x5A, 0x74, 0x4F, 0x79, 0x68, 0x31, 0x51, 0x52, 0x77, 0x56, 0x54, 0x73, 0x4D, 0x6F, 0x35, 0x35, 0x34, 0x57, 0x67, 0x0A, 0x69, 0x63, 0x45, 0x46, 0x4F, 0x77, 0x45, 0x33, 0x30, 0x7A, 0x39, 0x4A, + 0x34, 0x6E, 0x66, 0x72, 0x49, 0x38, 0x69, 0x49, 0x5A, 0x6A, 0x73, 0x39, 0x4F, 0x58, 0x59, 0x68, 0x52, 0x76, 0x48, 0x73, 0x58, 0x79, 0x4F, 0x34, 0x36, 0x36, 0x4A, 0x6D, 0x64, 0x58, 0x54, 0x42, 0x51, 0x50, 0x66, 0x59, 0x61, 0x4A, 0x71, 0x54, + 0x34, 0x69, 0x32, 0x70, 0x4C, 0x72, 0x30, 0x63, 0x6F, 0x78, 0x37, 0x49, 0x64, 0x4D, 0x61, 0x6B, 0x4C, 0x58, 0x6F, 0x67, 0x71, 0x7A, 0x75, 0x34, 0x0A, 0x73, 0x45, 0x62, 0x39, 0x62, 0x39, 0x31, 0x66, 0x55, 0x6C, 0x56, 0x31, 0x59, 0x76, 0x43, + 0x58, 0x6F, 0x48, 0x7A, 0x58, 0x4F, 0x50, 0x30, 0x6C, 0x33, 0x38, 0x32, 0x67, 0x6D, 0x78, 0x44, 0x50, 0x69, 0x37, 0x67, 0x34, 0x58, 0x6C, 0x37, 0x46, 0x74, 0x4B, 0x59, 0x43, 0x4E, 0x71, 0x45, 0x65, 0x58, 0x78, 0x7A, 0x50, 0x34, 0x70, 0x61, + 0x64, 0x4B, 0x61, 0x72, 0x39, 0x6D, 0x4B, 0x35, 0x53, 0x34, 0x66, 0x4E, 0x42, 0x55, 0x76, 0x75, 0x70, 0x4C, 0x6E, 0x4B, 0x57, 0x0A, 0x6E, 0x79, 0x66, 0x6A, 0x71, 0x6E, 0x4E, 0x39, 0x2B, 0x42, 0x6F, 0x6A, 0x5A, 0x6E, 0x73, 0x37, 0x71, 0x32, + 0x57, 0x77, 0x4D, 0x67, 0x46, 0x4C, 0x46, 0x54, 0x34, 0x39, 0x6F, 0x6B, 0x38, 0x4D, 0x4B, 0x7A, 0x57, 0x69, 0x78, 0x74, 0x6C, 0x6E, 0x45, 0x6A, 0x55, 0x77, 0x7A, 0x58, 0x59, 0x75, 0x46, 0x72, 0x4F, 0x5A, 0x6E, 0x6B, 0x31, 0x50, 0x54, 0x69, + 0x30, 0x37, 0x4E, 0x45, 0x50, 0x68, 0x6D, 0x67, 0x34, 0x4E, 0x70, 0x47, 0x61, 0x58, 0x75, 0x74, 0x49, 0x63, 0x0A, 0x53, 0x6B, 0x77, 0x73, 0x4B, 0x6F, 0x75, 0x4C, 0x67, 0x55, 0x39, 0x78, 0x47, 0x71, 0x6E, 0x64, 0x58, 0x48, 0x74, 0x37, 0x43, + 0x4D, 0x55, 0x41, 0x44, 0x54, 0x64, 0x41, 0x34, 0x33, 0x78, 0x37, 0x56, 0x46, 0x38, 0x76, 0x68, 0x56, 0x39, 0x32, 0x39, 0x76, 0x65, 0x6E, 0x73, 0x42, 0x78, 0x58, 0x56, 0x73, 0x46, 0x79, 0x36, 0x4B, 0x32, 0x69, 0x72, 0x34, 0x30, 0x7A, 0x53, + 0x62, 0x6F, 0x66, 0x69, 0x74, 0x7A, 0x6D, 0x64, 0x48, 0x78, 0x67, 0x68, 0x6D, 0x2B, 0x48, 0x0A, 0x6C, 0x33, 0x73, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x49, 0x53, 0x52, 0x47, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x58, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, + 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x47, 0x7A, 0x43, 0x43, 0x41, 0x61, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x51, 0x51, 0x64, 0x4B, 0x64, 0x30, 0x58, 0x4C, 0x71, 0x37, 0x71, 0x65, 0x41, 0x77, 0x53, 0x78, 0x73, 0x36, 0x53, 0x2B, 0x48, 0x55, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, + 0x42, 0x50, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x70, 0x4D, 0x43, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x67, 0x53, 0x57, 0x35, 0x30, 0x5A, + 0x58, 0x4A, 0x75, 0x5A, 0x58, 0x51, 0x67, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x55, 0x6D, 0x56, 0x7A, 0x5A, 0x57, 0x46, 0x79, 0x59, 0x32, 0x67, 0x67, 0x52, 0x33, 0x4A, 0x76, 0x64, 0x58, 0x41, 0x78, 0x46, + 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x44, 0x45, 0x6C, 0x54, 0x0A, 0x55, 0x6B, 0x63, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x59, 0x4D, 0x6A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x44, 0x41, 0x35, + 0x4D, 0x44, 0x51, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x44, 0x41, 0x35, 0x4D, 0x54, 0x63, 0x78, 0x4E, 0x6A, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x45, 0x38, 0x78, 0x43, 0x7A, 0x41, 0x4A, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x0A, 0x4D, 0x53, 0x6B, 0x77, 0x4A, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x79, 0x42, 0x4A, 0x62, 0x6E, 0x52, 0x6C, 0x63, 0x6D, 0x35, 0x6C, 0x64, 0x43, 0x42, + 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x53, 0x5A, 0x58, 0x4E, 0x6C, 0x59, 0x58, 0x4A, 0x6A, 0x61, 0x43, 0x42, 0x48, 0x63, 0x6D, 0x39, 0x31, 0x63, 0x44, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x41, 0x78, 0x4D, 0x4D, 0x53, 0x56, 0x4E, 0x53, 0x0A, 0x52, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x46, 0x67, 0x79, 0x4D, 0x48, 0x59, 0x77, 0x45, 0x41, 0x59, 0x48, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x43, 0x41, 0x51, + 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x7A, 0x5A, 0x76, 0x56, 0x6E, 0x34, 0x43, 0x44, 0x43, 0x75, 0x77, 0x4A, 0x53, 0x76, 0x4D, 0x57, 0x53, 0x6A, 0x35, 0x63, 0x7A, 0x33, 0x65, 0x73, 0x33, 0x6D, + 0x63, 0x46, 0x44, 0x52, 0x30, 0x48, 0x0A, 0x74, 0x74, 0x77, 0x57, 0x2B, 0x31, 0x71, 0x4C, 0x46, 0x4E, 0x76, 0x69, 0x63, 0x57, 0x44, 0x45, 0x75, 0x6B, 0x57, 0x56, 0x45, 0x59, 0x6D, 0x4F, 0x36, 0x67, 0x62, 0x66, 0x39, 0x79, 0x6F, 0x57, 0x48, + 0x4B, 0x53, 0x35, 0x78, 0x63, 0x55, 0x79, 0x34, 0x41, 0x50, 0x67, 0x48, 0x6F, 0x49, 0x59, 0x4F, 0x49, 0x76, 0x58, 0x52, 0x64, 0x67, 0x4B, 0x61, 0x6D, 0x37, 0x6D, 0x41, 0x48, 0x66, 0x37, 0x41, 0x6C, 0x46, 0x39, 0x49, 0x74, 0x67, 0x4B, 0x62, + 0x70, 0x70, 0x62, 0x0A, 0x64, 0x39, 0x2F, 0x77, 0x2B, 0x6B, 0x48, 0x73, 0x4F, 0x64, 0x78, 0x31, 0x79, 0x6D, 0x67, 0x48, 0x44, 0x42, 0x2F, 0x71, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, + 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, + 0x0A, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x66, 0x45, 0x4B, 0x57, 0x72, 0x74, 0x35, 0x4C, 0x53, 0x44, 0x76, 0x36, 0x6B, 0x76, 0x69, 0x65, 0x6A, 0x4D, 0x39, 0x74, 0x69, 0x36, 0x6C, 0x79, 0x4E, 0x35, 0x55, 0x77, 0x43, 0x67, 0x59, + 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, 0x77, 0x5A, 0x51, 0x49, 0x77, 0x65, 0x33, 0x6C, 0x4F, 0x52, 0x6C, 0x43, 0x45, 0x77, 0x6B, 0x53, 0x48, 0x52, 0x68, 0x74, 0x46, 0x0A, 0x63, 0x50, + 0x39, 0x59, 0x6D, 0x64, 0x37, 0x30, 0x2F, 0x61, 0x54, 0x53, 0x56, 0x61, 0x59, 0x67, 0x4C, 0x58, 0x54, 0x57, 0x4E, 0x4C, 0x78, 0x42, 0x6F, 0x31, 0x42, 0x66, 0x41, 0x53, 0x64, 0x57, 0x74, 0x4C, 0x34, 0x6E, 0x64, 0x51, 0x61, 0x76, 0x45, 0x69, + 0x35, 0x31, 0x6D, 0x49, 0x33, 0x38, 0x41, 0x6A, 0x45, 0x41, 0x69, 0x2F, 0x56, 0x33, 0x62, 0x4E, 0x54, 0x49, 0x5A, 0x61, 0x72, 0x67, 0x43, 0x79, 0x7A, 0x75, 0x46, 0x4A, 0x30, 0x6E, 0x4E, 0x36, 0x54, 0x35, 0x0A, 0x55, 0x36, 0x56, 0x52, 0x35, + 0x43, 0x6D, 0x44, 0x31, 0x2F, 0x69, 0x51, 0x4D, 0x56, 0x74, 0x43, 0x6E, 0x77, 0x72, 0x31, 0x2F, 0x71, 0x34, 0x41, 0x61, 0x4F, 0x65, 0x4D, 0x53, 0x51, 0x2B, 0x32, 0x62, 0x31, 0x74, 0x62, 0x46, 0x66, 0x4C, 0x6E, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x48, 0x69, 0x50, 0x4B, 0x49, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x47, + 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x61, 0x6A, 0x43, 0x43, 0x41, 0x31, 0x4B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x4C, 0x64, 0x32, 0x73, 0x7A, 0x6D, 0x4B, 0x58, 0x6C, 0x4B, 0x46, + 0x44, 0x36, 0x4C, 0x44, 0x4E, 0x64, 0x6D, 0x70, 0x65, 0x59, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x50, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, + 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x55, 0x56, 0x7A, 0x45, 0x6A, 0x4D, 0x43, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x61, 0x51, 0x32, 0x68, 0x31, 0x62, 0x6D, 0x64, 0x6F, 0x64, 0x32, 0x45, 0x67, 0x56, 0x47, + 0x56, 0x73, 0x5A, 0x57, 0x4E, 0x76, 0x62, 0x53, 0x42, 0x44, 0x62, 0x79, 0x34, 0x73, 0x49, 0x45, 0x78, 0x30, 0x5A, 0x43, 0x34, 0x78, 0x47, 0x7A, 0x41, 0x5A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x45, 0x6B, 0x68, 0x70, 0x55, 0x45, + 0x74, 0x4A, 0x0A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4C, 0x53, 0x42, 0x48, 0x4D, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4F, 0x54, 0x41, 0x79, 0x4D, 0x6A, 0x49, 0x77, 0x4F, 0x54, 0x51, 0x32, 0x4D, + 0x44, 0x52, 0x61, 0x46, 0x77, 0x30, 0x7A, 0x4E, 0x7A, 0x45, 0x79, 0x4D, 0x7A, 0x45, 0x78, 0x4E, 0x54, 0x55, 0x35, 0x4E, 0x54, 0x6C, 0x61, 0x4D, 0x45, 0x38, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x0A, + 0x41, 0x6C, 0x52, 0x58, 0x4D, 0x53, 0x4D, 0x77, 0x49, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x70, 0x44, 0x61, 0x48, 0x56, 0x75, 0x5A, 0x32, 0x68, 0x33, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x78, 0x6C, 0x59, 0x32, 0x39, 0x74, + 0x49, 0x45, 0x4E, 0x76, 0x4C, 0x69, 0x77, 0x67, 0x54, 0x48, 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x62, 0x4D, 0x42, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x53, 0x53, 0x47, 0x6C, 0x51, 0x53, 0x30, 0x6B, 0x67, 0x0A, 0x55, 0x6D, 0x39, + 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x74, 0x49, 0x45, 0x63, 0x78, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, + 0x43, 0x41, 0x67, 0x38, 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x39, 0x42, 0x35, 0x2F, 0x55, 0x6E, 0x4D, 0x79, 0x44, 0x48, 0x50, 0x6B, 0x76, 0x52, 0x4E, 0x30, 0x0A, 0x6F, 0x39, 0x51, 0x77, 0x71, 0x4E, + 0x43, 0x75, 0x53, 0x39, 0x69, 0x32, 0x33, 0x33, 0x56, 0x48, 0x5A, 0x76, 0x52, 0x38, 0x35, 0x7A, 0x6B, 0x45, 0x48, 0x6D, 0x70, 0x77, 0x49, 0x4E, 0x4A, 0x61, 0x52, 0x33, 0x4A, 0x6E, 0x56, 0x66, 0x53, 0x6C, 0x36, 0x4A, 0x33, 0x56, 0x48, 0x69, + 0x47, 0x68, 0x38, 0x47, 0x65, 0x36, 0x7A, 0x43, 0x46, 0x6F, 0x76, 0x6B, 0x52, 0x54, 0x76, 0x34, 0x33, 0x35, 0x34, 0x74, 0x77, 0x76, 0x56, 0x63, 0x67, 0x33, 0x50, 0x78, 0x2B, 0x6B, 0x0A, 0x77, 0x4A, 0x79, 0x7A, 0x35, 0x48, 0x64, 0x63, 0x6F, + 0x45, 0x62, 0x2B, 0x64, 0x2F, 0x6F, 0x61, 0x6F, 0x44, 0x6A, 0x71, 0x37, 0x5A, 0x70, 0x79, 0x33, 0x69, 0x75, 0x39, 0x6C, 0x46, 0x63, 0x36, 0x75, 0x75, 0x78, 0x35, 0x35, 0x31, 0x39, 0x39, 0x51, 0x6D, 0x51, 0x35, 0x65, 0x69, 0x59, 0x32, 0x39, + 0x79, 0x54, 0x77, 0x31, 0x53, 0x2B, 0x36, 0x6C, 0x5A, 0x67, 0x52, 0x5A, 0x71, 0x32, 0x58, 0x4E, 0x64, 0x5A, 0x31, 0x41, 0x59, 0x44, 0x67, 0x72, 0x2F, 0x53, 0x45, 0x0A, 0x59, 0x59, 0x77, 0x4E, 0x48, 0x6C, 0x39, 0x38, 0x68, 0x35, 0x5A, 0x65, + 0x51, 0x61, 0x2F, 0x72, 0x68, 0x2B, 0x72, 0x34, 0x58, 0x66, 0x45, 0x75, 0x69, 0x41, 0x55, 0x2B, 0x54, 0x43, 0x4B, 0x37, 0x32, 0x68, 0x38, 0x71, 0x33, 0x56, 0x4A, 0x47, 0x5A, 0x44, 0x6E, 0x7A, 0x51, 0x73, 0x37, 0x5A, 0x6E, 0x67, 0x79, 0x7A, + 0x73, 0x48, 0x65, 0x58, 0x5A, 0x4A, 0x7A, 0x41, 0x39, 0x4B, 0x4D, 0x75, 0x48, 0x35, 0x55, 0x48, 0x73, 0x42, 0x66, 0x66, 0x4D, 0x4E, 0x73, 0x41, 0x0A, 0x47, 0x4A, 0x5A, 0x4D, 0x6F, 0x59, 0x46, 0x4C, 0x33, 0x51, 0x52, 0x74, 0x55, 0x36, 0x4D, + 0x39, 0x2F, 0x41, 0x65, 0x73, 0x31, 0x4D, 0x55, 0x33, 0x67, 0x75, 0x76, 0x6B, 0x6C, 0x51, 0x67, 0x5A, 0x4B, 0x49, 0x4C, 0x53, 0x51, 0x6A, 0x71, 0x6A, 0x32, 0x46, 0x50, 0x73, 0x65, 0x59, 0x6C, 0x67, 0x53, 0x47, 0x44, 0x49, 0x63, 0x70, 0x4A, + 0x51, 0x33, 0x41, 0x4F, 0x50, 0x67, 0x7A, 0x2B, 0x79, 0x51, 0x6C, 0x64, 0x61, 0x32, 0x32, 0x72, 0x70, 0x45, 0x5A, 0x66, 0x64, 0x0A, 0x68, 0x53, 0x69, 0x38, 0x4D, 0x45, 0x79, 0x72, 0x34, 0x38, 0x4B, 0x78, 0x52, 0x55, 0x52, 0x48, 0x48, 0x2B, + 0x43, 0x4B, 0x46, 0x67, 0x65, 0x57, 0x30, 0x69, 0x45, 0x50, 0x55, 0x38, 0x44, 0x74, 0x71, 0x58, 0x37, 0x55, 0x54, 0x75, 0x79, 0x62, 0x43, 0x65, 0x79, 0x76, 0x51, 0x71, 0x77, 0x77, 0x31, 0x72, 0x2F, 0x52, 0x45, 0x45, 0x58, 0x67, 0x70, 0x68, + 0x61, 0x79, 0x70, 0x63, 0x58, 0x54, 0x54, 0x33, 0x4F, 0x55, 0x4D, 0x33, 0x45, 0x43, 0x6F, 0x57, 0x71, 0x6A, 0x0A, 0x31, 0x6A, 0x4F, 0x58, 0x54, 0x79, 0x46, 0x6A, 0x48, 0x6C, 0x75, 0x50, 0x32, 0x63, 0x46, 0x65, 0x52, 0x58, 0x46, 0x33, 0x44, + 0x34, 0x46, 0x64, 0x58, 0x79, 0x47, 0x61, 0x72, 0x59, 0x50, 0x4D, 0x2B, 0x6C, 0x37, 0x57, 0x6A, 0x53, 0x4E, 0x66, 0x47, 0x7A, 0x31, 0x42, 0x72, 0x79, 0x42, 0x31, 0x5A, 0x6C, 0x70, 0x4B, 0x39, 0x70, 0x2F, 0x37, 0x71, 0x78, 0x6A, 0x33, 0x63, + 0x63, 0x43, 0x32, 0x48, 0x54, 0x48, 0x73, 0x4F, 0x79, 0x44, 0x72, 0x79, 0x2B, 0x4B, 0x34, 0x0A, 0x39, 0x61, 0x36, 0x53, 0x73, 0x76, 0x66, 0x68, 0x68, 0x45, 0x76, 0x79, 0x6F, 0x76, 0x4B, 0x54, 0x6D, 0x69, 0x4B, 0x65, 0x30, 0x78, 0x52, 0x76, + 0x4E, 0x6C, 0x53, 0x39, 0x48, 0x31, 0x35, 0x5A, 0x46, 0x62, 0x6C, 0x7A, 0x71, 0x4D, 0x46, 0x38, 0x62, 0x33, 0x74, 0x69, 0x36, 0x52, 0x5A, 0x73, 0x52, 0x31, 0x70, 0x6C, 0x38, 0x77, 0x34, 0x52, 0x6D, 0x30, 0x62, 0x5A, 0x2F, 0x57, 0x33, 0x63, + 0x31, 0x70, 0x7A, 0x41, 0x74, 0x48, 0x32, 0x6C, 0x73, 0x4E, 0x30, 0x2F, 0x0A, 0x56, 0x6D, 0x2B, 0x68, 0x2B, 0x66, 0x62, 0x6B, 0x45, 0x6B, 0x6A, 0x39, 0x42, 0x6E, 0x38, 0x53, 0x56, 0x37, 0x61, 0x70, 0x49, 0x30, 0x39, 0x62, 0x41, 0x38, 0x50, + 0x67, 0x63, 0x53, 0x6F, 0x6A, 0x74, 0x2F, 0x65, 0x77, 0x73, 0x54, 0x75, 0x38, 0x6D, 0x4C, 0x33, 0x57, 0x6D, 0x4B, 0x67, 0x4D, 0x61, 0x2F, 0x61, 0x4F, 0x45, 0x6D, 0x65, 0x6D, 0x38, 0x72, 0x4A, 0x59, 0x35, 0x41, 0x49, 0x4A, 0x45, 0x7A, 0x79, + 0x70, 0x75, 0x78, 0x43, 0x30, 0x30, 0x6A, 0x42, 0x46, 0x0A, 0x38, 0x65, 0x7A, 0x33, 0x41, 0x42, 0x48, 0x66, 0x5A, 0x66, 0x6A, 0x63, 0x4B, 0x30, 0x4E, 0x56, 0x76, 0x78, 0x61, 0x58, 0x78, 0x41, 0x2F, 0x56, 0x4C, 0x47, 0x47, 0x45, 0x71, 0x6E, + 0x4B, 0x47, 0x2F, 0x75, 0x59, 0x36, 0x66, 0x73, 0x49, 0x2F, 0x66, 0x65, 0x37, 0x38, 0x4C, 0x78, 0x51, 0x2B, 0x35, 0x6F, 0x58, 0x64, 0x55, 0x47, 0x2B, 0x33, 0x53, 0x65, 0x30, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, + 0x41, 0x77, 0x44, 0x77, 0x59, 0x44, 0x0A, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x38, + 0x6E, 0x63, 0x58, 0x2B, 0x6C, 0x36, 0x6F, 0x2F, 0x76, 0x59, 0x39, 0x63, 0x64, 0x56, 0x6F, 0x75, 0x73, 0x6C, 0x47, 0x44, 0x44, 0x6A, 0x59, 0x72, 0x37, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, + 0x41, 0x51, 0x44, 0x0A, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x42, 0x51, 0x55, 0x66, 0x42, 0x31, + 0x33, 0x48, 0x41, 0x45, 0x34, 0x2F, 0x2B, 0x71, 0x64, 0x64, 0x52, 0x78, 0x6F, 0x73, 0x75, 0x65, 0x6A, 0x36, 0x69, 0x70, 0x30, 0x36, 0x39, 0x31, 0x78, 0x31, 0x54, 0x50, 0x4F, 0x68, 0x77, 0x45, 0x6D, 0x53, 0x4B, 0x73, 0x78, 0x42, 0x48, 0x69, + 0x0A, 0x37, 0x7A, 0x4E, 0x4B, 0x70, 0x69, 0x4D, 0x64, 0x44, 0x67, 0x31, 0x48, 0x32, 0x44, 0x66, 0x48, 0x62, 0x36, 0x38, 0x30, 0x66, 0x30, 0x2B, 0x42, 0x61, 0x7A, 0x56, 0x50, 0x36, 0x58, 0x4B, 0x6C, 0x4D, 0x65, 0x4A, 0x34, 0x35, 0x2F, 0x64, + 0x4F, 0x6C, 0x42, 0x68, 0x62, 0x51, 0x48, 0x33, 0x50, 0x61, 0x79, 0x46, 0x55, 0x68, 0x75, 0x61, 0x56, 0x65, 0x76, 0x76, 0x47, 0x79, 0x75, 0x71, 0x63, 0x53, 0x45, 0x35, 0x58, 0x43, 0x56, 0x30, 0x76, 0x72, 0x50, 0x53, 0x6C, 0x0A, 0x74, 0x4A, + 0x63, 0x7A, 0x57, 0x4E, 0x57, 0x73, 0x65, 0x61, 0x6E, 0x4D, 0x58, 0x2F, 0x6D, 0x46, 0x2B, 0x6C, 0x4C, 0x46, 0x6A, 0x66, 0x69, 0x52, 0x46, 0x4F, 0x73, 0x36, 0x44, 0x52, 0x66, 0x51, 0x55, 0x73, 0x4A, 0x37, 0x34, 0x38, 0x4A, 0x7A, 0x6A, 0x6B, + 0x5A, 0x34, 0x42, 0x6A, 0x67, 0x73, 0x36, 0x46, 0x7A, 0x61, 0x5A, 0x73, 0x54, 0x30, 0x70, 0x50, 0x42, 0x57, 0x47, 0x54, 0x4D, 0x70, 0x57, 0x6D, 0x57, 0x53, 0x42, 0x55, 0x64, 0x47, 0x53, 0x71, 0x75, 0x45, 0x0A, 0x77, 0x78, 0x34, 0x6E, 0x6F, + 0x52, 0x38, 0x52, 0x6B, 0x70, 0x6B, 0x6E, 0x64, 0x5A, 0x4D, 0x50, 0x76, 0x44, 0x59, 0x37, 0x6C, 0x31, 0x65, 0x50, 0x4A, 0x6C, 0x73, 0x4D, 0x75, 0x35, 0x77, 0x50, 0x31, 0x47, 0x34, 0x77, 0x42, 0x39, 0x54, 0x63, 0x58, 0x7A, 0x5A, 0x6F, 0x5A, + 0x6A, 0x6D, 0x44, 0x6C, 0x69, 0x63, 0x6D, 0x69, 0x73, 0x6A, 0x45, 0x4F, 0x66, 0x36, 0x61, 0x49, 0x57, 0x2F, 0x56, 0x63, 0x6F, 0x62, 0x70, 0x66, 0x32, 0x4C, 0x6C, 0x6C, 0x30, 0x37, 0x51, 0x0A, 0x4A, 0x4E, 0x42, 0x41, 0x73, 0x4E, 0x42, 0x31, + 0x43, 0x49, 0x36, 0x39, 0x61, 0x4F, 0x34, 0x49, 0x31, 0x32, 0x35, 0x38, 0x45, 0x48, 0x42, 0x47, 0x47, 0x33, 0x7A, 0x67, 0x69, 0x4C, 0x4B, 0x65, 0x63, 0x6F, 0x61, 0x5A, 0x41, 0x65, 0x4F, 0x2F, 0x6E, 0x30, 0x6B, 0x5A, 0x74, 0x43, 0x57, 0x2B, + 0x56, 0x6D, 0x57, 0x75, 0x46, 0x32, 0x50, 0x6C, 0x48, 0x74, 0x2F, 0x6F, 0x2F, 0x30, 0x65, 0x6C, 0x76, 0x2B, 0x45, 0x6D, 0x42, 0x59, 0x54, 0x6B, 0x73, 0x4D, 0x43, 0x76, 0x0A, 0x35, 0x77, 0x69, 0x5A, 0x71, 0x41, 0x78, 0x65, 0x4A, 0x6F, 0x42, + 0x46, 0x31, 0x50, 0x68, 0x6F, 0x4C, 0x35, 0x61, 0x50, 0x72, 0x75, 0x4A, 0x4B, 0x48, 0x4A, 0x77, 0x57, 0x44, 0x42, 0x4E, 0x76, 0x4F, 0x49, 0x66, 0x32, 0x75, 0x38, 0x67, 0x30, 0x58, 0x35, 0x49, 0x44, 0x55, 0x58, 0x6C, 0x77, 0x70, 0x74, 0x2F, + 0x4C, 0x39, 0x5A, 0x6C, 0x4E, 0x65, 0x63, 0x31, 0x4F, 0x76, 0x46, 0x65, 0x66, 0x51, 0x30, 0x35, 0x72, 0x4C, 0x69, 0x73, 0x59, 0x2B, 0x47, 0x70, 0x7A, 0x0A, 0x6A, 0x4C, 0x72, 0x46, 0x4E, 0x65, 0x38, 0x35, 0x61, 0x6B, 0x45, 0x65, 0x7A, 0x33, + 0x47, 0x6F, 0x6F, 0x72, 0x4B, 0x47, 0x42, 0x31, 0x73, 0x36, 0x79, 0x65, 0x48, 0x76, 0x50, 0x32, 0x55, 0x45, 0x67, 0x45, 0x63, 0x79, 0x52, 0x48, 0x43, 0x56, 0x54, 0x6A, 0x46, 0x6E, 0x61, 0x6E, 0x52, 0x62, 0x45, 0x45, 0x56, 0x31, 0x36, 0x72, + 0x43, 0x66, 0x30, 0x4F, 0x59, 0x31, 0x2F, 0x6B, 0x36, 0x66, 0x69, 0x38, 0x77, 0x72, 0x6B, 0x6B, 0x56, 0x62, 0x62, 0x69, 0x56, 0x67, 0x0A, 0x68, 0x55, 0x62, 0x4E, 0x30, 0x61, 0x71, 0x77, 0x64, 0x6D, 0x61, 0x54, 0x64, 0x35, 0x61, 0x2B, 0x67, + 0x37, 0x34, 0x34, 0x74, 0x69, 0x52, 0x4F, 0x4A, 0x67, 0x76, 0x4D, 0x37, 0x58, 0x70, 0x57, 0x47, 0x75, 0x44, 0x70, 0x57, 0x73, 0x5A, 0x6B, 0x72, 0x55, 0x78, 0x36, 0x41, 0x45, 0x68, 0x45, 0x4C, 0x37, 0x6C, 0x41, 0x75, 0x78, 0x4D, 0x2B, 0x76, + 0x68, 0x56, 0x34, 0x6E, 0x59, 0x57, 0x42, 0x53, 0x69, 0x70, 0x58, 0x33, 0x74, 0x55, 0x5A, 0x51, 0x39, 0x72, 0x62, 0x0A, 0x79, 0x6C, 0x74, 0x48, 0x68, 0x6F, 0x4D, 0x4C, 0x50, 0x37, 0x59, 0x4E, 0x64, 0x6E, 0x68, 0x7A, 0x65, 0x53, 0x4A, 0x65, + 0x73, 0x59, 0x41, 0x66, 0x7A, 0x37, 0x37, 0x52, 0x50, 0x31, 0x59, 0x51, 0x6D, 0x43, 0x75, 0x56, 0x68, 0x36, 0x45, 0x66, 0x6E, 0x57, 0x51, 0x55, 0x59, 0x44, 0x6B, 0x73, 0x73, 0x77, 0x42, 0x56, 0x4C, 0x75, 0x54, 0x31, 0x73, 0x77, 0x35, 0x58, + 0x78, 0x4A, 0x46, 0x42, 0x41, 0x4A, 0x77, 0x2F, 0x36, 0x4B, 0x58, 0x66, 0x36, 0x76, 0x62, 0x2F, 0x0A, 0x79, 0x50, 0x43, 0x74, 0x62, 0x56, 0x4B, 0x6F, 0x46, 0x36, 0x75, 0x62, 0x59, 0x66, 0x77, 0x53, 0x55, 0x54, 0x58, 0x6B, 0x4A, 0x66, 0x32, + 0x76, 0x71, 0x6D, 0x71, 0x47, 0x4F, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x6C, 0x6F, + 0x62, 0x61, 0x6C, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2D, 0x20, 0x52, 0x34, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x4D, 0x49, 0x49, 0x42, 0x33, 0x44, 0x43, 0x43, 0x41, 0x59, 0x4F, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4E, 0x41, 0x67, 0x50, 0x6C, 0x66, 0x76, 0x55, 0x2F, 0x6B, 0x2F, 0x32, 0x6C, 0x43, 0x53, 0x47, 0x79, 0x70, 0x6A, 0x41, + 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x6A, 0x42, 0x51, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x74, 0x48, 0x62, 0x47, 0x39, 0x69, 0x0A, 0x59, 0x57, + 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x55, 0x6A, 0x51, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, + 0x6F, 0x54, 0x43, 0x6B, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x43, 0x6B, 0x64, 0x73, 0x0A, 0x62, 0x32, 0x4A, 0x68, 0x62, + 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x49, 0x78, 0x4D, 0x54, 0x45, 0x7A, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x67, 0x77, 0x4D, 0x54, 0x45, 0x35, 0x4D, + 0x44, 0x4D, 0x78, 0x4E, 0x44, 0x41, 0x33, 0x57, 0x6A, 0x42, 0x51, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x78, 0x74, 0x48, 0x62, 0x47, 0x39, 0x69, 0x0A, 0x59, 0x57, 0x78, 0x54, 0x61, 0x57, 0x64, 0x75, + 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x43, 0x30, 0x67, 0x55, 0x6A, 0x51, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, 0x6B, 0x64, 0x73, + 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x78, 0x45, 0x7A, 0x41, 0x52, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x43, 0x6B, 0x64, 0x73, 0x0A, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x46, 0x4E, 0x70, 0x5A, 0x32, 0x34, + 0x77, 0x57, 0x54, 0x41, 0x54, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x4D, 0x42, 0x42, 0x77, 0x4E, 0x43, 0x41, 0x41, 0x53, 0x34, 0x78, 0x6E, 0x6E, + 0x54, 0x6A, 0x32, 0x77, 0x6C, 0x44, 0x70, 0x38, 0x75, 0x4F, 0x52, 0x6B, 0x63, 0x41, 0x36, 0x53, 0x75, 0x6D, 0x75, 0x55, 0x35, 0x42, 0x77, 0x6B, 0x57, 0x0A, 0x79, 0x6D, 0x4F, 0x78, 0x75, 0x59, 0x62, 0x34, 0x69, 0x6C, 0x66, 0x42, 0x56, 0x38, + 0x35, 0x43, 0x2B, 0x6E, 0x4F, 0x68, 0x39, 0x32, 0x56, 0x43, 0x2F, 0x78, 0x37, 0x42, 0x41, 0x4C, 0x4A, 0x75, 0x63, 0x77, 0x37, 0x2F, 0x78, 0x79, 0x48, 0x6C, 0x47, 0x4B, 0x53, 0x71, 0x32, 0x58, 0x45, 0x2F, 0x71, 0x4E, 0x53, 0x35, 0x7A, 0x6F, + 0x77, 0x64, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x0A, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, + 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x56, 0x4C, 0x42, 0x37, 0x72, 0x55, 0x57, 0x34, 0x34, 0x6B, 0x42, 0x2F, 0x2B, + 0x77, 0x70, 0x75, 0x2B, 0x37, 0x34, 0x7A, 0x79, 0x54, 0x79, 0x6A, 0x68, 0x4E, 0x55, 0x77, 0x43, 0x67, 0x59, 0x49, 0x0A, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x49, 0x44, 0x52, 0x77, 0x41, 0x77, 0x52, 0x41, 0x49, 0x67, + 0x49, 0x6B, 0x39, 0x30, 0x63, 0x72, 0x6C, 0x67, 0x72, 0x2F, 0x48, 0x6D, 0x6E, 0x4B, 0x41, 0x57, 0x42, 0x56, 0x42, 0x66, 0x77, 0x31, 0x34, 0x37, 0x62, 0x6D, 0x46, 0x30, 0x37, 0x37, 0x34, 0x42, 0x78, 0x4C, 0x34, 0x59, 0x53, 0x46, 0x6C, 0x68, + 0x67, 0x6A, 0x49, 0x43, 0x49, 0x43, 0x61, 0x64, 0x56, 0x47, 0x4E, 0x41, 0x33, 0x6A, 0x64, 0x67, 0x0A, 0x55, 0x4D, 0x2F, 0x49, 0x32, 0x4F, 0x32, 0x64, 0x67, 0x71, 0x34, 0x33, 0x6D, 0x4C, 0x79, 0x6A, 0x6A, 0x30, 0x78, 0x4D, 0x71, 0x54, 0x51, + 0x72, 0x62, 0x4F, 0x2F, 0x37, 0x6C, 0x5A, 0x73, 0x6D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x54, 0x53, + 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x56, 0x7A, 0x43, 0x43, 0x41, 0x7A, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4E, 0x41, 0x67, 0x50, 0x6C, 0x6B, 0x32, 0x38, 0x78, 0x73, 0x42, 0x4E, + 0x4A, 0x69, 0x47, 0x75, 0x69, 0x46, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, + 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, + 0x51, 0x67, 0x0A, 0x55, 0x6A, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, + 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, + 0x43, 0x68, 0x4D, 0x5A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, + 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x0A, 0x43, 0x53, 0x71, + 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x32, 0x45, 0x51, 0x4B, 0x4C, 0x48, 0x75, 0x4F, + 0x68, 0x64, 0x35, 0x73, 0x37, 0x33, 0x4C, 0x2B, 0x55, 0x50, 0x72, 0x65, 0x56, 0x70, 0x30, 0x41, 0x38, 0x6F, 0x66, 0x32, 0x43, 0x2B, 0x58, 0x30, 0x79, 0x42, 0x6F, 0x4A, 0x78, 0x39, 0x76, 0x61, 0x4D, 0x0A, 0x66, 0x2F, 0x76, 0x6F, 0x32, 0x37, + 0x78, 0x71, 0x4C, 0x70, 0x65, 0x58, 0x6F, 0x34, 0x78, 0x4C, 0x2B, 0x53, 0x76, 0x32, 0x73, 0x66, 0x6E, 0x4F, 0x68, 0x42, 0x32, 0x78, 0x2B, 0x63, 0x57, 0x58, 0x33, 0x75, 0x2B, 0x35, 0x38, 0x71, 0x50, 0x70, 0x76, 0x42, 0x4B, 0x4A, 0x58, 0x71, + 0x65, 0x71, 0x55, 0x71, 0x76, 0x34, 0x49, 0x79, 0x66, 0x4C, 0x70, 0x4C, 0x47, 0x63, 0x59, 0x39, 0x76, 0x58, 0x6D, 0x58, 0x37, 0x77, 0x43, 0x6C, 0x37, 0x72, 0x61, 0x4B, 0x62, 0x30, 0x0A, 0x78, 0x6C, 0x70, 0x48, 0x44, 0x55, 0x30, 0x51, 0x4D, + 0x2B, 0x4E, 0x4F, 0x73, 0x52, 0x4F, 0x6A, 0x79, 0x42, 0x68, 0x73, 0x53, 0x2B, 0x7A, 0x38, 0x43, 0x5A, 0x44, 0x66, 0x6E, 0x57, 0x51, 0x70, 0x4A, 0x53, 0x4D, 0x48, 0x6F, 0x62, 0x54, 0x53, 0x50, 0x53, 0x35, 0x67, 0x34, 0x4D, 0x2F, 0x53, 0x43, + 0x59, 0x65, 0x37, 0x7A, 0x55, 0x6A, 0x77, 0x54, 0x63, 0x4C, 0x43, 0x65, 0x6F, 0x69, 0x4B, 0x75, 0x37, 0x72, 0x50, 0x57, 0x52, 0x6E, 0x57, 0x72, 0x34, 0x2B, 0x77, 0x0A, 0x42, 0x37, 0x43, 0x65, 0x4D, 0x66, 0x47, 0x43, 0x77, 0x63, 0x44, 0x66, + 0x4C, 0x71, 0x5A, 0x74, 0x62, 0x42, 0x6B, 0x4F, 0x74, 0x64, 0x68, 0x2B, 0x4A, 0x68, 0x70, 0x46, 0x41, 0x7A, 0x32, 0x77, 0x65, 0x61, 0x53, 0x55, 0x4B, 0x4B, 0x30, 0x50, 0x66, 0x79, 0x62, 0x6C, 0x71, 0x41, 0x6A, 0x2B, 0x6C, 0x75, 0x67, 0x38, + 0x61, 0x4A, 0x52, 0x54, 0x37, 0x6F, 0x4D, 0x36, 0x69, 0x43, 0x73, 0x56, 0x6C, 0x67, 0x6D, 0x79, 0x34, 0x48, 0x71, 0x4D, 0x4C, 0x6E, 0x58, 0x57, 0x0A, 0x6E, 0x4F, 0x75, 0x6E, 0x56, 0x6D, 0x53, 0x50, 0x6C, 0x6B, 0x39, 0x6F, 0x72, 0x6A, 0x32, + 0x58, 0x77, 0x6F, 0x53, 0x50, 0x77, 0x4C, 0x78, 0x41, 0x77, 0x41, 0x74, 0x63, 0x76, 0x66, 0x61, 0x48, 0x73, 0x7A, 0x56, 0x73, 0x72, 0x42, 0x68, 0x51, 0x66, 0x34, 0x54, 0x67, 0x54, 0x4D, 0x32, 0x53, 0x30, 0x79, 0x44, 0x70, 0x4D, 0x37, 0x78, + 0x53, 0x6D, 0x61, 0x38, 0x79, 0x74, 0x53, 0x6D, 0x7A, 0x4A, 0x53, 0x71, 0x30, 0x53, 0x50, 0x6C, 0x79, 0x34, 0x63, 0x70, 0x6B, 0x0A, 0x39, 0x2B, 0x61, 0x43, 0x45, 0x49, 0x33, 0x6F, 0x6E, 0x63, 0x4B, 0x4B, 0x69, 0x50, 0x6F, 0x34, 0x5A, 0x6F, + 0x72, 0x38, 0x59, 0x2F, 0x6B, 0x42, 0x2B, 0x58, 0x6A, 0x39, 0x65, 0x31, 0x78, 0x33, 0x2B, 0x6E, 0x61, 0x48, 0x2B, 0x75, 0x7A, 0x66, 0x73, 0x51, 0x35, 0x35, 0x6C, 0x56, 0x65, 0x30, 0x76, 0x53, 0x62, 0x76, 0x31, 0x67, 0x48, 0x52, 0x36, 0x78, + 0x59, 0x4B, 0x75, 0x34, 0x34, 0x4C, 0x74, 0x63, 0x58, 0x46, 0x69, 0x6C, 0x57, 0x72, 0x30, 0x36, 0x7A, 0x71, 0x0A, 0x6B, 0x55, 0x73, 0x70, 0x7A, 0x42, 0x6D, 0x6B, 0x4D, 0x69, 0x56, 0x4F, 0x4B, 0x76, 0x46, 0x6C, 0x52, 0x4E, 0x41, 0x43, 0x7A, + 0x71, 0x72, 0x4F, 0x53, 0x62, 0x54, 0x71, 0x6E, 0x33, 0x79, 0x44, 0x73, 0x45, 0x42, 0x37, 0x35, 0x30, 0x4F, 0x72, 0x70, 0x32, 0x79, 0x6A, 0x6A, 0x33, 0x32, 0x4A, 0x67, 0x66, 0x70, 0x4D, 0x70, 0x66, 0x2F, 0x56, 0x6A, 0x73, 0x50, 0x4F, 0x53, + 0x2B, 0x43, 0x31, 0x32, 0x4C, 0x4F, 0x4F, 0x52, 0x63, 0x39, 0x32, 0x77, 0x4F, 0x31, 0x41, 0x0A, 0x4B, 0x2F, 0x31, 0x54, 0x44, 0x37, 0x43, 0x6E, 0x31, 0x54, 0x73, 0x4E, 0x73, 0x59, 0x71, 0x69, 0x41, 0x39, 0x34, 0x78, 0x72, 0x63, 0x78, 0x33, + 0x36, 0x6D, 0x39, 0x37, 0x50, 0x74, 0x62, 0x66, 0x6B, 0x53, 0x49, 0x53, 0x35, 0x72, 0x37, 0x36, 0x32, 0x44, 0x4C, 0x38, 0x45, 0x47, 0x4D, 0x55, 0x55, 0x58, 0x4C, 0x65, 0x58, 0x64, 0x59, 0x57, 0x6B, 0x37, 0x30, 0x70, 0x61, 0x44, 0x50, 0x76, + 0x4F, 0x6D, 0x62, 0x73, 0x42, 0x34, 0x6F, 0x6D, 0x33, 0x78, 0x50, 0x58, 0x0A, 0x56, 0x32, 0x56, 0x34, 0x4A, 0x39, 0x35, 0x65, 0x53, 0x52, 0x51, 0x41, 0x6F, 0x67, 0x42, 0x2F, 0x6D, 0x71, 0x67, 0x68, 0x74, 0x71, 0x6D, 0x78, 0x6C, 0x62, 0x43, + 0x6C, 0x75, 0x51, 0x30, 0x57, 0x45, 0x64, 0x72, 0x48, 0x62, 0x45, 0x67, 0x38, 0x51, 0x4F, 0x42, 0x2B, 0x44, 0x56, 0x72, 0x4E, 0x56, 0x6A, 0x7A, 0x52, 0x6C, 0x77, 0x57, 0x35, 0x79, 0x30, 0x76, 0x74, 0x4F, 0x55, 0x75, 0x63, 0x78, 0x44, 0x2F, + 0x53, 0x56, 0x52, 0x4E, 0x75, 0x4A, 0x4C, 0x44, 0x57, 0x0A, 0x63, 0x66, 0x72, 0x30, 0x77, 0x62, 0x72, 0x4D, 0x37, 0x52, 0x76, 0x31, 0x2F, 0x6F, 0x46, 0x42, 0x32, 0x41, 0x43, 0x59, 0x50, 0x54, 0x72, 0x49, 0x72, 0x6E, 0x71, 0x59, 0x4E, 0x78, + 0x67, 0x46, 0x6C, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, + 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x35, 0x4B, 0x38, 0x72, 0x4A, + 0x6E, 0x45, 0x61, 0x4B, 0x30, 0x67, 0x6E, 0x68, 0x53, 0x39, 0x53, 0x5A, 0x69, 0x7A, 0x76, 0x38, 0x49, 0x6B, 0x54, 0x63, 0x54, 0x34, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, + 0x51, 0x41, 0x44, 0x0A, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4A, 0x2B, 0x71, 0x51, 0x69, 0x62, 0x62, 0x43, 0x35, 0x75, 0x2B, 0x2F, 0x78, 0x36, 0x57, 0x6B, 0x69, 0x34, 0x2B, 0x6F, 0x6D, 0x56, 0x4B, 0x61, 0x70, 0x69, 0x36, 0x49, 0x73, 0x74, 0x39, + 0x77, 0x54, 0x72, 0x59, 0x67, 0x67, 0x6F, 0x47, 0x78, 0x76, 0x61, 0x6C, 0x33, 0x73, 0x42, 0x4F, 0x68, 0x32, 0x5A, 0x35, 0x6F, 0x66, 0x6D, 0x6D, 0x57, 0x4A, 0x79, 0x71, 0x2B, 0x62, 0x58, 0x6D, 0x59, 0x4F, 0x66, 0x67, 0x36, 0x4C, 0x45, 0x65, + 0x0A, 0x51, 0x6B, 0x45, 0x7A, 0x43, 0x7A, 0x63, 0x39, 0x7A, 0x6F, 0x6C, 0x77, 0x46, 0x63, 0x71, 0x31, 0x4A, 0x4B, 0x6A, 0x50, 0x61, 0x37, 0x58, 0x53, 0x51, 0x43, 0x47, 0x59, 0x7A, 0x79, 0x49, 0x30, 0x7A, 0x7A, 0x76, 0x46, 0x49, 0x6F, 0x54, + 0x67, 0x78, 0x51, 0x36, 0x4B, 0x66, 0x46, 0x32, 0x49, 0x35, 0x44, 0x55, 0x6B, 0x7A, 0x70, 0x73, 0x2B, 0x47, 0x6C, 0x51, 0x65, 0x62, 0x74, 0x75, 0x79, 0x68, 0x36, 0x66, 0x38, 0x38, 0x2F, 0x71, 0x42, 0x56, 0x52, 0x52, 0x69, 0x0A, 0x43, 0x6C, + 0x6D, 0x70, 0x49, 0x67, 0x55, 0x78, 0x50, 0x6F, 0x4C, 0x57, 0x37, 0x74, 0x74, 0x58, 0x4E, 0x4C, 0x77, 0x7A, 0x6C, 0x64, 0x4D, 0x58, 0x47, 0x2B, 0x67, 0x6E, 0x6F, 0x6F, 0x74, 0x37, 0x54, 0x69, 0x59, 0x61, 0x65, 0x6C, 0x70, 0x6B, 0x74, 0x74, + 0x47, 0x73, 0x4E, 0x2F, 0x48, 0x39, 0x6F, 0x50, 0x4D, 0x34, 0x37, 0x48, 0x4C, 0x77, 0x45, 0x58, 0x57, 0x64, 0x79, 0x7A, 0x52, 0x53, 0x6A, 0x65, 0x5A, 0x32, 0x61, 0x78, 0x66, 0x47, 0x33, 0x34, 0x61, 0x72, 0x0A, 0x4A, 0x34, 0x35, 0x4A, 0x4B, + 0x33, 0x56, 0x6D, 0x67, 0x52, 0x41, 0x68, 0x70, 0x75, 0x6F, 0x2B, 0x39, 0x4B, 0x34, 0x6C, 0x2F, 0x33, 0x77, 0x56, 0x33, 0x73, 0x36, 0x4D, 0x4A, 0x54, 0x2F, 0x4B, 0x59, 0x6E, 0x41, 0x4B, 0x39, 0x79, 0x38, 0x4A, 0x5A, 0x67, 0x66, 0x49, 0x50, + 0x78, 0x7A, 0x38, 0x38, 0x4E, 0x74, 0x46, 0x4D, 0x4E, 0x39, 0x69, 0x69, 0x4D, 0x47, 0x31, 0x44, 0x35, 0x33, 0x44, 0x6E, 0x30, 0x72, 0x65, 0x57, 0x56, 0x6C, 0x48, 0x78, 0x59, 0x63, 0x69, 0x0A, 0x4E, 0x75, 0x61, 0x43, 0x70, 0x2B, 0x30, 0x4B, + 0x75, 0x65, 0x49, 0x48, 0x6F, 0x49, 0x31, 0x37, 0x65, 0x6B, 0x6F, 0x38, 0x63, 0x64, 0x4C, 0x69, 0x41, 0x36, 0x45, 0x66, 0x4D, 0x67, 0x66, 0x64, 0x47, 0x2B, 0x52, 0x43, 0x7A, 0x67, 0x77, 0x41, 0x52, 0x57, 0x47, 0x41, 0x74, 0x51, 0x73, 0x67, + 0x57, 0x53, 0x6C, 0x34, 0x76, 0x66, 0x6C, 0x56, 0x79, 0x32, 0x50, 0x46, 0x50, 0x45, 0x7A, 0x30, 0x74, 0x76, 0x2F, 0x62, 0x61, 0x6C, 0x38, 0x78, 0x61, 0x35, 0x6D, 0x65, 0x0A, 0x4C, 0x4D, 0x46, 0x72, 0x55, 0x4B, 0x54, 0x58, 0x35, 0x68, 0x67, + 0x55, 0x76, 0x59, 0x55, 0x2F, 0x5A, 0x36, 0x74, 0x47, 0x6E, 0x36, 0x44, 0x2F, 0x51, 0x71, 0x63, 0x36, 0x66, 0x31, 0x7A, 0x4C, 0x58, 0x62, 0x42, 0x77, 0x48, 0x53, 0x73, 0x30, 0x39, 0x64, 0x52, 0x32, 0x43, 0x51, 0x7A, 0x72, 0x65, 0x45, 0x78, + 0x5A, 0x42, 0x66, 0x4D, 0x7A, 0x51, 0x73, 0x4E, 0x68, 0x46, 0x52, 0x41, 0x62, 0x64, 0x30, 0x33, 0x4F, 0x49, 0x6F, 0x7A, 0x55, 0x68, 0x66, 0x4A, 0x46, 0x0A, 0x66, 0x62, 0x64, 0x54, 0x36, 0x75, 0x39, 0x41, 0x57, 0x70, 0x51, 0x4B, 0x58, 0x43, + 0x42, 0x66, 0x54, 0x6B, 0x42, 0x64, 0x59, 0x69, 0x4A, 0x32, 0x33, 0x2F, 0x2F, 0x4F, 0x59, 0x62, 0x32, 0x4D, 0x49, 0x33, 0x6A, 0x53, 0x4E, 0x77, 0x4C, 0x67, 0x6A, 0x74, 0x37, 0x52, 0x45, 0x54, 0x65, 0x4A, 0x39, 0x72, 0x2F, 0x74, 0x53, 0x51, + 0x64, 0x69, 0x72, 0x70, 0x4C, 0x73, 0x51, 0x42, 0x71, 0x76, 0x46, 0x41, 0x6E, 0x5A, 0x30, 0x45, 0x36, 0x79, 0x6F, 0x76, 0x65, 0x2B, 0x0A, 0x37, 0x75, 0x37, 0x59, 0x2F, 0x39, 0x77, 0x61, 0x4C, 0x64, 0x36, 0x34, 0x4E, 0x6E, 0x48, 0x69, 0x2F, + 0x48, 0x6D, 0x33, 0x6C, 0x43, 0x58, 0x52, 0x53, 0x48, 0x4E, 0x62, 0x6F, 0x54, 0x58, 0x6E, 0x73, 0x35, 0x6C, 0x6E, 0x64, 0x63, 0x45, 0x5A, 0x4F, 0x69, 0x74, 0x48, 0x54, 0x74, 0x4E, 0x43, 0x6A, 0x76, 0x30, 0x78, 0x79, 0x42, 0x5A, 0x6D, 0x32, + 0x74, 0x49, 0x4D, 0x50, 0x4E, 0x75, 0x7A, 0x6A, 0x73, 0x6D, 0x68, 0x44, 0x59, 0x41, 0x50, 0x65, 0x78, 0x5A, 0x33, 0x0A, 0x46, 0x4C, 0x2F, 0x2F, 0x32, 0x77, 0x6D, 0x55, 0x73, 0x70, 0x4F, 0x38, 0x49, 0x46, 0x67, 0x56, 0x36, 0x64, 0x74, 0x78, + 0x51, 0x2F, 0x50, 0x65, 0x45, 0x4D, 0x4D, 0x41, 0x33, 0x4B, 0x67, 0x71, 0x6C, 0x62, 0x62, 0x43, 0x31, 0x6A, 0x2B, 0x51, 0x61, 0x33, 0x62, 0x62, 0x62, 0x50, 0x36, 0x4D, 0x76, 0x50, 0x4A, 0x77, 0x4E, 0x51, 0x7A, 0x63, 0x6D, 0x52, 0x6B, 0x31, + 0x33, 0x4E, 0x66, 0x49, 0x52, 0x6D, 0x50, 0x56, 0x4E, 0x6E, 0x47, 0x75, 0x56, 0x2F, 0x75, 0x33, 0x0A, 0x67, 0x6D, 0x33, 0x63, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x54, 0x53, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x56, 0x7A, 0x43, 0x43, 0x41, 0x7A, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x4E, 0x41, 0x67, 0x50, 0x6C, 0x72, 0x73, 0x57, 0x4E, 0x42, 0x43, 0x55, 0x61, 0x71, 0x78, 0x45, 0x6C, 0x71, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, + 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, + 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, + 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x0A, 0x55, 0x6A, 0x49, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, + 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, + 0x7A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x43, 0x68, 0x4D, 0x5A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, + 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x49, 0x77, + 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x0A, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, + 0x43, 0x41, 0x51, 0x44, 0x4F, 0x33, 0x76, 0x32, 0x6D, 0x2B, 0x2B, 0x7A, 0x73, 0x46, 0x44, 0x51, 0x38, 0x42, 0x77, 0x5A, 0x61, 0x62, 0x46, 0x6E, 0x33, 0x47, 0x54, 0x58, 0x64, 0x39, 0x38, 0x47, 0x64, 0x56, 0x61, 0x72, 0x54, 0x7A, 0x54, 0x75, + 0x6B, 0x6B, 0x33, 0x4C, 0x76, 0x0A, 0x43, 0x76, 0x70, 0x74, 0x6E, 0x66, 0x62, 0x77, 0x68, 0x59, 0x42, 0x62, 0x6F, 0x55, 0x68, 0x53, 0x6E, 0x7A, 0x6E, 0x46, 0x74, 0x2B, 0x34, 0x6F, 0x72, 0x4F, 0x2F, 0x4C, 0x64, 0x6D, 0x67, 0x55, 0x75, 0x64, + 0x2B, 0x74, 0x41, 0x57, 0x79, 0x5A, 0x48, 0x38, 0x51, 0x69, 0x48, 0x5A, 0x2F, 0x2B, 0x63, 0x6E, 0x66, 0x67, 0x4C, 0x46, 0x75, 0x76, 0x35, 0x41, 0x53, 0x2F, 0x54, 0x33, 0x4B, 0x67, 0x47, 0x6A, 0x53, 0x59, 0x36, 0x44, 0x6C, 0x6F, 0x37, 0x4A, + 0x55, 0x6C, 0x0A, 0x65, 0x33, 0x61, 0x68, 0x35, 0x6D, 0x6D, 0x35, 0x68, 0x52, 0x6D, 0x39, 0x69, 0x59, 0x7A, 0x2B, 0x72, 0x65, 0x30, 0x32, 0x36, 0x6E, 0x4F, 0x38, 0x2F, 0x34, 0x50, 0x69, 0x79, 0x33, 0x33, 0x42, 0x30, 0x73, 0x35, 0x4B, 0x73, + 0x34, 0x30, 0x46, 0x6E, 0x6F, 0x74, 0x4A, 0x6B, 0x39, 0x2F, 0x42, 0x57, 0x39, 0x42, 0x75, 0x58, 0x76, 0x41, 0x75, 0x4D, 0x43, 0x36, 0x43, 0x2F, 0x50, 0x71, 0x38, 0x74, 0x42, 0x63, 0x4B, 0x53, 0x4F, 0x57, 0x49, 0x6D, 0x38, 0x57, 0x62, 0x0A, + 0x61, 0x39, 0x36, 0x77, 0x79, 0x72, 0x51, 0x44, 0x38, 0x4E, 0x72, 0x30, 0x6B, 0x4C, 0x68, 0x6C, 0x5A, 0x50, 0x64, 0x63, 0x54, 0x4B, 0x33, 0x6F, 0x66, 0x6D, 0x5A, 0x65, 0x6D, 0x64, 0x65, 0x34, 0x77, 0x6A, 0x37, 0x49, 0x30, 0x42, 0x4F, 0x64, + 0x72, 0x65, 0x37, 0x6B, 0x52, 0x58, 0x75, 0x4A, 0x56, 0x66, 0x65, 0x4B, 0x48, 0x32, 0x4A, 0x53, 0x68, 0x42, 0x4B, 0x7A, 0x77, 0x6B, 0x43, 0x58, 0x34, 0x34, 0x6F, 0x66, 0x52, 0x35, 0x47, 0x6D, 0x64, 0x46, 0x72, 0x53, 0x0A, 0x2B, 0x4C, 0x46, + 0x6A, 0x4B, 0x42, 0x43, 0x34, 0x73, 0x77, 0x6D, 0x34, 0x56, 0x6E, 0x64, 0x41, 0x6F, 0x69, 0x61, 0x59, 0x65, 0x63, 0x62, 0x2B, 0x33, 0x79, 0x58, 0x75, 0x50, 0x75, 0x57, 0x67, 0x66, 0x39, 0x52, 0x68, 0x44, 0x31, 0x46, 0x4C, 0x50, 0x44, 0x2B, + 0x4D, 0x32, 0x75, 0x46, 0x77, 0x64, 0x4E, 0x6A, 0x43, 0x61, 0x4B, 0x48, 0x35, 0x77, 0x51, 0x7A, 0x70, 0x6F, 0x65, 0x4A, 0x2F, 0x75, 0x31, 0x55, 0x38, 0x64, 0x67, 0x62, 0x75, 0x61, 0x6B, 0x37, 0x4D, 0x0A, 0x6B, 0x6F, 0x67, 0x77, 0x54, 0x5A, + 0x71, 0x39, 0x54, 0x77, 0x74, 0x49, 0x6D, 0x6F, 0x53, 0x31, 0x6D, 0x4B, 0x50, 0x56, 0x2B, 0x33, 0x50, 0x42, 0x56, 0x32, 0x48, 0x64, 0x4B, 0x46, 0x5A, 0x31, 0x45, 0x36, 0x36, 0x48, 0x6A, 0x75, 0x63, 0x4D, 0x55, 0x51, 0x6B, 0x51, 0x64, 0x59, + 0x68, 0x4D, 0x76, 0x49, 0x33, 0x35, 0x65, 0x7A, 0x7A, 0x55, 0x49, 0x6B, 0x67, 0x66, 0x4B, 0x74, 0x7A, 0x72, 0x61, 0x37, 0x74, 0x45, 0x73, 0x63, 0x73, 0x7A, 0x63, 0x54, 0x4A, 0x47, 0x0A, 0x72, 0x36, 0x31, 0x4B, 0x38, 0x59, 0x7A, 0x6F, 0x64, + 0x44, 0x71, 0x73, 0x35, 0x78, 0x6F, 0x69, 0x63, 0x34, 0x44, 0x53, 0x4D, 0x50, 0x63, 0x6C, 0x51, 0x73, 0x63, 0x69, 0x4F, 0x7A, 0x73, 0x53, 0x72, 0x5A, 0x59, 0x75, 0x78, 0x73, 0x4E, 0x32, 0x42, 0x36, 0x6F, 0x67, 0x74, 0x7A, 0x56, 0x4A, 0x56, + 0x2B, 0x6D, 0x53, 0x53, 0x65, 0x68, 0x32, 0x46, 0x6E, 0x49, 0x78, 0x5A, 0x79, 0x75, 0x57, 0x66, 0x6F, 0x71, 0x6A, 0x78, 0x35, 0x52, 0x57, 0x49, 0x72, 0x39, 0x71, 0x0A, 0x53, 0x33, 0x34, 0x42, 0x49, 0x62, 0x49, 0x6A, 0x4D, 0x74, 0x2F, 0x6B, + 0x6D, 0x6B, 0x52, 0x74, 0x57, 0x56, 0x74, 0x64, 0x39, 0x51, 0x43, 0x67, 0x48, 0x4A, 0x76, 0x47, 0x65, 0x4A, 0x65, 0x4E, 0x6B, 0x50, 0x2B, 0x62, 0x79, 0x4B, 0x71, 0x30, 0x72, 0x78, 0x46, 0x52, 0x4F, 0x56, 0x37, 0x5A, 0x2B, 0x32, 0x65, 0x74, + 0x31, 0x56, 0x73, 0x52, 0x6E, 0x54, 0x4B, 0x61, 0x47, 0x37, 0x33, 0x56, 0x75, 0x6C, 0x75, 0x6C, 0x79, 0x63, 0x73, 0x6C, 0x61, 0x56, 0x4E, 0x56, 0x0A, 0x4A, 0x31, 0x7A, 0x67, 0x79, 0x6A, 0x62, 0x4C, 0x69, 0x47, 0x48, 0x37, 0x48, 0x72, 0x66, + 0x51, 0x79, 0x2B, 0x34, 0x57, 0x2B, 0x39, 0x4F, 0x6D, 0x54, 0x4E, 0x36, 0x53, 0x70, 0x64, 0x54, 0x69, 0x33, 0x2F, 0x55, 0x47, 0x56, 0x4E, 0x34, 0x75, 0x6E, 0x55, 0x75, 0x30, 0x6B, 0x7A, 0x43, 0x71, 0x67, 0x63, 0x37, 0x64, 0x47, 0x74, 0x78, + 0x52, 0x63, 0x77, 0x31, 0x50, 0x63, 0x4F, 0x6E, 0x6C, 0x74, 0x68, 0x59, 0x68, 0x47, 0x58, 0x6D, 0x79, 0x35, 0x6F, 0x6B, 0x4C, 0x0A, 0x64, 0x57, 0x54, 0x4B, 0x31, 0x61, 0x75, 0x38, 0x43, 0x63, 0x45, 0x59, 0x6F, 0x66, 0x2F, 0x55, 0x56, 0x4B, + 0x47, 0x46, 0x50, 0x50, 0x30, 0x55, 0x4A, 0x41, 0x4F, 0x79, 0x68, 0x39, 0x4F, 0x6B, 0x74, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, + 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, + 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x75, 0x2F, 0x2F, 0x4B, 0x6A, 0x69, 0x4F, 0x66, 0x54, 0x35, 0x6E, 0x4B, 0x32, 0x2B, 0x4A, 0x6F, 0x70, 0x71, 0x55, 0x56, 0x4A, 0x78, 0x63, 0x65, 0x32, 0x51, 0x34, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, + 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x0A, 0x67, 0x67, 0x49, 0x42, 0x41, 0x42, 0x2F, 0x4B, 0x7A, 0x74, 0x33, 0x48, 0x76, 0x71, 0x47, 0x66, 0x32, 0x53, 0x64, 0x4D, 0x43, 0x39, 0x77, 0x58, + 0x6D, 0x42, 0x46, 0x71, 0x69, 0x4E, 0x34, 0x39, 0x35, 0x6E, 0x46, 0x57, 0x63, 0x72, 0x4B, 0x65, 0x47, 0x6B, 0x36, 0x63, 0x31, 0x53, 0x75, 0x59, 0x4A, 0x46, 0x32, 0x62, 0x61, 0x33, 0x75, 0x77, 0x4D, 0x34, 0x49, 0x4A, 0x76, 0x64, 0x38, 0x6C, + 0x52, 0x75, 0x71, 0x59, 0x6E, 0x72, 0x59, 0x62, 0x2F, 0x6F, 0x4D, 0x38, 0x0A, 0x30, 0x6D, 0x4A, 0x68, 0x77, 0x51, 0x54, 0x74, 0x7A, 0x75, 0x44, 0x46, 0x79, 0x63, 0x67, 0x54, 0x45, 0x31, 0x58, 0x6E, 0x71, 0x47, 0x4F, 0x74, 0x6A, 0x48, 0x73, + 0x42, 0x2F, 0x6E, 0x63, 0x77, 0x34, 0x63, 0x35, 0x6F, 0x6D, 0x77, 0x58, 0x34, 0x45, 0x75, 0x35, 0x35, 0x4D, 0x61, 0x42, 0x42, 0x52, 0x54, 0x55, 0x6F, 0x43, 0x6E, 0x47, 0x6B, 0x4A, 0x45, 0x2B, 0x4D, 0x33, 0x44, 0x79, 0x43, 0x42, 0x31, 0x39, + 0x6D, 0x33, 0x48, 0x30, 0x51, 0x2F, 0x67, 0x78, 0x68, 0x0A, 0x73, 0x77, 0x57, 0x56, 0x37, 0x75, 0x47, 0x75, 0x67, 0x51, 0x2B, 0x6F, 0x2B, 0x4D, 0x65, 0x50, 0x54, 0x61, 0x67, 0x6A, 0x41, 0x69, 0x5A, 0x72, 0x48, 0x59, 0x4E, 0x53, 0x56, 0x63, + 0x36, 0x31, 0x4C, 0x77, 0x44, 0x4B, 0x67, 0x45, 0x44, 0x67, 0x34, 0x58, 0x53, 0x73, 0x59, 0x50, 0x57, 0x48, 0x67, 0x4A, 0x32, 0x75, 0x4E, 0x6D, 0x53, 0x52, 0x58, 0x62, 0x42, 0x6F, 0x47, 0x4F, 0x71, 0x4B, 0x59, 0x63, 0x6C, 0x33, 0x71, 0x4A, + 0x66, 0x45, 0x79, 0x63, 0x65, 0x6C, 0x0A, 0x2F, 0x46, 0x56, 0x4C, 0x38, 0x2F, 0x42, 0x2F, 0x75, 0x57, 0x55, 0x39, 0x4A, 0x32, 0x6A, 0x51, 0x7A, 0x47, 0x76, 0x36, 0x55, 0x35, 0x33, 0x68, 0x6B, 0x52, 0x72, 0x4A, 0x58, 0x52, 0x71, 0x57, 0x62, + 0x54, 0x4B, 0x48, 0x37, 0x51, 0x4D, 0x67, 0x79, 0x41, 0x4C, 0x4F, 0x57, 0x72, 0x37, 0x5A, 0x36, 0x76, 0x32, 0x79, 0x54, 0x63, 0x51, 0x76, 0x47, 0x39, 0x39, 0x66, 0x65, 0x76, 0x58, 0x34, 0x69, 0x38, 0x62, 0x75, 0x4D, 0x54, 0x6F, 0x6C, 0x55, + 0x56, 0x56, 0x6E, 0x0A, 0x6A, 0x57, 0x51, 0x79, 0x65, 0x2B, 0x6D, 0x65, 0x77, 0x34, 0x4B, 0x36, 0x4B, 0x69, 0x33, 0x70, 0x48, 0x72, 0x54, 0x67, 0x53, 0x41, 0x61, 0x69, 0x2F, 0x47, 0x65, 0x76, 0x48, 0x79, 0x49, 0x43, 0x63, 0x2F, 0x73, 0x67, + 0x43, 0x71, 0x2B, 0x64, 0x56, 0x45, 0x75, 0x68, 0x7A, 0x66, 0x39, 0x67, 0x52, 0x37, 0x41, 0x2F, 0x58, 0x65, 0x38, 0x62, 0x56, 0x72, 0x32, 0x58, 0x49, 0x5A, 0x59, 0x74, 0x43, 0x74, 0x46, 0x65, 0x6E, 0x54, 0x67, 0x43, 0x52, 0x32, 0x79, 0x35, + 0x0A, 0x39, 0x50, 0x59, 0x6A, 0x4A, 0x62, 0x69, 0x67, 0x61, 0x70, 0x6F, 0x72, 0x64, 0x77, 0x6A, 0x36, 0x78, 0x4C, 0x45, 0x6F, 0x6B, 0x43, 0x5A, 0x59, 0x43, 0x44, 0x7A, 0x69, 0x66, 0x71, 0x72, 0x58, 0x50, 0x57, 0x2B, 0x36, 0x4D, 0x59, 0x67, + 0x4B, 0x42, 0x65, 0x73, 0x6E, 0x74, 0x61, 0x46, 0x4A, 0x37, 0x71, 0x42, 0x46, 0x56, 0x48, 0x76, 0x6D, 0x4A, 0x32, 0x57, 0x5A, 0x49, 0x43, 0x47, 0x6F, 0x6F, 0x37, 0x7A, 0x37, 0x47, 0x4A, 0x61, 0x37, 0x55, 0x6D, 0x38, 0x4D, 0x0A, 0x37, 0x59, + 0x4E, 0x52, 0x54, 0x4F, 0x6C, 0x5A, 0x34, 0x69, 0x42, 0x67, 0x78, 0x63, 0x4A, 0x6C, 0x6B, 0x6F, 0x4B, 0x4D, 0x38, 0x78, 0x41, 0x66, 0x44, 0x6F, 0x71, 0x58, 0x76, 0x6E, 0x65, 0x43, 0x62, 0x54, 0x2B, 0x50, 0x48, 0x56, 0x32, 0x38, 0x53, 0x53, + 0x65, 0x39, 0x7A, 0x45, 0x38, 0x50, 0x34, 0x63, 0x35, 0x32, 0x68, 0x67, 0x51, 0x6A, 0x78, 0x63, 0x43, 0x4D, 0x45, 0x6C, 0x76, 0x39, 0x32, 0x34, 0x53, 0x67, 0x4A, 0x50, 0x46, 0x49, 0x2F, 0x32, 0x52, 0x38, 0x0A, 0x30, 0x4C, 0x35, 0x63, 0x46, + 0x74, 0x48, 0x76, 0x6D, 0x61, 0x33, 0x41, 0x48, 0x2F, 0x76, 0x4C, 0x72, 0x72, 0x77, 0x34, 0x49, 0x67, 0x59, 0x6D, 0x5A, 0x4E, 0x72, 0x61, 0x6C, 0x77, 0x34, 0x2F, 0x4B, 0x42, 0x56, 0x45, 0x71, 0x45, 0x38, 0x41, 0x79, 0x76, 0x43, 0x61, 0x7A, + 0x4D, 0x39, 0x30, 0x61, 0x72, 0x51, 0x2B, 0x50, 0x4F, 0x75, 0x56, 0x37, 0x4C, 0x58, 0x54, 0x57, 0x74, 0x69, 0x42, 0x6D, 0x65, 0x6C, 0x44, 0x47, 0x44, 0x66, 0x72, 0x73, 0x37, 0x76, 0x52, 0x0A, 0x57, 0x47, 0x4A, 0x42, 0x38, 0x32, 0x62, 0x53, + 0x6A, 0x36, 0x70, 0x34, 0x6C, 0x56, 0x51, 0x67, 0x77, 0x31, 0x6F, 0x75, 0x64, 0x43, 0x76, 0x56, 0x30, 0x62, 0x34, 0x59, 0x61, 0x63, 0x43, 0x73, 0x31, 0x61, 0x54, 0x50, 0x4F, 0x62, 0x70, 0x52, 0x68, 0x41, 0x4E, 0x6C, 0x36, 0x57, 0x4C, 0x41, + 0x59, 0x76, 0x37, 0x59, 0x54, 0x56, 0x57, 0x57, 0x34, 0x74, 0x41, 0x52, 0x2B, 0x6B, 0x67, 0x30, 0x45, 0x65, 0x79, 0x65, 0x37, 0x51, 0x55, 0x64, 0x35, 0x4D, 0x6A, 0x57, 0x0A, 0x48, 0x59, 0x62, 0x4C, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, + 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x54, 0x53, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x54, 0x43, + 0x43, 0x41, 0x59, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4E, 0x41, 0x67, 0x50, 0x6C, 0x75, 0x49, 0x4C, 0x72, 0x49, 0x50, 0x67, 0x6C, 0x4A, 0x32, 0x30, 0x39, 0x5A, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, + 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x69, 0x0A, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, + 0x4D, 0x5A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, + 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x4D, 0x77, 0x0A, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, + 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4D, 0x7A, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, + 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, 0x0A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, + 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, + 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x4D, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x0A, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x51, + 0x66, 0x54, 0x7A, 0x4F, 0x48, 0x4D, 0x79, 0x6D, 0x4B, 0x6F, 0x59, 0x54, 0x65, 0x79, 0x38, 0x63, 0x68, 0x57, 0x45, 0x47, 0x4A, 0x36, 0x6C, 0x61, 0x64, 0x4B, 0x30, 0x75, 0x46, 0x78, 0x68, 0x31, 0x4D, 0x4A, 0x37, 0x78, 0x2F, 0x4A, 0x6C, 0x46, + 0x79, 0x62, 0x2B, 0x4B, 0x66, 0x31, 0x71, 0x50, 0x4B, 0x7A, 0x45, 0x55, 0x55, 0x52, 0x6F, 0x75, 0x74, 0x0A, 0x37, 0x33, 0x36, 0x47, 0x6A, 0x4F, 0x79, 0x78, 0x66, 0x69, 0x2F, 0x2F, 0x71, 0x58, 0x47, 0x64, 0x47, 0x49, 0x52, 0x46, 0x42, 0x45, + 0x46, 0x56, 0x62, 0x69, 0x76, 0x71, 0x4A, 0x6E, 0x2B, 0x37, 0x6B, 0x41, 0x48, 0x6A, 0x53, 0x78, 0x6D, 0x36, 0x35, 0x46, 0x53, 0x57, 0x52, 0x51, 0x6D, 0x78, 0x31, 0x57, 0x79, 0x52, 0x52, 0x4B, 0x32, 0x45, 0x45, 0x34, 0x36, 0x61, 0x6A, 0x41, + 0x32, 0x41, 0x44, 0x44, 0x4C, 0x32, 0x34, 0x43, 0x65, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x0A, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x50, 0x42, + 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x42, 0x38, 0x53, 0x61, 0x36, 0x6F, + 0x43, 0x32, 0x75, 0x68, 0x59, 0x48, 0x50, 0x30, 0x2F, 0x45, 0x71, 0x0A, 0x45, 0x72, 0x32, 0x34, 0x43, 0x6D, 0x66, 0x39, 0x76, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x70, + 0x41, 0x44, 0x42, 0x6D, 0x41, 0x6A, 0x45, 0x41, 0x39, 0x75, 0x45, 0x67, 0x6C, 0x52, 0x52, 0x37, 0x56, 0x4B, 0x4F, 0x51, 0x46, 0x68, 0x47, 0x2F, 0x68, 0x4D, 0x6A, 0x71, 0x62, 0x32, 0x73, 0x58, 0x6E, 0x68, 0x35, 0x47, 0x6D, 0x43, 0x43, 0x62, + 0x6E, 0x39, 0x4D, 0x4E, 0x32, 0x61, 0x7A, 0x54, 0x0A, 0x4C, 0x38, 0x31, 0x38, 0x2B, 0x46, 0x73, 0x75, 0x56, 0x62, 0x75, 0x2F, 0x33, 0x5A, 0x4C, 0x33, 0x70, 0x41, 0x7A, 0x63, 0x4D, 0x65, 0x47, 0x69, 0x41, 0x6A, 0x45, 0x41, 0x2F, 0x4A, 0x64, + 0x6D, 0x5A, 0x75, 0x56, 0x44, 0x46, 0x68, 0x4F, 0x44, 0x33, 0x63, 0x66, 0x66, 0x4C, 0x37, 0x34, 0x55, 0x4F, 0x4F, 0x30, 0x42, 0x7A, 0x72, 0x45, 0x58, 0x47, 0x68, 0x46, 0x31, 0x36, 0x62, 0x30, 0x44, 0x6A, 0x79, 0x5A, 0x2B, 0x68, 0x4F, 0x58, + 0x4A, 0x59, 0x4B, 0x61, 0x56, 0x0A, 0x31, 0x31, 0x52, 0x5A, 0x74, 0x2B, 0x63, 0x52, 0x4C, 0x49, 0x6E, 0x55, 0x75, 0x65, 0x34, 0x58, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x47, 0x54, 0x53, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x34, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, + 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x54, 0x43, 0x43, 0x41, 0x59, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x4E, 0x41, 0x67, 0x50, 0x6C, 0x77, 0x47, 0x6A, 0x76, 0x59, 0x78, 0x71, 0x63, 0x63, 0x70, 0x42, 0x51, 0x55, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x48, 0x4D, 0x51, + 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x69, 0x0A, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, + 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, + 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x51, 0x77, 0x0A, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, + 0x4D, 0x7A, 0x59, 0x77, 0x4E, 0x6A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x42, 0x48, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x69, + 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x5A, 0x0A, 0x52, 0x32, 0x39, 0x76, 0x5A, 0x32, 0x78, 0x6C, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x70, 0x59, 0x32, 0x56, + 0x7A, 0x49, 0x45, 0x78, 0x4D, 0x51, 0x7A, 0x45, 0x55, 0x4D, 0x42, 0x49, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x4C, 0x52, 0x31, 0x52, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x51, 0x77, 0x64, 0x6A, 0x41, + 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x0A, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x54, 0x7A, 0x64, 0x48, 0x4F, 0x6E, 0x61, 0x49, 0x74, 0x67, 0x72, 0x6B, + 0x4F, 0x34, 0x4E, 0x63, 0x57, 0x42, 0x4D, 0x48, 0x74, 0x4C, 0x53, 0x5A, 0x33, 0x37, 0x77, 0x57, 0x48, 0x4F, 0x35, 0x74, 0x35, 0x47, 0x76, 0x57, 0x76, 0x56, 0x59, 0x52, 0x67, 0x31, 0x72, 0x6B, 0x44, 0x64, 0x63, 0x2F, 0x65, 0x4A, 0x6B, 0x54, + 0x42, 0x61, 0x36, 0x7A, 0x7A, 0x75, 0x0A, 0x68, 0x58, 0x79, 0x69, 0x51, 0x48, 0x59, 0x37, 0x71, 0x63, 0x61, 0x34, 0x52, 0x39, 0x67, 0x71, 0x35, 0x35, 0x4B, 0x52, 0x61, 0x6E, 0x50, 0x70, 0x73, 0x58, 0x49, 0x35, 0x6E, 0x79, 0x6D, 0x66, 0x6F, + 0x70, 0x6A, 0x54, 0x58, 0x31, 0x35, 0x59, 0x68, 0x6D, 0x55, 0x50, 0x6F, 0x59, 0x52, 0x6C, 0x42, 0x74, 0x48, 0x63, 0x69, 0x38, 0x6E, 0x48, 0x63, 0x38, 0x69, 0x4D, 0x61, 0x69, 0x2F, 0x6C, 0x78, 0x4B, 0x76, 0x52, 0x48, 0x59, 0x71, 0x6A, 0x51, + 0x6A, 0x42, 0x41, 0x0A, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, + 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x53, 0x41, 0x54, 0x4E, 0x62, 0x72, 0x64, 0x50, 0x39, 0x4A, 0x4E, 0x71, 0x50, 0x56, 0x32, 0x50, 0x79, 0x31, + 0x0A, 0x50, 0x73, 0x56, 0x71, 0x38, 0x4A, 0x51, 0x64, 0x6A, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x70, 0x41, 0x44, 0x42, 0x6D, 0x41, 0x6A, 0x45, 0x41, 0x36, 0x45, 0x44, + 0x2F, 0x67, 0x39, 0x34, 0x44, 0x39, 0x4A, 0x2B, 0x75, 0x48, 0x58, 0x71, 0x6E, 0x4C, 0x72, 0x6D, 0x76, 0x54, 0x2F, 0x61, 0x44, 0x48, 0x51, 0x34, 0x74, 0x68, 0x51, 0x45, 0x64, 0x30, 0x64, 0x6C, 0x71, 0x37, 0x41, 0x2F, 0x43, 0x0A, 0x72, 0x38, + 0x64, 0x65, 0x56, 0x6C, 0x35, 0x63, 0x31, 0x52, 0x78, 0x59, 0x49, 0x69, 0x67, 0x4C, 0x39, 0x7A, 0x43, 0x32, 0x4C, 0x37, 0x46, 0x38, 0x41, 0x6A, 0x45, 0x41, 0x38, 0x47, 0x45, 0x38, 0x70, 0x2F, 0x53, 0x67, 0x67, 0x75, 0x4D, 0x68, 0x31, 0x59, + 0x51, 0x64, 0x63, 0x34, 0x61, 0x63, 0x4C, 0x61, 0x2F, 0x4B, 0x4E, 0x4A, 0x76, 0x78, 0x6E, 0x37, 0x6B, 0x6A, 0x4E, 0x75, 0x4B, 0x38, 0x59, 0x41, 0x4F, 0x64, 0x67, 0x4C, 0x4F, 0x61, 0x56, 0x73, 0x6A, 0x68, 0x0A, 0x34, 0x72, 0x73, 0x55, 0x65, + 0x63, 0x72, 0x4E, 0x49, 0x64, 0x53, 0x55, 0x74, 0x55, 0x6C, 0x44, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, + 0x65, 0x6C, 0x69, 0x61, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x76, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x64, 0x44, 0x43, 0x43, 0x41, 0x31, 0x79, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x50, 0x41, 0x57, 0x64, 0x66, 0x4A, 0x39, 0x62, 0x2B, 0x65, 0x75, 0x50, 0x6B, 0x72, 0x4C, 0x34, 0x4A, 0x57, 0x77, 0x57, 0x65, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x43, 0x77, 0x55, + 0x41, 0x4D, 0x45, 0x51, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x0A, 0x41, 0x6B, 0x5A, 0x4A, 0x4D, 0x52, 0x6F, 0x77, 0x47, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x46, 0x55, 0x5A, 0x57, + 0x78, 0x70, 0x59, 0x53, 0x42, 0x47, 0x61, 0x57, 0x35, 0x73, 0x59, 0x57, 0x35, 0x6B, 0x49, 0x45, 0x39, 0x35, 0x61, 0x6A, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x51, 0x56, 0x47, 0x56, 0x73, 0x61, 0x57, + 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x32, 0x0A, 0x4D, 0x6A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x78, 0x4F, 0x44, 0x45, 0x78, 0x4D, 0x6A, 0x6B, 0x78, 0x4D, 0x54, 0x55, 0x31, 0x4E, 0x54, 0x52, 0x61, 0x46, + 0x77, 0x30, 0x30, 0x4D, 0x7A, 0x45, 0x78, 0x4D, 0x6A, 0x6B, 0x78, 0x4D, 0x54, 0x55, 0x31, 0x4E, 0x54, 0x52, 0x61, 0x4D, 0x45, 0x51, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x5A, 0x4A, 0x4D, + 0x52, 0x6F, 0x77, 0x47, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x0A, 0x44, 0x42, 0x46, 0x55, 0x5A, 0x57, 0x78, 0x70, 0x59, 0x53, 0x42, 0x47, 0x61, 0x57, 0x35, 0x73, 0x59, 0x57, 0x35, 0x6B, 0x49, 0x45, 0x39, 0x35, 0x61, 0x6A, 0x45, 0x5A, + 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x51, 0x56, 0x47, 0x56, 0x73, 0x61, 0x57, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x32, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x0A, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4C, 0x4C, + 0x51, 0x50, 0x77, 0x65, 0x38, 0x34, 0x6E, 0x76, 0x51, 0x61, 0x35, 0x6E, 0x34, 0x34, 0x6E, 0x64, 0x70, 0x35, 0x38, 0x36, 0x64, 0x70, 0x41, 0x4F, 0x38, 0x67, 0x6D, 0x32, 0x68, 0x2F, 0x6F, 0x46, 0x6C, 0x48, 0x30, 0x77, 0x6E, 0x72, 0x49, 0x34, + 0x41, 0x75, 0x68, 0x5A, 0x37, 0x0A, 0x36, 0x7A, 0x42, 0x71, 0x41, 0x4D, 0x43, 0x7A, 0x64, 0x47, 0x68, 0x2B, 0x73, 0x71, 0x2F, 0x48, 0x31, 0x57, 0x4B, 0x7A, 0x65, 0x6A, 0x39, 0x51, 0x79, 0x6F, 0x77, 0x32, 0x52, 0x43, 0x52, 0x6A, 0x30, 0x6A, + 0x62, 0x70, 0x44, 0x49, 0x58, 0x32, 0x51, 0x33, 0x62, 0x56, 0x54, 0x4B, 0x46, 0x67, 0x63, 0x6D, 0x66, 0x69, 0x4B, 0x44, 0x4F, 0x6C, 0x79, 0x7A, 0x47, 0x34, 0x4F, 0x69, 0x49, 0x6A, 0x4E, 0x4C, 0x68, 0x39, 0x76, 0x56, 0x59, 0x69, 0x51, 0x4A, + 0x33, 0x71, 0x0A, 0x39, 0x48, 0x73, 0x44, 0x72, 0x57, 0x6A, 0x38, 0x73, 0x6F, 0x46, 0x50, 0x6D, 0x4E, 0x42, 0x30, 0x36, 0x6F, 0x33, 0x6C, 0x66, 0x63, 0x31, 0x6A, 0x77, 0x36, 0x50, 0x32, 0x33, 0x70, 0x4C, 0x43, 0x57, 0x42, 0x6E, 0x67, 0x6C, + 0x72, 0x76, 0x46, 0x78, 0x4B, 0x6B, 0x39, 0x70, 0x58, 0x53, 0x57, 0x2F, 0x71, 0x2F, 0x35, 0x69, 0x61, 0x71, 0x39, 0x6C, 0x52, 0x64, 0x55, 0x32, 0x48, 0x68, 0x45, 0x38, 0x51, 0x78, 0x33, 0x46, 0x5A, 0x4C, 0x67, 0x6D, 0x45, 0x4B, 0x6E, 0x0A, + 0x70, 0x4E, 0x61, 0x71, 0x49, 0x4A, 0x4C, 0x4E, 0x77, 0x61, 0x43, 0x7A, 0x6C, 0x72, 0x49, 0x36, 0x68, 0x45, 0x4B, 0x4E, 0x66, 0x64, 0x57, 0x56, 0x35, 0x4E, 0x62, 0x62, 0x36, 0x57, 0x4C, 0x45, 0x57, 0x4C, 0x4E, 0x35, 0x78, 0x59, 0x7A, 0x54, + 0x4E, 0x54, 0x4F, 0x44, 0x6E, 0x33, 0x57, 0x68, 0x55, 0x69, 0x64, 0x68, 0x4F, 0x50, 0x46, 0x5A, 0x50, 0x59, 0x35, 0x51, 0x34, 0x4C, 0x31, 0x35, 0x50, 0x4F, 0x64, 0x73, 0x6C, 0x76, 0x35, 0x65, 0x32, 0x51, 0x4A, 0x6C, 0x0A, 0x74, 0x49, 0x35, + 0x63, 0x30, 0x42, 0x45, 0x30, 0x33, 0x31, 0x32, 0x2F, 0x55, 0x71, 0x65, 0x42, 0x41, 0x4D, 0x4E, 0x2F, 0x6D, 0x55, 0x57, 0x5A, 0x46, 0x64, 0x55, 0x58, 0x79, 0x41, 0x70, 0x54, 0x37, 0x47, 0x50, 0x7A, 0x6D, 0x58, 0x33, 0x4D, 0x61, 0x52, 0x4B, + 0x47, 0x77, 0x68, 0x66, 0x77, 0x41, 0x5A, 0x36, 0x2F, 0x68, 0x4C, 0x7A, 0x52, 0x55, 0x73, 0x73, 0x62, 0x6B, 0x6D, 0x62, 0x4F, 0x70, 0x46, 0x50, 0x6C, 0x6F, 0x62, 0x2F, 0x45, 0x32, 0x77, 0x6E, 0x57, 0x0A, 0x35, 0x6F, 0x6C, 0x57, 0x4B, 0x38, + 0x6A, 0x6A, 0x66, 0x4E, 0x37, 0x6A, 0x2F, 0x34, 0x6E, 0x6C, 0x4E, 0x57, 0x34, 0x6F, 0x36, 0x47, 0x77, 0x4C, 0x49, 0x31, 0x47, 0x70, 0x4A, 0x51, 0x58, 0x72, 0x53, 0x50, 0x6A, 0x64, 0x73, 0x63, 0x72, 0x36, 0x62, 0x41, 0x68, 0x52, 0x37, 0x37, + 0x63, 0x59, 0x62, 0x45, 0x54, 0x4B, 0x4A, 0x75, 0x46, 0x7A, 0x78, 0x6F, 0x6B, 0x47, 0x67, 0x65, 0x57, 0x4B, 0x72, 0x4C, 0x44, 0x69, 0x4B, 0x63, 0x61, 0x35, 0x4A, 0x4C, 0x4E, 0x72, 0x0A, 0x52, 0x42, 0x48, 0x30, 0x70, 0x55, 0x50, 0x43, 0x54, + 0x45, 0x50, 0x6C, 0x63, 0x44, 0x61, 0x4D, 0x74, 0x6A, 0x4E, 0x58, 0x65, 0x70, 0x55, 0x75, 0x67, 0x71, 0x44, 0x30, 0x58, 0x42, 0x43, 0x7A, 0x59, 0x59, 0x50, 0x32, 0x41, 0x67, 0x57, 0x47, 0x4C, 0x6E, 0x77, 0x74, 0x62, 0x4E, 0x77, 0x44, 0x52, + 0x6D, 0x34, 0x31, 0x6B, 0x39, 0x56, 0x36, 0x6C, 0x53, 0x2F, 0x65, 0x49, 0x4E, 0x68, 0x62, 0x66, 0x70, 0x53, 0x51, 0x42, 0x47, 0x71, 0x36, 0x57, 0x54, 0x30, 0x45, 0x0A, 0x42, 0x58, 0x57, 0x64, 0x4E, 0x36, 0x49, 0x4F, 0x4C, 0x6A, 0x33, 0x72, + 0x77, 0x61, 0x52, 0x53, 0x67, 0x2F, 0x37, 0x51, 0x61, 0x39, 0x52, 0x6D, 0x6A, 0x74, 0x7A, 0x47, 0x36, 0x52, 0x4A, 0x4F, 0x48, 0x53, 0x70, 0x58, 0x71, 0x68, 0x43, 0x38, 0x66, 0x46, 0x36, 0x43, 0x66, 0x61, 0x61, 0x6D, 0x79, 0x66, 0x49, 0x74, + 0x75, 0x66, 0x55, 0x58, 0x4A, 0x36, 0x33, 0x52, 0x44, 0x6F, 0x6C, 0x55, 0x4B, 0x35, 0x58, 0x36, 0x77, 0x4B, 0x30, 0x64, 0x6D, 0x42, 0x52, 0x34, 0x0A, 0x4D, 0x30, 0x4B, 0x47, 0x43, 0x71, 0x6C, 0x7A, 0x74, 0x66, 0x74, 0x30, 0x44, 0x62, 0x63, + 0x62, 0x4D, 0x42, 0x6E, 0x45, 0x57, 0x67, 0x34, 0x63, 0x4A, 0x37, 0x66, 0x61, 0x47, 0x4E, 0x44, 0x2F, 0x69, 0x73, 0x67, 0x46, 0x75, 0x76, 0x47, 0x71, 0x48, 0x4B, 0x49, 0x33, 0x74, 0x2B, 0x5A, 0x49, 0x70, 0x45, 0x59, 0x73, 0x6C, 0x4F, 0x71, + 0x6F, 0x64, 0x6D, 0x4A, 0x48, 0x69, 0x78, 0x42, 0x54, 0x42, 0x30, 0x68, 0x58, 0x62, 0x4F, 0x4B, 0x53, 0x54, 0x62, 0x61, 0x75, 0x0A, 0x42, 0x63, 0x76, 0x63, 0x77, 0x55, 0x70, 0x65, 0x6A, 0x36, 0x77, 0x39, 0x47, 0x55, 0x37, 0x43, 0x37, 0x57, + 0x42, 0x31, 0x4B, 0x39, 0x76, 0x42, 0x79, 0x6B, 0x4C, 0x56, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x48, + 0x4B, 0x73, 0x35, 0x44, 0x4E, 0x35, 0x71, 0x6B, 0x57, 0x48, 0x39, 0x76, 0x32, 0x73, 0x48, 0x5A, 0x37, 0x57, 0x0A, 0x78, 0x79, 0x2B, 0x47, 0x32, 0x43, 0x51, 0x35, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, + 0x42, 0x52, 0x79, 0x72, 0x4F, 0x51, 0x7A, 0x65, 0x61, 0x70, 0x46, 0x68, 0x2F, 0x62, 0x39, 0x72, 0x42, 0x32, 0x65, 0x31, 0x73, 0x63, 0x76, 0x68, 0x74, 0x67, 0x6B, 0x4F, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, + 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x0A, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, + 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x6F, 0x44, 0x74, 0x5A, 0x70, 0x77, 0x6D, 0x55, 0x50, 0x6A, 0x61, 0x45, 0x30, 0x6E, 0x34, 0x76, 0x4F, 0x61, 0x57, 0x57, + 0x6C, 0x2F, 0x6F, 0x52, 0x72, 0x66, 0x78, 0x6E, 0x38, 0x33, 0x45, 0x4A, 0x0A, 0x38, 0x72, 0x4B, 0x4A, 0x68, 0x47, 0x64, 0x45, 0x72, 0x37, 0x6E, 0x76, 0x37, 0x5A, 0x62, 0x73, 0x6E, 0x47, 0x54, 0x62, 0x4D, 0x6A, 0x42, 0x76, 0x5A, 0x35, 0x71, + 0x73, 0x66, 0x6C, 0x2B, 0x79, 0x71, 0x77, 0x45, 0x32, 0x66, 0x6F, 0x48, 0x36, 0x35, 0x49, 0x52, 0x65, 0x30, 0x71, 0x77, 0x32, 0x34, 0x47, 0x74, 0x69, 0x78, 0x58, 0x31, 0x4C, 0x44, 0x6F, 0x4A, 0x74, 0x30, 0x6E, 0x5A, 0x69, 0x30, 0x66, 0x36, + 0x58, 0x2B, 0x4A, 0x38, 0x77, 0x66, 0x42, 0x6A, 0x35, 0x0A, 0x74, 0x46, 0x4A, 0x33, 0x67, 0x68, 0x31, 0x32, 0x32, 0x39, 0x4D, 0x64, 0x71, 0x66, 0x44, 0x42, 0x6D, 0x67, 0x43, 0x39, 0x62, 0x58, 0x58, 0x59, 0x66, 0x65, 0x66, 0x36, 0x78, 0x7A, + 0x69, 0x6A, 0x6E, 0x48, 0x44, 0x6F, 0x52, 0x6E, 0x6B, 0x44, 0x72, 0x79, 0x35, 0x30, 0x32, 0x33, 0x58, 0x34, 0x62, 0x6C, 0x4D, 0x4D, 0x41, 0x38, 0x69, 0x5A, 0x47, 0x6F, 0x6B, 0x31, 0x47, 0x54, 0x7A, 0x54, 0x79, 0x56, 0x52, 0x38, 0x71, 0x50, + 0x41, 0x73, 0x35, 0x6D, 0x34, 0x48, 0x0A, 0x65, 0x57, 0x39, 0x71, 0x34, 0x65, 0x62, 0x71, 0x6B, 0x59, 0x4A, 0x70, 0x43, 0x68, 0x33, 0x44, 0x66, 0x6C, 0x6D, 0x69, 0x6E, 0x6D, 0x74, 0x47, 0x46, 0x5A, 0x68, 0x62, 0x30, 0x36, 0x39, 0x47, 0x48, + 0x57, 0x4C, 0x49, 0x7A, 0x6F, 0x42, 0x53, 0x53, 0x52, 0x45, 0x2F, 0x79, 0x51, 0x51, 0x53, 0x77, 0x78, 0x4E, 0x38, 0x50, 0x7A, 0x75, 0x4B, 0x6C, 0x74, 0x73, 0x38, 0x6F, 0x42, 0x34, 0x4B, 0x74, 0x49, 0x74, 0x55, 0x73, 0x69, 0x52, 0x6E, 0x44, + 0x65, 0x2B, 0x43, 0x0A, 0x79, 0x37, 0x34, 0x38, 0x66, 0x64, 0x48, 0x69, 0x66, 0x36, 0x34, 0x57, 0x31, 0x6C, 0x5A, 0x59, 0x75, 0x64, 0x6F, 0x67, 0x73, 0x59, 0x4D, 0x56, 0x6F, 0x65, 0x2B, 0x4B, 0x54, 0x54, 0x4A, 0x76, 0x51, 0x53, 0x38, 0x54, + 0x55, 0x6F, 0x4B, 0x55, 0x31, 0x78, 0x72, 0x42, 0x65, 0x4B, 0x4A, 0x52, 0x33, 0x53, 0x74, 0x77, 0x62, 0x62, 0x63, 0x61, 0x2B, 0x66, 0x65, 0x77, 0x34, 0x47, 0x65, 0x58, 0x56, 0x74, 0x74, 0x38, 0x59, 0x56, 0x4D, 0x4A, 0x41, 0x79, 0x67, 0x43, + 0x0A, 0x51, 0x4D, 0x65, 0x7A, 0x32, 0x50, 0x32, 0x63, 0x63, 0x47, 0x72, 0x47, 0x4B, 0x4D, 0x4F, 0x46, 0x36, 0x65, 0x4C, 0x74, 0x47, 0x70, 0x4F, 0x67, 0x33, 0x6B, 0x75, 0x59, 0x6F, 0x6F, 0x51, 0x2B, 0x42, 0x58, 0x63, 0x42, 0x6C, 0x6A, 0x33, + 0x37, 0x74, 0x43, 0x41, 0x50, 0x6E, 0x48, 0x49, 0x43, 0x65, 0x68, 0x49, 0x76, 0x31, 0x61, 0x4F, 0x36, 0x55, 0x58, 0x69, 0x76, 0x4B, 0x69, 0x74, 0x45, 0x5A, 0x55, 0x36, 0x31, 0x2F, 0x51, 0x72, 0x6F, 0x77, 0x63, 0x31, 0x35, 0x0A, 0x68, 0x32, + 0x45, 0x72, 0x33, 0x6F, 0x42, 0x58, 0x52, 0x62, 0x39, 0x6E, 0x38, 0x5A, 0x75, 0x52, 0x58, 0x71, 0x57, 0x6B, 0x37, 0x46, 0x6C, 0x49, 0x45, 0x41, 0x30, 0x34, 0x78, 0x37, 0x44, 0x36, 0x77, 0x30, 0x52, 0x74, 0x42, 0x50, 0x56, 0x34, 0x55, 0x42, + 0x79, 0x53, 0x6C, 0x6C, 0x76, 0x61, 0x39, 0x62, 0x67, 0x75, 0x75, 0x6C, 0x76, 0x50, 0x35, 0x66, 0x42, 0x71, 0x6E, 0x55, 0x73, 0x76, 0x57, 0x48, 0x4D, 0x74, 0x54, 0x79, 0x33, 0x45, 0x48, 0x44, 0x37, 0x30, 0x0A, 0x73, 0x7A, 0x2B, 0x72, 0x46, + 0x51, 0x34, 0x37, 0x47, 0x55, 0x47, 0x4B, 0x70, 0x4D, 0x46, 0x58, 0x45, 0x6D, 0x5A, 0x78, 0x54, 0x50, 0x70, 0x54, 0x34, 0x31, 0x66, 0x72, 0x59, 0x70, 0x55, 0x4A, 0x6E, 0x6C, 0x54, 0x64, 0x30, 0x63, 0x49, 0x38, 0x56, 0x7A, 0x79, 0x39, 0x4F, + 0x4B, 0x32, 0x59, 0x5A, 0x4C, 0x65, 0x34, 0x41, 0x35, 0x70, 0x54, 0x56, 0x6D, 0x42, 0x64, 0x73, 0x39, 0x68, 0x43, 0x47, 0x31, 0x78, 0x4C, 0x45, 0x6F, 0x6F, 0x63, 0x36, 0x2B, 0x74, 0x39, 0x0A, 0x78, 0x6E, 0x70, 0x70, 0x78, 0x79, 0x64, 0x2F, + 0x70, 0x50, 0x69, 0x4C, 0x38, 0x75, 0x53, 0x55, 0x5A, 0x6F, 0x64, 0x4C, 0x36, 0x5A, 0x51, 0x48, 0x43, 0x52, 0x4A, 0x35, 0x69, 0x72, 0x4C, 0x72, 0x64, 0x41, 0x54, 0x63, 0x7A, 0x76, 0x52, 0x45, 0x57, 0x65, 0x41, 0x57, 0x79, 0x73, 0x55, 0x73, + 0x57, 0x4E, 0x63, 0x38, 0x65, 0x38, 0x39, 0x69, 0x68, 0x6D, 0x70, 0x51, 0x66, 0x54, 0x55, 0x32, 0x5A, 0x71, 0x66, 0x37, 0x4E, 0x2B, 0x63, 0x6F, 0x78, 0x39, 0x6A, 0x51, 0x0A, 0x72, 0x61, 0x56, 0x70, 0x6C, 0x49, 0x2F, 0x6F, 0x77, 0x64, 0x38, + 0x6B, 0x2B, 0x42, 0x73, 0x48, 0x4D, 0x59, 0x65, 0x42, 0x32, 0x46, 0x33, 0x32, 0x36, 0x43, 0x6A, 0x59, 0x53, 0x6C, 0x4B, 0x41, 0x72, 0x42, 0x50, 0x75, 0x55, 0x42, 0x51, 0x65, 0x6D, 0x4D, 0x63, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, + 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x2D, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x42, 0x52, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, + 0x31, 0x20, 0x32, 0x30, 0x32, 0x30, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x32, 0x7A, 0x43, 0x43, 0x41, 0x6D, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x51, 0x66, 0x4D, 0x6D, 0x50, 0x4B, 0x34, 0x54, 0x58, 0x33, 0x2B, 0x6F, 0x50, 0x79, 0x57, 0x57, 0x61, 0x30, 0x30, 0x74, 0x4E, 0x6C, 0x6A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, + 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x45, 0x0A, 0x52, 0x54, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x43, 0x31, 0x55, 0x63, 0x6E, + 0x56, 0x7A, 0x64, 0x43, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6C, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x4A, 0x53, 0x49, 0x46, + 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x53, 0x41, 0x79, 0x0A, 0x4D, 0x44, 0x49, 0x77, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x54, 0x41, 0x35, 0x4E, 0x44, 0x55, 0x77, 0x4D, + 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x31, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x54, 0x41, 0x35, 0x4E, 0x44, 0x51, 0x31, 0x4F, 0x56, 0x6F, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, + 0x45, 0x55, 0x78, 0x46, 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x45, 0x51, 0x74, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x52, 0x32, 0x31, 0x69, 0x53, 0x44, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x5A, 0x52, 0x43, 0x31, 0x55, 0x55, 0x6C, 0x56, 0x54, 0x56, 0x43, 0x42, 0x43, 0x55, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x79, + 0x4D, 0x44, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x0A, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4D, 0x62, 0x4C, 0x78, 0x79, 0x6A, + 0x52, 0x2B, 0x34, 0x54, 0x31, 0x6D, 0x75, 0x39, 0x43, 0x46, 0x43, 0x44, 0x68, 0x51, 0x32, 0x74, 0x75, 0x64, 0x61, 0x33, 0x38, 0x4B, 0x77, 0x4F, 0x45, 0x31, 0x48, 0x61, 0x54, 0x4A, 0x64, 0x64, 0x5A, 0x4F, 0x30, 0x46, 0x6C, 0x61, 0x78, 0x37, + 0x6D, 0x4E, 0x43, 0x71, 0x37, 0x0A, 0x64, 0x50, 0x59, 0x53, 0x7A, 0x75, 0x68, 0x74, 0x35, 0x36, 0x76, 0x6B, 0x50, 0x45, 0x34, 0x2F, 0x52, 0x41, 0x69, 0x4C, 0x7A, 0x52, 0x5A, 0x78, 0x79, 0x37, 0x2B, 0x53, 0x6D, 0x66, 0x53, 0x6B, 0x31, 0x7A, + 0x78, 0x51, 0x56, 0x46, 0x4B, 0x51, 0x68, 0x59, 0x4E, 0x34, 0x6C, 0x47, 0x64, 0x6E, 0x6F, 0x78, 0x77, 0x4A, 0x47, 0x54, 0x31, 0x31, 0x4E, 0x49, 0x58, 0x65, 0x37, 0x57, 0x42, 0x39, 0x78, 0x77, 0x79, 0x30, 0x51, 0x56, 0x4B, 0x35, 0x62, 0x75, + 0x58, 0x75, 0x0A, 0x51, 0x71, 0x4F, 0x43, 0x41, 0x51, 0x30, 0x77, 0x67, 0x67, 0x45, 0x4A, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, + 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x48, 0x4F, 0x52, 0x45, 0x4B, 0x76, 0x2F, 0x56, 0x62, 0x4E, 0x61, 0x66, 0x41, 0x6B, 0x6C, 0x31, 0x62, 0x4B, 0x36, 0x43, 0x4B, 0x42, 0x72, 0x71, 0x78, 0x39, 0x74, 0x0A, + 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x43, 0x42, 0x78, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x66, 0x42, 0x49, 0x47, 0x2B, 0x4D, 0x49, 0x47, 0x37, + 0x4D, 0x44, 0x36, 0x67, 0x50, 0x4B, 0x41, 0x36, 0x68, 0x6A, 0x68, 0x6F, 0x64, 0x48, 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x6D, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x75, 0x0A, 0x62, 0x6D, 0x56, + 0x30, 0x4C, 0x32, 0x4E, 0x79, 0x62, 0x43, 0x39, 0x6B, 0x4C, 0x58, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x58, 0x32, 0x4A, 0x79, 0x58, 0x33, 0x4A, 0x76, 0x62, 0x33, 0x52, 0x66, 0x59, 0x32, 0x46, 0x66, 0x4D, 0x56, 0x38, 0x79, 0x4D, 0x44, 0x49, + 0x77, 0x4C, 0x6D, 0x4E, 0x79, 0x62, 0x44, 0x42, 0x35, 0x6F, 0x48, 0x65, 0x67, 0x64, 0x59, 0x5A, 0x7A, 0x62, 0x47, 0x52, 0x68, 0x63, 0x44, 0x6F, 0x76, 0x4C, 0x32, 0x52, 0x70, 0x63, 0x6D, 0x56, 0x6A, 0x0A, 0x64, 0x47, 0x39, 0x79, 0x65, 0x53, + 0x35, 0x6B, 0x4C, 0x58, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4C, 0x6D, 0x35, 0x6C, 0x64, 0x43, 0x39, 0x44, 0x54, 0x6A, 0x31, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x4A, 0x54, 0x49, 0x77, 0x51, 0x6C, 0x49, 0x6C, 0x4D, 0x6A, + 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4A, 0x54, 0x49, 0x77, 0x51, 0x30, 0x45, 0x6C, 0x4D, 0x6A, 0x41, 0x78, 0x4A, 0x54, 0x49, 0x77, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x43, 0x78, 0x50, 0x0A, 0x50, 0x55, 0x51, 0x74, 0x56, 0x48, 0x4A, 0x31, 0x63, + 0x33, 0x51, 0x6C, 0x4D, 0x6A, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4C, 0x45, 0x4D, 0x39, 0x52, 0x45, 0x55, 0x2F, 0x59, 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x56, 0x79, 0x5A, 0x58, 0x5A, 0x76, 0x59, + 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x62, 0x47, 0x6C, 0x7A, 0x64, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x0A, 0x41, 0x77, 0x4E, 0x70, 0x41, 0x44, 0x42, 0x6D, 0x41, 0x6A, 0x45, 0x41, + 0x6C, 0x4A, 0x41, 0x74, 0x45, 0x2F, 0x72, 0x68, 0x59, 0x2F, 0x68, 0x68, 0x59, 0x2B, 0x69, 0x74, 0x68, 0x58, 0x68, 0x55, 0x6B, 0x5A, 0x79, 0x34, 0x6B, 0x7A, 0x67, 0x2B, 0x47, 0x6B, 0x48, 0x61, 0x51, 0x42, 0x5A, 0x54, 0x51, 0x67, 0x6A, 0x4B, + 0x4C, 0x34, 0x37, 0x78, 0x50, 0x6F, 0x46, 0x57, 0x77, 0x4B, 0x72, 0x59, 0x37, 0x52, 0x6A, 0x45, 0x73, 0x4B, 0x37, 0x30, 0x50, 0x76, 0x6F, 0x6D, 0x0A, 0x41, 0x6A, 0x45, 0x41, 0x38, 0x79, 0x6A, 0x69, 0x78, 0x74, 0x73, 0x72, 0x6D, 0x66, 0x75, + 0x33, 0x55, 0x62, 0x67, 0x6B, 0x6F, 0x36, 0x53, 0x55, 0x65, 0x68, 0x6F, 0x2F, 0x35, 0x6A, 0x62, 0x69, 0x41, 0x31, 0x63, 0x7A, 0x69, 0x6A, 0x44, 0x4C, 0x67, 0x73, 0x66, 0x57, 0x46, 0x42, 0x48, 0x56, 0x64, 0x57, 0x4E, 0x62, 0x46, 0x4A, 0x57, + 0x63, 0x48, 0x77, 0x48, 0x50, 0x32, 0x4E, 0x56, 0x79, 0x70, 0x77, 0x38, 0x37, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x0A, 0x44, 0x2D, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x45, 0x56, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x31, 0x20, 0x32, 0x30, 0x32, 0x30, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x32, 0x7A, 0x43, 0x43, 0x41, 0x6D, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x58, 0x77, 0x4A, 0x42, 0x31, 0x33, 0x71, 0x48, 0x66, 0x45, 0x77, 0x44, 0x6F, 0x36, 0x79, 0x57, 0x6A, 0x66, 0x76, + 0x2F, 0x30, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x45, 0x0A, 0x52, 0x54, + 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x43, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x44, 0x45, 0x78, 0x6C, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x56, 0x57, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x53, 0x41, 0x79, 0x0A, 0x4D, 0x44, 0x49, 0x77, 0x4D, + 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x54, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x31, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x54, 0x41, 0x35, 0x4E, 0x54, 0x6B, 0x31, 0x4F, + 0x56, 0x6F, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x46, 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x45, 0x51, 0x74, + 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x52, 0x32, 0x31, 0x69, 0x53, 0x44, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x5A, 0x52, 0x43, 0x31, 0x55, 0x55, 0x6C, 0x56, 0x54, 0x56, 0x43, 0x42, 0x46, + 0x56, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x44, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x0A, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, + 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x50, 0x45, 0x4C, 0x33, 0x59, 0x5A, 0x44, 0x49, 0x42, 0x6E, 0x66, 0x6C, 0x34, 0x58, 0x6F, 0x49, 0x6B, 0x71, 0x62, 0x7A, 0x35, 0x32, 0x59, 0x76, 0x37, 0x51, + 0x46, 0x4A, 0x73, 0x6E, 0x4C, 0x34, 0x36, 0x62, 0x53, 0x6A, 0x38, 0x57, 0x65, 0x65, 0x48, 0x73, 0x78, 0x69, 0x61, 0x6D, 0x4A, 0x72, 0x53, 0x63, 0x38, 0x0A, 0x5A, 0x52, 0x43, 0x43, 0x2F, 0x4E, 0x2F, 0x44, 0x6E, 0x55, 0x37, 0x77, 0x4D, 0x79, + 0x50, 0x45, 0x30, 0x6A, 0x4C, 0x31, 0x48, 0x4C, 0x44, 0x66, 0x4D, 0x78, 0x64, 0x64, 0x78, 0x66, 0x43, 0x78, 0x69, 0x76, 0x6E, 0x76, 0x75, 0x62, 0x63, 0x55, 0x79, 0x69, 0x6C, 0x4B, 0x77, 0x67, 0x2B, 0x70, 0x66, 0x33, 0x56, 0x6C, 0x53, 0x53, + 0x6F, 0x77, 0x5A, 0x2F, 0x52, 0x6B, 0x39, 0x39, 0x59, 0x61, 0x64, 0x39, 0x72, 0x44, 0x77, 0x70, 0x64, 0x68, 0x51, 0x6E, 0x74, 0x4A, 0x0A, 0x72, 0x61, 0x4F, 0x43, 0x41, 0x51, 0x30, 0x77, 0x67, 0x67, 0x45, 0x4A, 0x4D, 0x41, 0x38, 0x47, 0x41, + 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x48, 0x38, 0x51, 0x41, 0x52, 0x59, 0x33, 0x4F, + 0x71, 0x51, 0x6F, 0x35, 0x46, 0x44, 0x34, 0x70, 0x50, 0x66, 0x73, 0x61, 0x7A, 0x4B, 0x32, 0x2F, 0x75, 0x6D, 0x4C, 0x0A, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, + 0x42, 0x6A, 0x43, 0x42, 0x78, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x66, 0x42, 0x49, 0x47, 0x2B, 0x4D, 0x49, 0x47, 0x37, 0x4D, 0x44, 0x36, 0x67, 0x50, 0x4B, 0x41, 0x36, 0x68, 0x6A, 0x68, 0x6F, 0x64, 0x48, 0x52, 0x77, 0x4F, 0x69, 0x38, 0x76, + 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x6D, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x75, 0x0A, 0x62, 0x6D, 0x56, 0x30, 0x4C, 0x32, 0x4E, 0x79, 0x62, 0x43, 0x39, 0x6B, 0x4C, 0x58, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x58, 0x32, 0x56, + 0x32, 0x58, 0x33, 0x4A, 0x76, 0x62, 0x33, 0x52, 0x66, 0x59, 0x32, 0x46, 0x66, 0x4D, 0x56, 0x38, 0x79, 0x4D, 0x44, 0x49, 0x77, 0x4C, 0x6D, 0x4E, 0x79, 0x62, 0x44, 0x42, 0x35, 0x6F, 0x48, 0x65, 0x67, 0x64, 0x59, 0x5A, 0x7A, 0x62, 0x47, 0x52, + 0x68, 0x63, 0x44, 0x6F, 0x76, 0x4C, 0x32, 0x52, 0x70, 0x63, 0x6D, 0x56, 0x6A, 0x0A, 0x64, 0x47, 0x39, 0x79, 0x65, 0x53, 0x35, 0x6B, 0x4C, 0x58, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x4C, 0x6D, 0x35, 0x6C, 0x64, 0x43, 0x39, 0x44, 0x54, 0x6A, + 0x31, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x4A, 0x54, 0x49, 0x77, 0x52, 0x56, 0x59, 0x6C, 0x4D, 0x6A, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4A, 0x54, 0x49, 0x77, 0x51, 0x30, 0x45, 0x6C, 0x4D, 0x6A, 0x41, 0x78, 0x4A, 0x54, + 0x49, 0x77, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x43, 0x78, 0x50, 0x0A, 0x50, 0x55, 0x51, 0x74, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x6C, 0x4D, 0x6A, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4C, 0x45, 0x4D, 0x39, 0x52, 0x45, 0x55, 0x2F, 0x59, + 0x32, 0x56, 0x79, 0x64, 0x47, 0x6C, 0x6D, 0x61, 0x57, 0x4E, 0x68, 0x64, 0x47, 0x56, 0x79, 0x5A, 0x58, 0x5A, 0x76, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x62, 0x47, 0x6C, 0x7A, 0x64, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, + 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x0A, 0x41, 0x77, 0x4E, 0x70, 0x41, 0x44, 0x42, 0x6D, 0x41, 0x6A, 0x45, 0x41, 0x79, 0x6A, 0x7A, 0x47, 0x4B, 0x6E, 0x58, 0x43, 0x58, 0x6E, 0x56, 0x69, 0x4F, 0x54, 0x59, 0x41, 0x59, 0x46, 0x71, 0x4C, + 0x77, 0x5A, 0x4F, 0x5A, 0x7A, 0x4E, 0x6E, 0x62, 0x51, 0x54, 0x73, 0x37, 0x68, 0x35, 0x6B, 0x58, 0x4F, 0x39, 0x58, 0x4D, 0x54, 0x38, 0x6F, 0x69, 0x39, 0x36, 0x43, 0x41, 0x79, 0x2F, 0x6D, 0x30, 0x73, 0x52, 0x74, 0x57, 0x39, 0x58, 0x4C, 0x53, + 0x2F, 0x42, 0x6E, 0x52, 0x0A, 0x41, 0x6A, 0x45, 0x41, 0x6B, 0x66, 0x63, 0x77, 0x6B, 0x7A, 0x38, 0x51, 0x52, 0x69, 0x74, 0x78, 0x70, 0x4E, 0x41, 0x37, 0x52, 0x4A, 0x76, 0x41, 0x4B, 0x51, 0x49, 0x46, 0x73, 0x6B, 0x46, 0x33, 0x55, 0x66, 0x4E, + 0x35, 0x57, 0x70, 0x36, 0x4F, 0x46, 0x4B, 0x42, 0x4F, 0x51, 0x74, 0x4A, 0x62, 0x67, 0x66, 0x4D, 0x30, 0x61, 0x67, 0x50, 0x6E, 0x49, 0x6A, 0x68, 0x51, 0x57, 0x2B, 0x30, 0x5A, 0x54, 0x30, 0x4D, 0x57, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, + 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x45, 0x43, 0x43, 0x20, 0x50, 0x33, + 0x38, 0x34, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x35, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x47, 0x54, 0x43, 0x43, 0x41, 0x5A, 0x2B, + 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x65, 0x43, 0x54, 0x5A, 0x61, 0x7A, 0x33, 0x32, 0x63, 0x69, 0x35, 0x50, 0x68, 0x77, 0x4C, 0x42, 0x43, 0x6F, 0x75, 0x38, 0x7A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, + 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x4F, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x58, 0x4D, 0x42, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, + 0x4D, 0x4F, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4A, 0x6A, 0x41, 0x6B, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x48, 0x55, 0x52, 0x70, 0x5A, 0x32, + 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x44, 0x4D, 0x34, 0x0A, 0x4E, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x31, 0x4D, 0x42, 0x34, 0x58, 0x44, + 0x54, 0x49, 0x78, 0x4D, 0x44, 0x45, 0x78, 0x4E, 0x54, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x32, 0x4D, 0x44, 0x45, 0x78, 0x4E, 0x44, 0x49, 0x7A, 0x4E, 0x54, 0x6B, 0x31, 0x4F, 0x56, 0x6F, 0x77, 0x54, + 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x0A, 0x46, 0x7A, 0x41, 0x56, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x6B, 0x52, 0x70, 0x5A, 0x32, 0x6C, 0x44, + 0x5A, 0x58, 0x4A, 0x30, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x4D, 0x53, 0x59, 0x77, 0x4A, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x31, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, 0x64, 0x43, 0x42, 0x55, + 0x54, 0x46, 0x4D, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x41, 0x7A, 0x4F, 0x44, 0x51, 0x67, 0x0A, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x4E, 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, + 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4D, 0x46, 0x45, 0x6F, 0x63, 0x38, 0x52, 0x6C, 0x31, 0x43, 0x61, 0x33, 0x69, 0x4F, 0x43, 0x4E, 0x51, 0x66, 0x4E, 0x30, 0x4D, 0x73, + 0x59, 0x6E, 0x64, 0x4C, 0x78, 0x66, 0x33, 0x63, 0x31, 0x54, 0x7A, 0x76, 0x64, 0x0A, 0x6C, 0x48, 0x4A, 0x53, 0x37, 0x63, 0x49, 0x37, 0x2B, 0x4F, 0x7A, 0x36, 0x65, 0x32, 0x74, 0x59, 0x49, 0x4F, 0x79, 0x5A, 0x72, 0x73, 0x6E, 0x38, 0x61, 0x4C, + 0x4E, 0x31, 0x75, 0x64, 0x73, 0x4A, 0x37, 0x4D, 0x67, 0x54, 0x39, 0x55, 0x37, 0x47, 0x43, 0x68, 0x31, 0x6D, 0x4D, 0x45, 0x79, 0x37, 0x48, 0x30, 0x63, 0x4B, 0x50, 0x47, 0x45, 0x51, 0x51, 0x69, 0x6C, 0x38, 0x70, 0x51, 0x67, 0x4F, 0x34, 0x43, + 0x4C, 0x70, 0x30, 0x7A, 0x56, 0x6F, 0x7A, 0x70, 0x74, 0x6A, 0x0A, 0x6E, 0x34, 0x53, 0x31, 0x6D, 0x55, 0x31, 0x59, 0x6F, 0x49, 0x37, 0x31, 0x56, 0x4F, 0x65, 0x56, 0x79, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, + 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4D, 0x46, 0x52, 0x52, 0x56, 0x42, 0x5A, 0x71, 0x7A, 0x37, 0x6E, 0x4C, 0x46, 0x72, 0x36, 0x49, 0x43, 0x49, 0x53, 0x42, 0x34, 0x43, 0x49, 0x66, 0x42, 0x46, 0x71, 0x4D, 0x41, 0x34, 0x47, 0x41, + 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, + 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x67, 0x41, 0x4D, 0x47, 0x55, 0x43, 0x4D, 0x51, 0x43, 0x4A, 0x61, 0x6F, 0x31, 0x48, 0x35, 0x2B, 0x7A, 0x38, 0x62, 0x6C, 0x55, 0x44, + 0x32, 0x57, 0x64, 0x73, 0x0A, 0x4A, 0x6B, 0x36, 0x44, 0x78, 0x76, 0x33, 0x4A, 0x2B, 0x79, 0x73, 0x54, 0x76, 0x4C, 0x64, 0x36, 0x6A, 0x4C, 0x52, 0x6C, 0x30, 0x6D, 0x6C, 0x70, 0x59, 0x78, 0x4E, 0x6A, 0x4F, 0x79, 0x5A, 0x51, 0x4C, 0x67, 0x47, + 0x68, 0x65, 0x51, 0x61, 0x52, 0x6E, 0x55, 0x69, 0x2F, 0x77, 0x72, 0x34, 0x43, 0x4D, 0x45, 0x66, 0x44, 0x46, 0x58, 0x75, 0x78, 0x6F, 0x4A, 0x47, 0x5A, 0x53, 0x5A, 0x4F, 0x6F, 0x50, 0x48, 0x7A, 0x6F, 0x52, 0x67, 0x61, 0x4C, 0x4C, 0x50, 0x49, + 0x78, 0x0A, 0x41, 0x4A, 0x53, 0x64, 0x59, 0x73, 0x69, 0x4A, 0x76, 0x52, 0x6D, 0x45, 0x46, 0x4F, 0x6D, 0x6C, 0x2B, 0x77, 0x47, 0x34, 0x44, 0x58, 0x5A, 0x44, 0x6A, 0x43, 0x35, 0x54, 0x79, 0x33, 0x7A, 0x66, 0x44, 0x42, 0x65, 0x57, 0x55, 0x41, + 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x69, 0x67, 0x69, 0x43, 0x65, 0x72, 0x74, 0x20, 0x54, + 0x4C, 0x53, 0x20, 0x52, 0x53, 0x41, 0x34, 0x30, 0x39, 0x36, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x47, 0x35, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, + 0x5A, 0x6A, 0x43, 0x43, 0x41, 0x30, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x43, 0x50, 0x6D, 0x30, 0x65, 0x4B, 0x6A, 0x36, 0x66, 0x74, 0x70, 0x71, 0x4D, 0x7A, 0x65, 0x4A, 0x33, 0x6E, 0x7A, 0x50, 0x69, 0x6A, 0x41, 0x4E, + 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x4E, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, + 0x58, 0x4D, 0x42, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4F, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x4D, 0x54, 0x48, 0x45, 0x52, 0x70, 0x5A, 0x32, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x30, 0x0A, 0x4D, 0x44, 0x6B, 0x32, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, + 0x51, 0x67, 0x52, 0x7A, 0x55, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x59, 0x77, 0x4D, 0x54, 0x45, 0x30, 0x4D, 0x6A, + 0x4D, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x42, 0x4E, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x58, 0x4D, 0x42, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, + 0x68, 0x4D, 0x4F, 0x52, 0x47, 0x6C, 0x6E, 0x61, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x51, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x48, 0x45, 0x52, 0x70, 0x5A, + 0x32, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x30, 0x4D, 0x44, 0x6B, 0x32, 0x0A, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x52, 0x7A, 0x55, 0x77, 0x67, 0x67, 0x49, 0x69, + 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x7A, + 0x30, 0x50, 0x54, 0x4A, 0x65, 0x52, 0x47, 0x64, 0x2F, 0x66, 0x78, 0x6D, 0x67, 0x65, 0x66, 0x4D, 0x31, 0x65, 0x53, 0x38, 0x0A, 0x37, 0x49, 0x45, 0x2B, 0x61, 0x6A, 0x57, 0x4F, 0x4C, 0x72, 0x66, 0x6E, 0x33, 0x71, 0x2F, 0x35, 0x42, 0x30, 0x33, + 0x50, 0x4D, 0x4A, 0x33, 0x71, 0x43, 0x51, 0x75, 0x5A, 0x76, 0x57, 0x78, 0x58, 0x32, 0x68, 0x68, 0x4B, 0x75, 0x48, 0x69, 0x73, 0x4F, 0x6A, 0x6D, 0x6F, 0x70, 0x6B, 0x69, 0x73, 0x4C, 0x6E, 0x4C, 0x6C, 0x76, 0x65, 0x76, 0x78, 0x47, 0x73, 0x33, + 0x6E, 0x70, 0x41, 0x4F, 0x70, 0x50, 0x78, 0x47, 0x30, 0x32, 0x43, 0x2B, 0x4A, 0x46, 0x76, 0x75, 0x55, 0x0A, 0x41, 0x54, 0x32, 0x37, 0x4C, 0x2F, 0x67, 0x54, 0x42, 0x61, 0x46, 0x34, 0x48, 0x49, 0x34, 0x6F, 0x34, 0x45, 0x58, 0x67, 0x67, 0x2F, + 0x52, 0x5A, 0x47, 0x35, 0x57, 0x7A, 0x72, 0x6E, 0x34, 0x44, 0x52, 0x65, 0x57, 0x2B, 0x77, 0x6B, 0x4C, 0x2B, 0x37, 0x76, 0x49, 0x38, 0x74, 0x6F, 0x55, 0x54, 0x6D, 0x44, 0x4B, 0x64, 0x46, 0x71, 0x67, 0x70, 0x77, 0x67, 0x73, 0x63, 0x4F, 0x4E, + 0x79, 0x66, 0x4D, 0x58, 0x64, 0x63, 0x76, 0x79, 0x65, 0x6A, 0x2F, 0x43, 0x65, 0x73, 0x0A, 0x74, 0x79, 0x75, 0x39, 0x64, 0x4A, 0x73, 0x58, 0x4C, 0x66, 0x4B, 0x42, 0x32, 0x6C, 0x32, 0x77, 0x34, 0x53, 0x4D, 0x58, 0x50, 0x6F, 0x68, 0x4B, 0x45, + 0x69, 0x50, 0x51, 0x36, 0x73, 0x2B, 0x64, 0x33, 0x67, 0x4D, 0x58, 0x73, 0x55, 0x4A, 0x4B, 0x6F, 0x42, 0x5A, 0x4D, 0x70, 0x47, 0x32, 0x54, 0x36, 0x54, 0x38, 0x36, 0x37, 0x6A, 0x70, 0x38, 0x6E, 0x56, 0x69, 0x64, 0x39, 0x45, 0x36, 0x50, 0x2F, + 0x44, 0x73, 0x6A, 0x79, 0x47, 0x32, 0x34, 0x34, 0x67, 0x58, 0x61, 0x0A, 0x7A, 0x4F, 0x76, 0x73, 0x77, 0x7A, 0x48, 0x30, 0x31, 0x36, 0x63, 0x70, 0x56, 0x49, 0x44, 0x50, 0x52, 0x46, 0x74, 0x4D, 0x62, 0x7A, 0x43, 0x65, 0x38, 0x38, 0x7A, 0x64, + 0x48, 0x35, 0x52, 0x44, 0x6E, 0x55, 0x31, 0x2F, 0x63, 0x48, 0x41, 0x4E, 0x31, 0x44, 0x72, 0x52, 0x4E, 0x2F, 0x42, 0x73, 0x6E, 0x5A, 0x76, 0x41, 0x46, 0x4A, 0x4E, 0x59, 0x37, 0x38, 0x31, 0x42, 0x4F, 0x48, 0x57, 0x38, 0x45, 0x77, 0x4F, 0x56, + 0x66, 0x48, 0x2F, 0x6A, 0x58, 0x4F, 0x6E, 0x56, 0x0A, 0x44, 0x64, 0x58, 0x69, 0x66, 0x42, 0x42, 0x69, 0x71, 0x6D, 0x76, 0x77, 0x50, 0x58, 0x62, 0x7A, 0x50, 0x36, 0x50, 0x6F, 0x73, 0x4D, 0x48, 0x39, 0x37, 0x36, 0x70, 0x58, 0x54, 0x61, 0x79, + 0x47, 0x70, 0x78, 0x69, 0x30, 0x4B, 0x63, 0x45, 0x73, 0x44, 0x72, 0x39, 0x6B, 0x76, 0x69, 0x6D, 0x4D, 0x32, 0x41, 0x49, 0x74, 0x7A, 0x56, 0x77, 0x76, 0x38, 0x6E, 0x2F, 0x76, 0x46, 0x66, 0x51, 0x4D, 0x46, 0x61, 0x77, 0x4B, 0x73, 0x50, 0x48, + 0x54, 0x44, 0x55, 0x39, 0x71, 0x0A, 0x54, 0x58, 0x65, 0x58, 0x41, 0x61, 0x44, 0x78, 0x5A, 0x72, 0x65, 0x33, 0x7A, 0x75, 0x2F, 0x4F, 0x37, 0x4F, 0x79, 0x6C, 0x64, 0x63, 0x71, 0x73, 0x34, 0x2B, 0x46, 0x6A, 0x39, 0x37, 0x69, 0x68, 0x42, 0x4D, + 0x69, 0x38, 0x65, 0x7A, 0x39, 0x64, 0x4C, 0x52, 0x59, 0x69, 0x56, 0x75, 0x31, 0x49, 0x53, 0x66, 0x36, 0x6E, 0x4C, 0x33, 0x6B, 0x77, 0x4A, 0x5A, 0x75, 0x36, 0x61, 0x79, 0x30, 0x2F, 0x6E, 0x54, 0x76, 0x45, 0x46, 0x2B, 0x63, 0x64, 0x4C, 0x76, + 0x76, 0x79, 0x0A, 0x7A, 0x36, 0x62, 0x38, 0x34, 0x78, 0x51, 0x73, 0x6C, 0x70, 0x67, 0x68, 0x6A, 0x4C, 0x53, 0x52, 0x36, 0x52, 0x6C, 0x67, 0x67, 0x2F, 0x49, 0x77, 0x4B, 0x77, 0x5A, 0x7A, 0x55, 0x4E, 0x57, 0x59, 0x4F, 0x77, 0x62, 0x70, 0x78, + 0x34, 0x6F, 0x4D, 0x59, 0x49, 0x77, 0x6F, 0x2B, 0x46, 0x4B, 0x62, 0x62, 0x75, 0x48, 0x32, 0x54, 0x62, 0x73, 0x47, 0x4A, 0x4A, 0x76, 0x58, 0x4B, 0x79, 0x59, 0x2F, 0x2F, 0x53, 0x6F, 0x76, 0x63, 0x66, 0x58, 0x57, 0x4A, 0x4C, 0x35, 0x2F, 0x0A, + 0x4D, 0x5A, 0x34, 0x50, 0x62, 0x65, 0x69, 0x50, 0x54, 0x30, 0x32, 0x6A, 0x50, 0x2F, 0x38, 0x31, 0x36, 0x74, 0x39, 0x4A, 0x58, 0x6B, 0x47, 0x50, 0x68, 0x76, 0x6E, 0x78, 0x64, 0x33, 0x6C, 0x4C, 0x47, 0x37, 0x53, 0x6A, 0x58, 0x69, 0x2F, 0x37, + 0x52, 0x67, 0x4C, 0x51, 0x5A, 0x68, 0x4E, 0x65, 0x58, 0x6F, 0x56, 0x50, 0x7A, 0x74, 0x68, 0x77, 0x69, 0x48, 0x76, 0x4F, 0x41, 0x62, 0x57, 0x57, 0x6C, 0x39, 0x66, 0x4E, 0x66, 0x66, 0x32, 0x43, 0x2B, 0x4D, 0x49, 0x6B, 0x0A, 0x77, 0x63, 0x6F, + 0x42, 0x4F, 0x55, 0x2B, 0x4E, 0x6F, 0x73, 0x45, 0x55, 0x51, 0x42, 0x2B, 0x63, 0x5A, 0x74, 0x55, 0x4D, 0x43, 0x55, 0x62, 0x57, 0x38, 0x74, 0x44, 0x52, 0x53, 0x48, 0x5A, 0x57, 0x4F, 0x6B, 0x50, 0x4C, 0x74, 0x67, 0x6F, 0x52, 0x4F, 0x62, 0x71, + 0x4D, 0x45, 0x32, 0x77, 0x47, 0x74, 0x5A, 0x37, 0x50, 0x36, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x0A, 0x46, 0x67, 0x51, 0x55, 0x55, 0x54, + 0x4D, 0x63, 0x37, 0x54, 0x5A, 0x41, 0x72, 0x78, 0x66, 0x54, 0x4A, 0x63, 0x31, 0x70, 0x61, 0x50, 0x4B, 0x76, 0x54, 0x69, 0x4D, 0x2B, 0x73, 0x30, 0x45, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, + 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, + 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x47, 0x43, 0x6D, 0x72, 0x31, 0x74, 0x66, 0x56, 0x39, 0x71, 0x4A, 0x32, 0x30, 0x74, 0x51, 0x71, 0x63, 0x51, 0x6A, 0x4E, 0x53, 0x48, 0x2F, 0x30, + 0x47, 0x45, 0x77, 0x68, 0x4A, 0x47, 0x33, 0x50, 0x78, 0x44, 0x50, 0x4A, 0x59, 0x37, 0x4A, 0x76, 0x30, 0x59, 0x30, 0x32, 0x63, 0x45, 0x68, 0x4A, 0x68, 0x78, 0x77, 0x0A, 0x47, 0x58, 0x49, 0x65, 0x6F, 0x38, 0x6D, 0x48, 0x2F, 0x71, 0x6C, 0x44, + 0x5A, 0x4A, 0x59, 0x36, 0x79, 0x46, 0x4D, 0x45, 0x43, 0x72, 0x5A, 0x42, 0x75, 0x38, 0x52, 0x48, 0x41, 0x4E, 0x6D, 0x66, 0x47, 0x42, 0x67, 0x37, 0x73, 0x67, 0x37, 0x7A, 0x4E, 0x4F, 0x6F, 0x6B, 0x39, 0x39, 0x32, 0x76, 0x49, 0x47, 0x43, 0x75, + 0x6B, 0x69, 0x68, 0x66, 0x4E, 0x75, 0x64, 0x64, 0x35, 0x4E, 0x37, 0x48, 0x50, 0x4E, 0x74, 0x51, 0x4F, 0x61, 0x32, 0x37, 0x50, 0x53, 0x68, 0x4E, 0x0A, 0x6C, 0x6E, 0x78, 0x32, 0x78, 0x6C, 0x76, 0x30, 0x77, 0x64, 0x73, 0x55, 0x70, 0x61, 0x73, + 0x5A, 0x59, 0x67, 0x63, 0x59, 0x51, 0x46, 0x2B, 0x58, 0x6B, 0x64, 0x79, 0x63, 0x78, 0x36, 0x75, 0x31, 0x55, 0x51, 0x33, 0x6D, 0x61, 0x56, 0x4E, 0x56, 0x7A, 0x44, 0x6C, 0x39, 0x32, 0x73, 0x55, 0x52, 0x56, 0x58, 0x4C, 0x46, 0x4F, 0x34, 0x75, + 0x4A, 0x2B, 0x44, 0x51, 0x74, 0x70, 0x42, 0x66, 0x6C, 0x46, 0x2B, 0x61, 0x5A, 0x66, 0x54, 0x43, 0x49, 0x49, 0x54, 0x66, 0x4E, 0x0A, 0x4D, 0x42, 0x63, 0x39, 0x75, 0x50, 0x4B, 0x38, 0x71, 0x48, 0x57, 0x67, 0x51, 0x39, 0x77, 0x2B, 0x69, 0x55, + 0x75, 0x51, 0x72, 0x6D, 0x30, 0x44, 0x34, 0x42, 0x79, 0x6A, 0x6F, 0x4A, 0x59, 0x4A, 0x75, 0x33, 0x32, 0x6A, 0x74, 0x79, 0x6F, 0x51, 0x52, 0x45, 0x74, 0x47, 0x42, 0x7A, 0x52, 0x6A, 0x37, 0x54, 0x47, 0x35, 0x42, 0x4F, 0x36, 0x6A, 0x6D, 0x35, + 0x71, 0x75, 0x35, 0x6A, 0x46, 0x34, 0x39, 0x4F, 0x6F, 0x6B, 0x59, 0x54, 0x75, 0x72, 0x57, 0x47, 0x54, 0x2F, 0x0A, 0x75, 0x34, 0x63, 0x6E, 0x59, 0x69, 0x57, 0x42, 0x33, 0x39, 0x79, 0x68, 0x4C, 0x2F, 0x62, 0x74, 0x70, 0x2F, 0x39, 0x36, 0x6A, + 0x31, 0x45, 0x75, 0x4D, 0x50, 0x69, 0x6B, 0x41, 0x64, 0x4B, 0x46, 0x4F, 0x56, 0x38, 0x42, 0x6D, 0x5A, 0x5A, 0x76, 0x57, 0x6C, 0x74, 0x77, 0x47, 0x55, 0x62, 0x2B, 0x68, 0x6D, 0x41, 0x2B, 0x72, 0x59, 0x41, 0x51, 0x43, 0x64, 0x30, 0x35, 0x4A, + 0x53, 0x39, 0x59, 0x66, 0x37, 0x76, 0x53, 0x64, 0x50, 0x44, 0x33, 0x52, 0x68, 0x39, 0x47, 0x0A, 0x4F, 0x55, 0x72, 0x59, 0x55, 0x39, 0x44, 0x7A, 0x4C, 0x6A, 0x74, 0x78, 0x70, 0x64, 0x52, 0x76, 0x2F, 0x50, 0x4E, 0x6E, 0x35, 0x41, 0x65, 0x50, + 0x33, 0x53, 0x59, 0x5A, 0x34, 0x59, 0x31, 0x62, 0x2B, 0x71, 0x4F, 0x54, 0x45, 0x5A, 0x76, 0x70, 0x79, 0x44, 0x72, 0x44, 0x56, 0x57, 0x69, 0x61, 0x6B, 0x75, 0x46, 0x53, 0x64, 0x6A, 0x6A, 0x6F, 0x34, 0x62, 0x71, 0x39, 0x2B, 0x30, 0x2F, 0x56, + 0x37, 0x37, 0x50, 0x6E, 0x53, 0x49, 0x4D, 0x78, 0x38, 0x49, 0x49, 0x68, 0x0A, 0x34, 0x37, 0x61, 0x2B, 0x70, 0x36, 0x74, 0x76, 0x37, 0x35, 0x2F, 0x66, 0x54, 0x4D, 0x38, 0x42, 0x75, 0x47, 0x4A, 0x71, 0x49, 0x7A, 0x33, 0x6E, 0x43, 0x55, 0x32, + 0x41, 0x47, 0x33, 0x73, 0x77, 0x70, 0x4D, 0x50, 0x64, 0x42, 0x33, 0x38, 0x30, 0x76, 0x71, 0x51, 0x6D, 0x73, 0x76, 0x5A, 0x42, 0x36, 0x41, 0x6B, 0x64, 0x34, 0x79, 0x43, 0x59, 0x71, 0x6A, 0x64, 0x50, 0x2F, 0x2F, 0x66, 0x78, 0x34, 0x69, 0x6C, + 0x77, 0x4D, 0x55, 0x63, 0x2F, 0x64, 0x4E, 0x41, 0x55, 0x0A, 0x46, 0x76, 0x6F, 0x68, 0x69, 0x67, 0x4C, 0x56, 0x69, 0x67, 0x6D, 0x55, 0x64, 0x79, 0x37, 0x79, 0x57, 0x53, 0x69, 0x4C, 0x66, 0x46, 0x43, 0x53, 0x43, 0x6D, 0x5A, 0x34, 0x4F, 0x49, + 0x4E, 0x31, 0x78, 0x4C, 0x56, 0x61, 0x71, 0x42, 0x48, 0x47, 0x35, 0x63, 0x47, 0x64, 0x5A, 0x6C, 0x58, 0x50, 0x55, 0x38, 0x53, 0x76, 0x31, 0x33, 0x57, 0x46, 0x71, 0x55, 0x49, 0x54, 0x56, 0x75, 0x77, 0x68, 0x64, 0x34, 0x47, 0x54, 0x57, 0x67, + 0x7A, 0x71, 0x6C, 0x74, 0x6C, 0x4A, 0x0A, 0x79, 0x71, 0x45, 0x49, 0x38, 0x70, 0x63, 0x37, 0x62, 0x5A, 0x73, 0x45, 0x47, 0x43, 0x52, 0x45, 0x6A, 0x6E, 0x77, 0x42, 0x38, 0x74, 0x77, 0x6C, 0x32, 0x46, 0x36, 0x47, 0x6D, 0x72, 0x45, 0x35, 0x32, + 0x2F, 0x57, 0x52, 0x4D, 0x6D, 0x72, 0x52, 0x70, 0x6E, 0x43, 0x4B, 0x6F, 0x76, 0x66, 0x65, 0x70, 0x45, 0x57, 0x46, 0x4A, 0x71, 0x67, 0x65, 0x6A, 0x46, 0x30, 0x70, 0x57, 0x38, 0x68, 0x4C, 0x32, 0x4A, 0x70, 0x71, 0x41, 0x31, 0x35, 0x77, 0x38, + 0x6F, 0x56, 0x50, 0x0A, 0x62, 0x45, 0x74, 0x6F, 0x4C, 0x38, 0x70, 0x55, 0x39, 0x6F, 0x7A, 0x61, 0x4D, 0x76, 0x37, 0x44, 0x61, 0x34, 0x4D, 0x2F, 0x4F, 0x4D, 0x5A, 0x2B, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, + 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x61, 0x69, 0x6E, 0x6C, 0x79, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x4D, 0x49, 0x49, 0x46, 0x52, 0x7A, 0x43, 0x43, 0x41, 0x79, 0x2B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x52, 0x41, 0x49, 0x34, 0x50, 0x2B, 0x55, 0x75, 0x51, 0x63, 0x57, 0x68, 0x6C, 0x4D, 0x31, 0x54, 0x30, 0x31, 0x45, 0x51, 0x35, + 0x74, 0x2B, 0x41, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x50, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, + 0x43, 0x56, 0x56, 0x4D, 0x78, 0x45, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x43, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x68, 0x61, 0x57, 0x35, 0x73, 0x65, 0x54, 0x45, 0x61, 0x4D, 0x42, 0x67, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x41, 0x78, 0x4D, 0x52, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x46, 0x70, 0x62, 0x6D, 0x78, 0x35, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6A, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x0A, 0x4D, 0x6A, 0x45, 0x77, 0x4E, 0x44, + 0x41, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x59, 0x77, 0x4E, 0x44, 0x41, 0x78, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x6A, 0x41, 0x39, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x51, 0x32, 0x56, 0x79, 0x0A, 0x64, 0x47, 0x46, 0x70, 0x62, 0x6D, 0x78, 0x35, 0x4D, + 0x52, 0x6F, 0x77, 0x47, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x46, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x59, 0x57, 0x6C, 0x75, 0x62, 0x48, 0x6B, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x53, 0x4D, 0x54, 0x43, 0x43, 0x41, + 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x0A, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, + 0x41, 0x4E, 0x41, 0x32, 0x31, 0x42, 0x2F, 0x71, 0x33, 0x61, 0x76, 0x6B, 0x30, 0x62, 0x62, 0x6D, 0x2B, 0x79, 0x4C, 0x41, 0x33, 0x52, 0x4D, 0x4E, 0x61, 0x6E, 0x73, 0x69, 0x45, 0x78, 0x79, 0x58, 0x50, 0x47, 0x68, 0x6A, 0x5A, 0x6A, 0x4B, 0x63, + 0x41, 0x37, 0x57, 0x4E, 0x70, 0x49, 0x47, 0x44, 0x32, 0x6E, 0x67, 0x77, 0x45, 0x63, 0x2F, 0x63, 0x73, 0x69, 0x75, 0x2B, 0x6B, 0x72, 0x2B, 0x4F, 0x0A, 0x35, 0x4D, 0x51, 0x54, 0x76, 0x71, 0x52, 0x6F, 0x54, 0x4E, 0x6F, 0x43, 0x61, 0x42, 0x5A, + 0x30, 0x76, 0x72, 0x4C, 0x64, 0x42, 0x4F, 0x52, 0x72, 0x4B, 0x74, 0x30, 0x33, 0x48, 0x32, 0x41, 0x73, 0x32, 0x2F, 0x58, 0x33, 0x6F, 0x58, 0x79, 0x56, 0x74, 0x77, 0x78, 0x77, 0x68, 0x69, 0x37, 0x78, 0x4F, 0x75, 0x39, 0x53, 0x39, 0x38, 0x7A, + 0x54, 0x6D, 0x2F, 0x6D, 0x4C, 0x76, 0x67, 0x37, 0x66, 0x4D, 0x62, 0x65, 0x64, 0x61, 0x46, 0x79, 0x53, 0x70, 0x76, 0x58, 0x6C, 0x0A, 0x38, 0x77, 0x6F, 0x30, 0x74, 0x66, 0x39, 0x37, 0x6F, 0x75, 0x53, 0x48, 0x6F, 0x63, 0x61, 0x76, 0x46, 0x77, + 0x44, 0x76, 0x41, 0x35, 0x48, 0x74, 0x71, 0x52, 0x78, 0x4F, 0x63, 0x54, 0x33, 0x53, 0x69, 0x32, 0x79, 0x4A, 0x39, 0x48, 0x69, 0x47, 0x35, 0x6D, 0x70, 0x4A, 0x6F, 0x4D, 0x36, 0x31, 0x30, 0x72, 0x43, 0x72, 0x6D, 0x2F, 0x62, 0x30, 0x31, 0x43, + 0x37, 0x6A, 0x63, 0x76, 0x6B, 0x32, 0x78, 0x75, 0x73, 0x56, 0x74, 0x79, 0x57, 0x4D, 0x4F, 0x76, 0x77, 0x6C, 0x0A, 0x44, 0x62, 0x4D, 0x69, 0x63, 0x79, 0x46, 0x30, 0x79, 0x45, 0x71, 0x57, 0x59, 0x5A, 0x4C, 0x31, 0x4C, 0x77, 0x73, 0x59, 0x70, + 0x66, 0x53, 0x74, 0x34, 0x75, 0x35, 0x42, 0x76, 0x51, 0x46, 0x35, 0x2B, 0x70, 0x61, 0x4D, 0x6A, 0x52, 0x63, 0x43, 0x4D, 0x4C, 0x54, 0x35, 0x72, 0x33, 0x67, 0x61, 0x6A, 0x4C, 0x51, 0x32, 0x45, 0x42, 0x41, 0x48, 0x42, 0x58, 0x44, 0x51, 0x39, + 0x44, 0x47, 0x51, 0x69, 0x6C, 0x48, 0x46, 0x68, 0x69, 0x5A, 0x35, 0x73, 0x68, 0x47, 0x49, 0x0A, 0x58, 0x73, 0x58, 0x77, 0x43, 0x6C, 0x54, 0x4E, 0x53, 0x61, 0x61, 0x2F, 0x41, 0x70, 0x7A, 0x53, 0x52, 0x4B, 0x66, 0x74, 0x34, 0x33, 0x6A, 0x76, + 0x52, 0x6C, 0x35, 0x74, 0x63, 0x64, 0x46, 0x35, 0x63, 0x42, 0x78, 0x47, 0x58, 0x31, 0x48, 0x70, 0x79, 0x54, 0x66, 0x63, 0x58, 0x33, 0x35, 0x70, 0x65, 0x30, 0x48, 0x66, 0x4E, 0x45, 0x58, 0x67, 0x4F, 0x34, 0x54, 0x30, 0x6F, 0x59, 0x6F, 0x4B, + 0x4E, 0x70, 0x34, 0x33, 0x7A, 0x47, 0x4A, 0x53, 0x34, 0x59, 0x6B, 0x4E, 0x0A, 0x4B, 0x50, 0x6C, 0x36, 0x49, 0x37, 0x45, 0x4E, 0x50, 0x54, 0x32, 0x61, 0x2F, 0x5A, 0x32, 0x42, 0x37, 0x79, 0x79, 0x51, 0x77, 0x48, 0x74, 0x45, 0x54, 0x72, 0x74, + 0x4A, 0x34, 0x41, 0x35, 0x4B, 0x56, 0x70, 0x4B, 0x38, 0x79, 0x37, 0x58, 0x64, 0x65, 0x52, 0x65, 0x4A, 0x6B, 0x64, 0x35, 0x68, 0x69, 0x58, 0x53, 0x53, 0x71, 0x4F, 0x4D, 0x79, 0x68, 0x62, 0x35, 0x4F, 0x68, 0x61, 0x52, 0x4C, 0x57, 0x63, 0x73, + 0x72, 0x78, 0x58, 0x69, 0x4F, 0x63, 0x56, 0x54, 0x51, 0x0A, 0x41, 0x6A, 0x65, 0x5A, 0x6A, 0x4F, 0x56, 0x4A, 0x36, 0x75, 0x42, 0x55, 0x63, 0x71, 0x51, 0x52, 0x42, 0x69, 0x38, 0x4C, 0x6A, 0x4D, 0x46, 0x62, 0x76, 0x72, 0x57, 0x68, 0x73, 0x46, + 0x4E, 0x75, 0x6E, 0x4C, 0x68, 0x67, 0x6B, 0x52, 0x39, 0x5A, 0x61, 0x2F, 0x6B, 0x74, 0x39, 0x4A, 0x51, 0x4B, 0x6C, 0x37, 0x58, 0x73, 0x78, 0x58, 0x59, 0x44, 0x56, 0x42, 0x74, 0x6C, 0x55, 0x72, 0x70, 0x4D, 0x6B, 0x6C, 0x5A, 0x52, 0x4E, 0x61, + 0x42, 0x41, 0x32, 0x43, 0x6E, 0x62, 0x0A, 0x72, 0x6C, 0x4A, 0x32, 0x4F, 0x79, 0x30, 0x77, 0x51, 0x4A, 0x75, 0x4B, 0x30, 0x45, 0x4A, 0x57, 0x74, 0x4C, 0x65, 0x49, 0x41, 0x61, 0x53, 0x48, 0x4F, 0x31, 0x4F, 0x57, 0x7A, 0x61, 0x4D, 0x57, 0x6A, + 0x2F, 0x4E, 0x6D, 0x71, 0x68, 0x65, 0x78, 0x78, 0x32, 0x44, 0x67, 0x77, 0x55, 0x4D, 0x46, 0x44, 0x4F, 0x36, 0x62, 0x57, 0x32, 0x42, 0x76, 0x42, 0x6C, 0x79, 0x48, 0x57, 0x79, 0x66, 0x35, 0x51, 0x42, 0x47, 0x65, 0x6E, 0x44, 0x50, 0x42, 0x74, + 0x2B, 0x55, 0x31, 0x0A, 0x56, 0x77, 0x56, 0x2F, 0x4A, 0x38, 0x34, 0x58, 0x49, 0x49, 0x77, 0x63, 0x2F, 0x50, 0x48, 0x37, 0x32, 0x6A, 0x45, 0x70, 0x53, 0x65, 0x33, 0x31, 0x43, 0x34, 0x53, 0x6E, 0x54, 0x38, 0x48, 0x32, 0x54, 0x73, 0x49, 0x6F, + 0x6E, 0x50, 0x72, 0x75, 0x34, 0x4B, 0x38, 0x48, 0x2B, 0x7A, 0x4D, 0x52, 0x65, 0x69, 0x46, 0x50, 0x43, 0x79, 0x45, 0x51, 0x74, 0x6B, 0x41, 0x36, 0x71, 0x79, 0x49, 0x36, 0x42, 0x4A, 0x79, 0x4C, 0x6D, 0x34, 0x53, 0x47, 0x63, 0x70, 0x72, 0x53, + 0x0A, 0x70, 0x36, 0x58, 0x45, 0x74, 0x48, 0x57, 0x52, 0x71, 0x53, 0x73, 0x6A, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, + 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x44, 0x67, + 0x51, 0x57, 0x42, 0x42, 0x54, 0x67, 0x71, 0x6A, 0x38, 0x6C, 0x6A, 0x5A, 0x39, 0x45, 0x58, 0x4D, 0x45, 0x36, 0x36, 0x43, 0x36, 0x75, 0x64, 0x30, 0x79, 0x45, 0x50, 0x6D, 0x63, 0x4D, 0x39, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, + 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x75, 0x56, 0x65, 0x76, 0x75, 0x42, 0x4C, 0x61, 0x56, 0x34, 0x4F, 0x50, 0x61, 0x41, 0x73, 0x7A, 0x0A, 0x48, 0x51, 0x4E, 0x54, 0x56, + 0x66, 0x53, 0x56, 0x63, 0x4F, 0x51, 0x72, 0x50, 0x62, 0x41, 0x35, 0x36, 0x2F, 0x71, 0x4A, 0x59, 0x76, 0x33, 0x33, 0x31, 0x68, 0x67, 0x45, 0x4C, 0x79, 0x45, 0x30, 0x33, 0x66, 0x46, 0x6F, 0x38, 0x4E, 0x57, 0x57, 0x57, 0x74, 0x37, 0x43, 0x67, + 0x4B, 0x50, 0x42, 0x6A, 0x63, 0x5A, 0x71, 0x39, 0x31, 0x6C, 0x33, 0x72, 0x68, 0x56, 0x6B, 0x7A, 0x31, 0x74, 0x35, 0x42, 0x58, 0x64, 0x6D, 0x36, 0x6F, 0x7A, 0x54, 0x61, 0x77, 0x33, 0x64, 0x0A, 0x38, 0x56, 0x6B, 0x73, 0x77, 0x54, 0x4F, 0x6C, + 0x4D, 0x49, 0x41, 0x56, 0x52, 0x51, 0x64, 0x46, 0x47, 0x6A, 0x45, 0x69, 0x74, 0x70, 0x49, 0x41, 0x71, 0x35, 0x6C, 0x4E, 0x4F, 0x6F, 0x39, 0x33, 0x72, 0x36, 0x6B, 0x69, 0x79, 0x69, 0x39, 0x6A, 0x79, 0x68, 0x58, 0x57, 0x78, 0x38, 0x62, 0x77, + 0x50, 0x57, 0x7A, 0x38, 0x48, 0x41, 0x32, 0x59, 0x45, 0x47, 0x47, 0x65, 0x45, 0x61, 0x49, 0x69, 0x31, 0x77, 0x72, 0x79, 0x6B, 0x58, 0x70, 0x72, 0x4F, 0x51, 0x34, 0x76, 0x0A, 0x4D, 0x4D, 0x4D, 0x32, 0x53, 0x5A, 0x2F, 0x67, 0x36, 0x51, 0x38, + 0x43, 0x52, 0x46, 0x41, 0x33, 0x6C, 0x46, 0x56, 0x39, 0x36, 0x70, 0x2F, 0x32, 0x4F, 0x37, 0x71, 0x55, 0x70, 0x55, 0x7A, 0x70, 0x76, 0x44, 0x35, 0x52, 0x74, 0x4F, 0x6A, 0x4B, 0x6B, 0x6A, 0x5A, 0x55, 0x62, 0x56, 0x77, 0x6C, 0x4B, 0x4E, 0x72, + 0x64, 0x72, 0x52, 0x54, 0x39, 0x30, 0x2B, 0x37, 0x69, 0x49, 0x67, 0x58, 0x72, 0x30, 0x50, 0x4B, 0x33, 0x61, 0x42, 0x4C, 0x58, 0x57, 0x6F, 0x70, 0x42, 0x0A, 0x47, 0x73, 0x61, 0x53, 0x70, 0x56, 0x6F, 0x37, 0x59, 0x30, 0x56, 0x50, 0x76, 0x2B, + 0x45, 0x36, 0x64, 0x79, 0x49, 0x76, 0x58, 0x4C, 0x39, 0x47, 0x2B, 0x56, 0x6F, 0x44, 0x68, 0x52, 0x4E, 0x43, 0x58, 0x38, 0x72, 0x65, 0x55, 0x39, 0x64, 0x69, 0x74, 0x61, 0x59, 0x31, 0x42, 0x4D, 0x4A, 0x48, 0x2F, 0x35, 0x6E, 0x39, 0x68, 0x4E, + 0x39, 0x63, 0x7A, 0x75, 0x6C, 0x65, 0x67, 0x43, 0x68, 0x42, 0x38, 0x6E, 0x33, 0x6E, 0x48, 0x70, 0x44, 0x59, 0x54, 0x33, 0x59, 0x2B, 0x0A, 0x67, 0x6A, 0x77, 0x4E, 0x2F, 0x4B, 0x55, 0x44, 0x2B, 0x6E, 0x73, 0x61, 0x32, 0x55, 0x55, 0x65, 0x59, + 0x4E, 0x72, 0x45, 0x6A, 0x76, 0x6E, 0x38, 0x4B, 0x38, 0x6C, 0x37, 0x6C, 0x63, 0x55, 0x71, 0x2F, 0x36, 0x71, 0x4A, 0x33, 0x34, 0x49, 0x78, 0x44, 0x33, 0x4C, 0x2F, 0x44, 0x43, 0x66, 0x58, 0x43, 0x68, 0x35, 0x57, 0x41, 0x46, 0x41, 0x65, 0x44, + 0x4A, 0x44, 0x42, 0x6C, 0x72, 0x58, 0x59, 0x46, 0x49, 0x57, 0x37, 0x70, 0x77, 0x30, 0x57, 0x77, 0x66, 0x67, 0x48, 0x0A, 0x4A, 0x42, 0x75, 0x36, 0x68, 0x61, 0x45, 0x61, 0x42, 0x51, 0x6D, 0x41, 0x75, 0x70, 0x56, 0x6A, 0x79, 0x54, 0x72, 0x73, + 0x4A, 0x5A, 0x39, 0x2F, 0x6E, 0x62, 0x71, 0x6B, 0x52, 0x78, 0x57, 0x62, 0x52, 0x48, 0x44, 0x78, 0x61, 0x6B, 0x76, 0x57, 0x4F, 0x46, 0x35, 0x44, 0x38, 0x78, 0x68, 0x2B, 0x55, 0x47, 0x37, 0x70, 0x57, 0x69, 0x6A, 0x6D, 0x5A, 0x65, 0x5A, 0x33, + 0x47, 0x7A, 0x72, 0x39, 0x48, 0x62, 0x34, 0x44, 0x4A, 0x71, 0x50, 0x62, 0x31, 0x4F, 0x47, 0x37, 0x0A, 0x66, 0x70, 0x59, 0x6E, 0x4B, 0x78, 0x33, 0x75, 0x70, 0x50, 0x76, 0x61, 0x4A, 0x56, 0x51, 0x54, 0x41, 0x39, 0x34, 0x35, 0x78, 0x73, 0x4D, + 0x66, 0x54, 0x5A, 0x44, 0x73, 0x6A, 0x78, 0x74, 0x4B, 0x30, 0x68, 0x7A, 0x74, 0x68, 0x5A, 0x55, 0x34, 0x55, 0x48, 0x6C, 0x47, 0x31, 0x73, 0x47, 0x51, 0x55, 0x44, 0x47, 0x70, 0x58, 0x4A, 0x70, 0x75, 0x48, 0x66, 0x55, 0x7A, 0x56, 0x6F, 0x75, + 0x6E, 0x6D, 0x64, 0x4C, 0x79, 0x79, 0x43, 0x77, 0x7A, 0x6B, 0x35, 0x49, 0x77, 0x0A, 0x78, 0x30, 0x36, 0x4D, 0x5A, 0x54, 0x4D, 0x51, 0x5A, 0x42, 0x66, 0x39, 0x4A, 0x42, 0x65, 0x57, 0x30, 0x59, 0x33, 0x43, 0x4F, 0x6D, 0x6F, 0x72, 0x36, 0x78, + 0x4F, 0x4C, 0x52, 0x50, 0x49, 0x68, 0x38, 0x30, 0x6F, 0x61, 0x74, 0x33, 0x64, 0x66, 0x31, 0x2B, 0x32, 0x49, 0x70, 0x48, 0x4C, 0x6C, 0x4F, 0x52, 0x2B, 0x56, 0x6E, 0x62, 0x35, 0x6E, 0x77, 0x58, 0x41, 0x52, 0x50, 0x62, 0x76, 0x30, 0x2B, 0x45, + 0x6D, 0x33, 0x34, 0x79, 0x61, 0x58, 0x4F, 0x70, 0x2F, 0x53, 0x0A, 0x58, 0x33, 0x7A, 0x37, 0x77, 0x4A, 0x6C, 0x38, 0x4F, 0x53, 0x6E, 0x67, 0x65, 0x78, 0x32, 0x2F, 0x44, 0x61, 0x65, 0x50, 0x30, 0x69, 0x6B, 0x30, 0x62, 0x69, 0x51, 0x56, 0x79, + 0x39, 0x36, 0x51, 0x58, 0x72, 0x38, 0x61, 0x78, 0x47, 0x62, 0x71, 0x77, 0x75, 0x61, 0x36, 0x4F, 0x56, 0x2B, 0x4B, 0x6D, 0x61, 0x6C, 0x42, 0x57, 0x51, 0x65, 0x77, 0x4C, 0x4B, 0x38, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x43, 0x65, 0x72, 0x74, 0x61, 0x69, 0x6E, 0x6C, 0x79, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x45, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x42, 0x39, 0x7A, 0x43, 0x43, 0x41, 0x58, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x42, 0x69, 0x55, 0x7A, 0x73, 0x55, 0x63, 0x44, 0x4D, 0x79, 0x64, 0x63, 0x2B, 0x59, 0x32, 0x61, 0x75, + 0x62, 0x2F, 0x4D, 0x2B, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x41, 0x39, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x56, 0x0A, + 0x55, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x51, 0x32, 0x56, 0x79, 0x64, 0x47, 0x46, 0x70, 0x62, 0x6D, 0x78, 0x35, 0x4D, 0x52, 0x6F, 0x77, 0x47, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, + 0x45, 0x78, 0x46, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x59, 0x57, 0x6C, 0x75, 0x62, 0x48, 0x6B, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x46, 0x4D, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x54, 0x41, 0x30, 0x0A, 0x4D, 0x44, 0x45, + 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4E, 0x6A, 0x41, 0x30, 0x4D, 0x44, 0x45, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x42, 0x61, 0x4D, 0x44, 0x30, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, + 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6C, 0x56, 0x54, 0x4D, 0x52, 0x49, 0x77, 0x45, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x6C, 0x44, 0x5A, 0x58, 0x4A, 0x30, 0x59, 0x57, 0x6C, 0x75, 0x0A, 0x62, 0x48, 0x6B, 0x78, 0x47, 0x6A, + 0x41, 0x59, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x45, 0x55, 0x4E, 0x6C, 0x63, 0x6E, 0x52, 0x68, 0x61, 0x57, 0x35, 0x73, 0x65, 0x53, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x55, 0x78, 0x4D, 0x48, 0x59, 0x77, 0x45, 0x41, + 0x59, 0x48, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x43, 0x41, 0x51, 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x33, 0x6D, 0x2F, 0x34, 0x0A, 0x66, 0x78, 0x7A, 0x66, 0x37, 0x66, 0x6C, 0x48, 0x68, + 0x34, 0x61, 0x78, 0x70, 0x4D, 0x43, 0x4B, 0x2B, 0x49, 0x4B, 0x58, 0x67, 0x4F, 0x71, 0x50, 0x79, 0x45, 0x70, 0x65, 0x4B, 0x6E, 0x32, 0x49, 0x61, 0x4B, 0x63, 0x42, 0x59, 0x68, 0x53, 0x52, 0x4A, 0x48, 0x70, 0x63, 0x6E, 0x71, 0x4D, 0x58, 0x66, + 0x59, 0x71, 0x47, 0x49, 0x54, 0x51, 0x59, 0x55, 0x42, 0x73, 0x51, 0x33, 0x74, 0x41, 0x33, 0x53, 0x79, 0x62, 0x48, 0x47, 0x57, 0x43, 0x41, 0x36, 0x54, 0x53, 0x39, 0x0A, 0x59, 0x42, 0x6B, 0x32, 0x51, 0x4E, 0x59, 0x70, 0x68, 0x77, 0x6B, 0x38, + 0x6B, 0x58, 0x72, 0x32, 0x76, 0x42, 0x4D, 0x6A, 0x33, 0x56, 0x6C, 0x4F, 0x42, 0x46, 0x37, 0x50, 0x79, 0x41, 0x49, 0x63, 0x47, 0x46, 0x50, 0x42, 0x4D, 0x64, 0x6A, 0x61, 0x49, 0x4F, 0x6C, 0x45, 0x6A, 0x65, 0x52, 0x32, 0x6F, 0x30, 0x49, 0x77, + 0x51, 0x44, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x0A, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, + 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x38, 0x79, 0x67, 0x59, 0x79, 0x32, 0x52, 0x31, 0x37, 0x69, 0x6B, 0x71, 0x36, 0x2B, 0x32, 0x75, 0x49, 0x31, 0x67, + 0x34, 0x68, 0x65, 0x76, 0x49, 0x49, 0x67, 0x63, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x0A, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, 0x77, 0x5A, 0x51, 0x49, 0x78, 0x41, 0x4C, 0x47, 0x4F, 0x57, 0x69, + 0x44, 0x44, 0x73, 0x68, 0x6C, 0x69, 0x54, 0x64, 0x36, 0x77, 0x54, 0x39, 0x39, 0x75, 0x30, 0x6E, 0x43, 0x4B, 0x38, 0x5A, 0x39, 0x2B, 0x61, 0x6F, 0x7A, 0x6D, 0x75, 0x74, 0x36, 0x44, 0x61, 0x63, 0x70, 0x70, 0x73, 0x36, 0x6B, 0x46, 0x74, 0x5A, + 0x61, 0x53, 0x46, 0x34, 0x66, 0x43, 0x30, 0x75, 0x72, 0x51, 0x65, 0x38, 0x37, 0x59, 0x51, 0x56, 0x74, 0x38, 0x0A, 0x72, 0x67, 0x49, 0x77, 0x52, 0x74, 0x37, 0x71, 0x79, 0x31, 0x32, 0x61, 0x37, 0x44, 0x4C, 0x43, 0x5A, 0x52, 0x61, 0x77, 0x54, + 0x44, 0x42, 0x63, 0x4D, 0x50, 0x50, 0x61, 0x54, 0x6E, 0x4F, 0x47, 0x42, 0x74, 0x6A, 0x4F, 0x69, 0x51, 0x52, 0x49, 0x4E, 0x7A, 0x66, 0x34, 0x33, 0x54, 0x4E, 0x52, 0x6E, 0x58, 0x43, 0x76, 0x65, 0x31, 0x58, 0x59, 0x41, 0x53, 0x35, 0x39, 0x42, + 0x57, 0x51, 0x4F, 0x68, 0x72, 0x69, 0x52, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, + 0x69, 0x74, 0x79, 0x20, 0x43, 0x6F, 0x6D, 0x6D, 0x75, 0x6E, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x43, 0x41, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4F, 0x44, 0x43, 0x43, 0x41, 0x62, 0x36, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x4A, 0x41, 0x4E, 0x5A, 0x64, 0x6D, 0x37, 0x4E, + 0x34, 0x67, 0x53, 0x37, 0x72, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x4D, 0x47, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x70, + 0x51, 0x4D, 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x78, 0x54, 0x52, 0x55, 0x4E, 0x50, 0x54, 0x53, 0x42, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x54, 0x65, 0x58, 0x4E, 0x30, 0x5A, 0x57, + 0x31, 0x7A, 0x49, 0x45, 0x4E, 0x50, 0x4C, 0x69, 0x78, 0x4D, 0x56, 0x45, 0x51, 0x75, 0x4D, 0x53, 0x73, 0x77, 0x4B, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x4A, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, + 0x42, 0x44, 0x62, 0x32, 0x31, 0x74, 0x0A, 0x64, 0x57, 0x35, 0x70, 0x59, 0x32, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x49, 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x51, 0x30, 0x45, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, + 0x54, 0x45, 0x32, 0x4D, 0x44, 0x59, 0x78, 0x4E, 0x6A, 0x41, 0x31, 0x4D, 0x54, 0x55, 0x79, 0x4F, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x34, 0x4D, 0x44, 0x45, 0x78, 0x4F, 0x44, 0x41, 0x31, 0x4D, 0x54, 0x55, 0x79, 0x4F, 0x46, 0x6F, 0x77, 0x59, + 0x54, 0x45, 0x4C, 0x0A, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x6C, 0x41, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x46, 0x4E, 0x46, 0x51, 0x30, 0x39, 0x4E, + 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x46, 0x4E, 0x35, 0x63, 0x33, 0x52, 0x6C, 0x62, 0x58, 0x4D, 0x67, 0x51, 0x30, 0x38, 0x75, 0x4C, 0x45, 0x78, 0x55, 0x52, 0x43, 0x34, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, + 0x0A, 0x42, 0x41, 0x4D, 0x54, 0x49, 0x6C, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x45, 0x4E, 0x76, 0x62, 0x57, 0x31, 0x31, 0x62, 0x6D, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x52, 0x55, 0x4E, + 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x52, 0x44, 0x51, 0x54, 0x45, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x0A, 0x49, 0x67, + 0x4E, 0x69, 0x41, 0x41, 0x53, 0x6B, 0x70, 0x57, 0x39, 0x67, 0x41, 0x77, 0x50, 0x44, 0x76, 0x54, 0x48, 0x30, 0x30, 0x78, 0x65, 0x63, 0x4B, 0x34, 0x52, 0x31, 0x72, 0x4F, 0x58, 0x39, 0x50, 0x56, 0x64, 0x75, 0x31, 0x32, 0x4F, 0x2F, 0x35, 0x67, + 0x53, 0x4A, 0x6B, 0x6F, 0x36, 0x42, 0x6E, 0x4F, 0x50, 0x70, 0x52, 0x32, 0x37, 0x4B, 0x6B, 0x42, 0x4C, 0x49, 0x45, 0x2B, 0x43, 0x6E, 0x6E, 0x66, 0x64, 0x6C, 0x64, 0x42, 0x39, 0x73, 0x45, 0x4C, 0x4C, 0x6F, 0x0A, 0x35, 0x4F, 0x6E, 0x76, 0x62, + 0x59, 0x55, 0x79, 0x6D, 0x55, 0x53, 0x78, 0x58, 0x76, 0x33, 0x4D, 0x64, 0x68, 0x44, 0x59, 0x57, 0x37, 0x32, 0x69, 0x78, 0x76, 0x6E, 0x57, 0x51, 0x75, 0x52, 0x58, 0x64, 0x74, 0x79, 0x51, 0x77, 0x6A, 0x57, 0x70, 0x53, 0x34, 0x67, 0x38, 0x45, + 0x6B, 0x64, 0x74, 0x58, 0x50, 0x39, 0x4A, 0x54, 0x78, 0x70, 0x4B, 0x55, 0x4C, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x0A, 0x42, 0x42, 0x53, 0x47, 0x48, 0x4F, 0x66, 0x2B, + 0x4C, 0x61, 0x56, 0x4B, 0x69, 0x77, 0x6A, 0x2B, 0x4B, 0x42, 0x48, 0x36, 0x76, 0x71, 0x4E, 0x6D, 0x2B, 0x47, 0x42, 0x5A, 0x4C, 0x7A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, + 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x4B, 0x0A, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, + 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x41, 0x44, 0x42, 0x6C, 0x41, 0x6A, 0x41, 0x56, 0x58, 0x55, 0x49, 0x39, 0x2F, 0x4C, 0x62, 0x75, 0x39, 0x7A, 0x75, 0x78, 0x4E, 0x75, 0x69, 0x65, 0x39, 0x73, 0x52, 0x47, 0x4B, 0x45, 0x6B, 0x7A, 0x30, 0x46, 0x68, + 0x44, 0x4B, 0x6D, 0x4D, 0x70, 0x7A, 0x45, 0x32, 0x78, 0x74, 0x48, 0x71, 0x69, 0x75, 0x51, 0x30, 0x34, 0x70, 0x56, 0x31, 0x49, 0x4B, 0x76, 0x33, 0x4C, 0x0A, 0x73, 0x6E, 0x4E, 0x64, 0x6F, 0x34, 0x67, 0x49, 0x78, 0x77, 0x77, 0x43, 0x4D, 0x51, + 0x44, 0x41, 0x71, 0x79, 0x30, 0x4F, 0x62, 0x65, 0x30, 0x59, 0x6F, 0x74, 0x74, 0x54, 0x36, 0x53, 0x58, 0x62, 0x56, 0x51, 0x6A, 0x67, 0x55, 0x4D, 0x7A, 0x66, 0x52, 0x47, 0x45, 0x57, 0x67, 0x71, 0x74, 0x4A, 0x73, 0x4C, 0x4B, 0x42, 0x37, 0x48, + 0x4F, 0x48, 0x65, 0x4C, 0x52, 0x4D, 0x73, 0x6D, 0x49, 0x62, 0x45, 0x76, 0x6F, 0x57, 0x54, 0x53, 0x56, 0x4C, 0x59, 0x37, 0x30, 0x65, 0x0A, 0x4E, 0x39, 0x6B, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, + 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x42, 0x4A, 0x43, 0x41, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x64, 0x44, 0x43, 0x43, 0x41, 0x31, 0x79, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x56, 0x57, 0x39, 0x6C, 0x34, 0x37, 0x54, 0x5A, 0x6B, 0x47, 0x6F, 0x62, 0x43, 0x64, 0x46, + 0x73, 0x50, 0x73, 0x42, 0x73, 0x49, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x55, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x6D, 0x4D, 0x43, 0x51, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x64, 0x51, 0x6B, 0x56, 0x4A, 0x53, 0x6B, 0x6C, 0x4F, 0x52, 0x79, 0x42, 0x44, 0x52, 0x56, 0x4A, 0x55, 0x53, 0x55, + 0x5A, 0x4A, 0x51, 0x30, 0x46, 0x55, 0x52, 0x53, 0x42, 0x42, 0x56, 0x56, 0x52, 0x49, 0x54, 0x31, 0x4A, 0x4A, 0x56, 0x46, 0x6B, 0x78, 0x48, 0x54, 0x41, 0x62, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x45, 0x4A, 0x4B, 0x0A, 0x51, + 0x30, 0x45, 0x67, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x35, 0x4D, 0x54, 0x49, 0x78, 0x4F, 0x54, 0x41, 0x7A, 0x4D, + 0x54, 0x59, 0x78, 0x4E, 0x31, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x30, 0x4D, 0x54, 0x49, 0x78, 0x4D, 0x6A, 0x41, 0x7A, 0x4D, 0x54, 0x59, 0x78, 0x4E, 0x31, 0x6F, 0x77, 0x56, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, + 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x4A, 0x6A, 0x41, 0x6B, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x55, 0x4A, 0x46, 0x53, 0x55, 0x70, 0x4A, 0x54, 0x6B, 0x63, 0x67, 0x51, 0x30, 0x56, 0x53, 0x56, 0x45, 0x6C, 0x47, + 0x53, 0x55, 0x4E, 0x42, 0x56, 0x45, 0x55, 0x67, 0x51, 0x56, 0x56, 0x55, 0x53, 0x45, 0x39, 0x53, 0x53, 0x56, 0x52, 0x5A, 0x4D, 0x52, 0x30, 0x77, 0x47, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x44, 0x42, 0x52, 0x43, 0x53, 0x6B, 0x4E, + 0x42, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, + 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x50, 0x46, 0x6D, 0x0A, 0x43, 0x4C, 0x33, 0x5A, 0x78, 0x52, 0x56, 0x68, 0x79, 0x34, + 0x51, 0x45, 0x51, 0x61, 0x56, 0x70, 0x4E, 0x33, 0x63, 0x64, 0x77, 0x62, 0x42, 0x37, 0x2B, 0x73, 0x4E, 0x33, 0x53, 0x4A, 0x41, 0x54, 0x63, 0x6D, 0x54, 0x52, 0x75, 0x48, 0x79, 0x51, 0x4E, 0x5A, 0x30, 0x59, 0x65, 0x59, 0x6A, 0x6A, 0x6C, 0x77, + 0x45, 0x38, 0x52, 0x34, 0x48, 0x79, 0x44, 0x71, 0x4B, 0x59, 0x44, 0x5A, 0x34, 0x2F, 0x4E, 0x2B, 0x41, 0x5A, 0x73, 0x70, 0x44, 0x79, 0x52, 0x68, 0x79, 0x53, 0x0A, 0x73, 0x54, 0x70, 0x68, 0x7A, 0x76, 0x71, 0x33, 0x52, 0x70, 0x34, 0x44, 0x68, + 0x74, 0x63, 0x7A, 0x62, 0x75, 0x33, 0x33, 0x52, 0x59, 0x78, 0x32, 0x4E, 0x39, 0x35, 0x75, 0x6C, 0x70, 0x48, 0x33, 0x31, 0x33, 0x34, 0x72, 0x68, 0x78, 0x66, 0x56, 0x69, 0x7A, 0x58, 0x75, 0x68, 0x4A, 0x46, 0x79, 0x56, 0x39, 0x78, 0x67, 0x77, + 0x38, 0x4F, 0x35, 0x35, 0x38, 0x64, 0x6E, 0x4A, 0x43, 0x4E, 0x50, 0x59, 0x77, 0x70, 0x6A, 0x39, 0x6D, 0x5A, 0x39, 0x53, 0x31, 0x57, 0x6E, 0x0A, 0x50, 0x33, 0x68, 0x6B, 0x53, 0x57, 0x6B, 0x53, 0x6C, 0x2B, 0x42, 0x4D, 0x44, 0x64, 0x4D, 0x4A, + 0x6F, 0x44, 0x49, 0x77, 0x4F, 0x76, 0x71, 0x66, 0x77, 0x50, 0x4B, 0x63, 0x78, 0x52, 0x49, 0x71, 0x4C, 0x68, 0x79, 0x31, 0x42, 0x44, 0x50, 0x61, 0x70, 0x44, 0x67, 0x52, 0x61, 0x74, 0x37, 0x47, 0x47, 0x50, 0x5A, 0x48, 0x4F, 0x69, 0x4A, 0x42, + 0x68, 0x79, 0x4C, 0x38, 0x78, 0x49, 0x6B, 0x6F, 0x56, 0x4E, 0x69, 0x4D, 0x70, 0x54, 0x41, 0x4B, 0x2B, 0x42, 0x63, 0x57, 0x0A, 0x79, 0x71, 0x77, 0x33, 0x2F, 0x58, 0x6D, 0x6E, 0x6B, 0x52, 0x64, 0x34, 0x4F, 0x4A, 0x6D, 0x74, 0x57, 0x4F, 0x32, + 0x79, 0x33, 0x73, 0x79, 0x4A, 0x66, 0x51, 0x4F, 0x63, 0x73, 0x34, 0x6C, 0x6C, 0x35, 0x2B, 0x4D, 0x37, 0x73, 0x53, 0x4B, 0x47, 0x6A, 0x77, 0x5A, 0x74, 0x65, 0x41, 0x66, 0x39, 0x6B, 0x52, 0x4A, 0x2F, 0x73, 0x47, 0x73, 0x63, 0x69, 0x51, 0x33, + 0x35, 0x75, 0x4D, 0x74, 0x30, 0x57, 0x77, 0x66, 0x43, 0x79, 0x50, 0x51, 0x31, 0x30, 0x57, 0x52, 0x6A, 0x0A, 0x65, 0x75, 0x6C, 0x75, 0x6D, 0x69, 0x6A, 0x57, 0x4D, 0x4C, 0x33, 0x6D, 0x47, 0x39, 0x30, 0x56, 0x72, 0x34, 0x54, 0x71, 0x6E, 0x4D, + 0x66, 0x4B, 0x39, 0x51, 0x37, 0x71, 0x38, 0x6C, 0x30, 0x70, 0x68, 0x34, 0x39, 0x70, 0x63, 0x7A, 0x6D, 0x2B, 0x4C, 0x69, 0x52, 0x76, 0x52, 0x53, 0x47, 0x73, 0x78, 0x64, 0x52, 0x70, 0x4A, 0x51, 0x61, 0x44, 0x72, 0x58, 0x70, 0x49, 0x68, 0x52, + 0x4D, 0x73, 0x44, 0x51, 0x61, 0x34, 0x62, 0x48, 0x6C, 0x57, 0x2F, 0x4B, 0x4E, 0x6E, 0x0A, 0x4D, 0x6F, 0x48, 0x31, 0x56, 0x36, 0x58, 0x4B, 0x56, 0x30, 0x4A, 0x70, 0x36, 0x56, 0x77, 0x6B, 0x59, 0x65, 0x2F, 0x69, 0x4D, 0x42, 0x68, 0x4F, 0x52, + 0x4A, 0x68, 0x56, 0x62, 0x33, 0x72, 0x43, 0x6B, 0x39, 0x67, 0x5A, 0x74, 0x74, 0x35, 0x38, 0x52, 0x34, 0x6F, 0x52, 0x54, 0x6B, 0x6C, 0x48, 0x32, 0x79, 0x69, 0x55, 0x41, 0x67, 0x75, 0x55, 0x53, 0x69, 0x7A, 0x35, 0x45, 0x74, 0x42, 0x50, 0x36, + 0x44, 0x46, 0x2B, 0x62, 0x48, 0x71, 0x2F, 0x70, 0x6A, 0x2B, 0x62, 0x0A, 0x4F, 0x54, 0x30, 0x43, 0x46, 0x71, 0x4D, 0x59, 0x73, 0x32, 0x65, 0x73, 0x57, 0x7A, 0x38, 0x73, 0x67, 0x79, 0x74, 0x6E, 0x4F, 0x59, 0x46, 0x63, 0x75, 0x58, 0x36, 0x55, + 0x31, 0x57, 0x54, 0x64, 0x6E, 0x6F, 0x39, 0x75, 0x72, 0x75, 0x68, 0x38, 0x57, 0x37, 0x54, 0x58, 0x61, 0x6B, 0x64, 0x49, 0x31, 0x33, 0x36, 0x7A, 0x31, 0x43, 0x32, 0x4F, 0x56, 0x6E, 0x5A, 0x4F, 0x7A, 0x32, 0x6E, 0x78, 0x62, 0x6B, 0x52, 0x73, + 0x31, 0x43, 0x54, 0x71, 0x6A, 0x53, 0x53, 0x68, 0x0A, 0x47, 0x4C, 0x2B, 0x39, 0x56, 0x2F, 0x36, 0x70, 0x6D, 0x54, 0x57, 0x31, 0x32, 0x78, 0x42, 0x33, 0x75, 0x44, 0x31, 0x49, 0x75, 0x74, 0x62, 0x42, 0x35, 0x2F, 0x45, 0x6A, 0x50, 0x74, 0x66, + 0x66, 0x68, 0x5A, 0x30, 0x6E, 0x50, 0x4E, 0x52, 0x41, 0x76, 0x51, 0x6F, 0x4D, 0x76, 0x66, 0x58, 0x6E, 0x6A, 0x53, 0x58, 0x57, 0x67, 0x58, 0x53, 0x48, 0x52, 0x74, 0x51, 0x70, 0x64, 0x61, 0x4A, 0x43, 0x62, 0x50, 0x64, 0x7A, 0x69, 0x65, 0x64, + 0x39, 0x76, 0x33, 0x70, 0x4B, 0x0A, 0x48, 0x39, 0x4D, 0x69, 0x79, 0x52, 0x56, 0x56, 0x7A, 0x39, 0x39, 0x76, 0x66, 0x46, 0x58, 0x51, 0x70, 0x49, 0x73, 0x48, 0x45, 0x54, 0x64, 0x66, 0x67, 0x36, 0x59, 0x6D, 0x56, 0x36, 0x59, 0x42, 0x57, 0x33, + 0x37, 0x2B, 0x57, 0x47, 0x67, 0x48, 0x71, 0x65, 0x6C, 0x36, 0x32, 0x62, 0x6E, 0x6F, 0x2F, 0x31, 0x41, 0x66, 0x71, 0x38, 0x4B, 0x30, 0x77, 0x4D, 0x37, 0x6F, 0x36, 0x76, 0x30, 0x50, 0x76, 0x59, 0x31, 0x4E, 0x75, 0x4C, 0x78, 0x78, 0x41, 0x67, + 0x4D, 0x42, 0x0A, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x46, 0x37, 0x2B, 0x33, 0x4D, 0x32, 0x49, 0x30, 0x68, 0x78, 0x6B, 0x6A, 0x6B, 0x34, + 0x39, 0x63, 0x55, 0x4C, 0x71, 0x63, 0x57, 0x6B, 0x2B, 0x57, 0x59, 0x41, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x0A, + 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, + 0x41, 0x67, 0x45, 0x41, 0x55, 0x6F, 0x4B, 0x73, 0x49, 0x54, 0x51, 0x66, 0x49, 0x2F, 0x4B, 0x69, 0x32, 0x50, 0x6D, 0x34, 0x72, 0x7A, 0x63, 0x32, 0x49, 0x49, 0x6E, 0x52, 0x4E, 0x77, 0x50, 0x57, 0x61, 0x5A, 0x2B, 0x34, 0x0A, 0x59, 0x52, 0x43, + 0x36, 0x6F, 0x6A, 0x47, 0x59, 0x57, 0x55, 0x66, 0x6F, 0x30, 0x51, 0x30, 0x6C, 0x48, 0x68, 0x56, 0x42, 0x44, 0x4F, 0x41, 0x71, 0x56, 0x64, 0x56, 0x58, 0x55, 0x73, 0x76, 0x34, 0x35, 0x4D, 0x64, 0x70, 0x6F, 0x78, 0x31, 0x4E, 0x63, 0x51, 0x4A, + 0x65, 0x58, 0x79, 0x46, 0x46, 0x59, 0x45, 0x68, 0x63, 0x43, 0x59, 0x35, 0x4A, 0x45, 0x4D, 0x45, 0x45, 0x33, 0x4B, 0x6C, 0x69, 0x61, 0x77, 0x4C, 0x77, 0x51, 0x38, 0x68, 0x4F, 0x6E, 0x54, 0x68, 0x4A, 0x0A, 0x64, 0x4D, 0x6B, 0x79, 0x63, 0x46, + 0x52, 0x74, 0x77, 0x55, 0x66, 0x38, 0x6A, 0x72, 0x51, 0x32, 0x6E, 0x74, 0x53, 0x63, 0x76, 0x64, 0x30, 0x67, 0x31, 0x6C, 0x50, 0x4A, 0x47, 0x4B, 0x6D, 0x31, 0x56, 0x72, 0x6C, 0x32, 0x69, 0x35, 0x56, 0x6E, 0x5A, 0x75, 0x36, 0x39, 0x6D, 0x50, + 0x36, 0x75, 0x37, 0x37, 0x35, 0x75, 0x2B, 0x32, 0x44, 0x32, 0x2F, 0x56, 0x6E, 0x47, 0x4B, 0x68, 0x73, 0x2F, 0x49, 0x30, 0x71, 0x55, 0x4A, 0x44, 0x41, 0x6E, 0x79, 0x49, 0x6D, 0x38, 0x0A, 0x36, 0x30, 0x51, 0x6B, 0x6D, 0x73, 0x73, 0x39, 0x76, + 0x6B, 0x2F, 0x56, 0x65, 0x73, 0x36, 0x4F, 0x46, 0x38, 0x74, 0x69, 0x77, 0x64, 0x6E, 0x65, 0x48, 0x67, 0x35, 0x36, 0x2F, 0x30, 0x4F, 0x47, 0x4E, 0x46, 0x4B, 0x38, 0x59, 0x54, 0x38, 0x38, 0x58, 0x37, 0x76, 0x5A, 0x64, 0x72, 0x52, 0x54, 0x76, + 0x4A, 0x65, 0x7A, 0x2F, 0x6F, 0x70, 0x4D, 0x45, 0x69, 0x34, 0x72, 0x38, 0x39, 0x66, 0x4F, 0x34, 0x61, 0x4C, 0x2F, 0x33, 0x58, 0x74, 0x77, 0x2B, 0x7A, 0x75, 0x68, 0x0A, 0x54, 0x61, 0x52, 0x6A, 0x41, 0x76, 0x30, 0x34, 0x6C, 0x35, 0x55, 0x2F, + 0x42, 0x58, 0x43, 0x67, 0x61, 0x39, 0x39, 0x69, 0x67, 0x55, 0x4F, 0x4C, 0x74, 0x46, 0x6B, 0x4E, 0x53, 0x6F, 0x78, 0x55, 0x6E, 0x4D, 0x57, 0x37, 0x67, 0x5A, 0x2F, 0x4E, 0x66, 0x61, 0x58, 0x76, 0x43, 0x79, 0x55, 0x65, 0x4F, 0x69, 0x44, 0x62, + 0x48, 0x50, 0x77, 0x66, 0x6D, 0x47, 0x63, 0x43, 0x43, 0x74, 0x52, 0x7A, 0x52, 0x42, 0x50, 0x62, 0x55, 0x59, 0x51, 0x61, 0x56, 0x51, 0x4E, 0x57, 0x0A, 0x34, 0x41, 0x42, 0x2B, 0x64, 0x41, 0x62, 0x2F, 0x4F, 0x4D, 0x52, 0x79, 0x48, 0x64, 0x4F, + 0x6F, 0x50, 0x32, 0x67, 0x78, 0x58, 0x64, 0x4D, 0x4A, 0x78, 0x79, 0x36, 0x4D, 0x57, 0x32, 0x50, 0x67, 0x36, 0x4E, 0x77, 0x65, 0x30, 0x75, 0x78, 0x68, 0x48, 0x76, 0x4C, 0x65, 0x35, 0x65, 0x2F, 0x32, 0x6D, 0x58, 0x5A, 0x67, 0x4C, 0x52, 0x36, + 0x55, 0x63, 0x6E, 0x48, 0x47, 0x43, 0x79, 0x6F, 0x79, 0x78, 0x35, 0x4A, 0x4F, 0x31, 0x55, 0x62, 0x58, 0x48, 0x66, 0x6D, 0x70, 0x0A, 0x47, 0x51, 0x72, 0x49, 0x2B, 0x70, 0x58, 0x4F, 0x62, 0x53, 0x4F, 0x59, 0x71, 0x67, 0x73, 0x34, 0x72, 0x5A, + 0x70, 0x57, 0x44, 0x57, 0x2B, 0x4E, 0x38, 0x54, 0x45, 0x41, 0x69, 0x4D, 0x45, 0x58, 0x6E, 0x4D, 0x30, 0x5A, 0x4E, 0x6A, 0x58, 0x2B, 0x56, 0x56, 0x4F, 0x67, 0x34, 0x44, 0x77, 0x7A, 0x58, 0x35, 0x5A, 0x65, 0x34, 0x6A, 0x4C, 0x70, 0x33, 0x7A, + 0x4F, 0x37, 0x42, 0x6B, 0x71, 0x70, 0x32, 0x49, 0x52, 0x7A, 0x7A, 0x6E, 0x66, 0x53, 0x78, 0x71, 0x78, 0x78, 0x0A, 0x34, 0x56, 0x79, 0x6A, 0x48, 0x51, 0x79, 0x37, 0x43, 0x74, 0x39, 0x66, 0x34, 0x71, 0x4E, 0x78, 0x32, 0x4E, 0x6F, 0x33, 0x57, + 0x71, 0x42, 0x34, 0x4B, 0x2F, 0x54, 0x55, 0x66, 0x65, 0x74, 0x32, 0x37, 0x66, 0x4A, 0x68, 0x63, 0x4B, 0x56, 0x6C, 0x6D, 0x74, 0x4F, 0x4A, 0x4E, 0x42, 0x69, 0x72, 0x2B, 0x33, 0x49, 0x2B, 0x31, 0x37, 0x51, 0x39, 0x65, 0x56, 0x7A, 0x59, 0x48, + 0x36, 0x45, 0x7A, 0x65, 0x39, 0x6D, 0x43, 0x55, 0x41, 0x79, 0x54, 0x46, 0x36, 0x70, 0x73, 0x0A, 0x33, 0x4D, 0x4B, 0x43, 0x75, 0x77, 0x4A, 0x58, 0x4E, 0x71, 0x2B, 0x59, 0x4A, 0x79, 0x6F, 0x35, 0x55, 0x4F, 0x47, 0x77, 0x69, 0x66, 0x55, 0x6C, + 0x6C, 0x33, 0x35, 0x48, 0x61, 0x42, 0x43, 0x30, 0x37, 0x48, 0x50, 0x4B, 0x73, 0x35, 0x66, 0x52, 0x4A, 0x4E, 0x7A, 0x32, 0x59, 0x71, 0x41, 0x6F, 0x30, 0x37, 0x57, 0x6A, 0x75, 0x47, 0x53, 0x33, 0x69, 0x47, 0x4A, 0x43, 0x7A, 0x35, 0x31, 0x54, + 0x7A, 0x5A, 0x6D, 0x2B, 0x5A, 0x47, 0x69, 0x50, 0x54, 0x78, 0x34, 0x53, 0x0A, 0x53, 0x50, 0x66, 0x53, 0x4B, 0x63, 0x4F, 0x59, 0x4B, 0x4D, 0x72, 0x79, 0x4D, 0x67, 0x75, 0x54, 0x6A, 0x43, 0x6C, 0x50, 0x50, 0x47, 0x41, 0x79, 0x7A, 0x51, 0x57, + 0x57, 0x59, 0x65, 0x7A, 0x79, 0x72, 0x2F, 0x36, 0x7A, 0x63, 0x43, 0x77, 0x75, 0x70, 0x76, 0x49, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x42, 0x4A, 0x43, 0x41, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4A, + 0x54, 0x43, 0x43, 0x41, 0x61, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x4C, 0x42, 0x63, 0x49, 0x66, 0x57, 0x51, 0x71, 0x77, 0x50, 0x36, 0x46, 0x47, 0x46, 0x6B, 0x47, 0x7A, 0x37, 0x52, 0x4B, 0x36, 0x7A, 0x41, 0x4B, 0x42, + 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x55, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x0A, 0x54, 0x6A, 0x45, 0x6D, 0x4D, 0x43, 0x51, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x64, 0x51, 0x6B, 0x56, 0x4A, 0x53, 0x6B, 0x6C, 0x4F, 0x52, 0x79, 0x42, 0x44, 0x52, 0x56, 0x4A, 0x55, 0x53, 0x55, 0x5A, 0x4A, 0x51, 0x30, 0x46, 0x55, 0x52, 0x53, 0x42, 0x42, 0x56, 0x56, 0x52, 0x49, + 0x54, 0x31, 0x4A, 0x4A, 0x56, 0x46, 0x6B, 0x78, 0x48, 0x54, 0x41, 0x62, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x46, 0x45, 0x4A, 0x4B, 0x51, 0x30, 0x45, 0x67, 0x0A, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x49, 0x46, 0x4A, + 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x45, 0x35, 0x4D, 0x54, 0x49, 0x78, 0x4F, 0x54, 0x41, 0x7A, 0x4D, 0x54, 0x67, 0x79, 0x4D, 0x56, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x30, 0x4D, 0x54, 0x49, + 0x78, 0x4D, 0x6A, 0x41, 0x7A, 0x4D, 0x54, 0x67, 0x79, 0x4D, 0x56, 0x6F, 0x77, 0x56, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x4A, 0x6A, 0x41, 0x6B, 0x42, 0x67, + 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x55, 0x4A, 0x46, 0x53, 0x55, 0x70, 0x4A, 0x54, 0x6B, 0x63, 0x67, 0x51, 0x30, 0x56, 0x53, 0x56, 0x45, 0x6C, 0x47, 0x53, 0x55, 0x4E, 0x42, 0x56, 0x45, 0x55, 0x67, 0x51, 0x56, 0x56, 0x55, 0x53, 0x45, + 0x39, 0x53, 0x53, 0x56, 0x52, 0x5A, 0x4D, 0x52, 0x30, 0x77, 0x47, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x52, 0x43, 0x0A, 0x53, 0x6B, 0x4E, 0x42, 0x49, 0x45, 0x64, 0x73, 0x62, 0x32, 0x4A, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, + 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x6A, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, + 0x4A, 0x33, 0x4C, 0x67, 0x4A, 0x47, 0x4E, 0x55, 0x32, 0x65, 0x31, 0x75, 0x56, 0x43, 0x78, 0x41, 0x2F, 0x6A, 0x6C, 0x0A, 0x53, 0x52, 0x39, 0x42, 0x49, 0x67, 0x6D, 0x77, 0x55, 0x56, 0x4A, 0x59, 0x31, 0x69, 0x73, 0x30, 0x6A, 0x38, 0x55, 0x53, + 0x52, 0x68, 0x54, 0x46, 0x69, 0x79, 0x38, 0x73, 0x68, 0x50, 0x38, 0x73, 0x62, 0x71, 0x6A, 0x56, 0x38, 0x51, 0x6E, 0x6A, 0x41, 0x79, 0x45, 0x55, 0x78, 0x45, 0x4D, 0x39, 0x66, 0x4D, 0x45, 0x73, 0x78, 0x45, 0x74, 0x71, 0x53, 0x73, 0x33, 0x70, + 0x68, 0x2B, 0x42, 0x39, 0x39, 0x69, 0x4B, 0x2B, 0x2B, 0x6B, 0x70, 0x52, 0x75, 0x44, 0x43, 0x4B, 0x0A, 0x2F, 0x65, 0x48, 0x65, 0x47, 0x42, 0x49, 0x4B, 0x39, 0x6B, 0x65, 0x33, 0x35, 0x78, 0x65, 0x2F, 0x4A, 0x34, 0x72, 0x55, 0x51, 0x55, 0x79, + 0x57, 0x50, 0x47, 0x43, 0x57, 0x77, 0x66, 0x30, 0x56, 0x48, 0x4B, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4E, 0x4A, 0x4B, 0x73, 0x56, 0x46, 0x2F, 0x42, 0x76, 0x44, + 0x52, 0x67, 0x68, 0x39, 0x4F, 0x62, 0x6C, 0x2B, 0x72, 0x67, 0x2F, 0x78, 0x49, 0x0A, 0x31, 0x4C, 0x43, 0x52, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, + 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, + 0x67, 0x41, 0x4D, 0x47, 0x55, 0x43, 0x4D, 0x42, 0x71, 0x38, 0x0A, 0x57, 0x39, 0x66, 0x2B, 0x71, 0x64, 0x4A, 0x55, 0x44, 0x6B, 0x70, 0x64, 0x30, 0x6D, 0x32, 0x78, 0x51, 0x4E, 0x7A, 0x30, 0x51, 0x39, 0x58, 0x53, 0x53, 0x70, 0x6B, 0x5A, 0x45, + 0x6C, 0x61, 0x41, 0x39, 0x34, 0x4D, 0x30, 0x34, 0x54, 0x56, 0x4F, 0x53, 0x47, 0x30, 0x45, 0x44, 0x31, 0x63, 0x78, 0x4D, 0x44, 0x41, 0x74, 0x73, 0x61, 0x71, 0x64, 0x41, 0x7A, 0x6A, 0x62, 0x42, 0x67, 0x49, 0x78, 0x41, 0x4D, 0x76, 0x4D, 0x68, + 0x31, 0x50, 0x4C, 0x65, 0x74, 0x38, 0x67, 0x0A, 0x55, 0x58, 0x4F, 0x51, 0x77, 0x4B, 0x68, 0x62, 0x59, 0x64, 0x44, 0x46, 0x55, 0x44, 0x6E, 0x39, 0x68, 0x66, 0x37, 0x42, 0x34, 0x33, 0x6A, 0x34, 0x70, 0x74, 0x5A, 0x4C, 0x76, 0x5A, 0x75, 0x48, + 0x6A, 0x77, 0x2F, 0x6C, 0x31, 0x6C, 0x4F, 0x57, 0x71, 0x7A, 0x7A, 0x49, 0x51, 0x4E, 0x70, 0x68, 0x39, 0x31, 0x4F, 0x6A, 0x39, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x74, 0x69, 0x67, 0x6F, 0x20, 0x50, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6E, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x45, 0x34, 0x36, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4F, 0x6A, 0x43, 0x43, 0x41, 0x63, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x51, 0x76, 0x4C, 0x4D, 0x32, 0x68, 0x74, 0x70, + 0x4E, 0x30, 0x52, 0x66, 0x46, 0x66, 0x35, 0x31, 0x4B, 0x42, 0x43, 0x34, 0x39, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x66, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x48, 0x0A, 0x51, 0x6A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x50, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x47, 0x6C, 0x6E, 0x62, 0x79, 0x42, 0x4D, 0x61, 0x57, 0x31, + 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x31, 0x54, 0x5A, 0x57, 0x4E, 0x30, 0x61, 0x57, 0x64, 0x76, 0x49, 0x46, 0x42, 0x31, 0x59, 0x6D, 0x78, 0x70, 0x59, 0x79, 0x42, + 0x54, 0x5A, 0x58, 0x4A, 0x32, 0x0A, 0x5A, 0x58, 0x49, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x56, 0x75, 0x64, 0x47, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x46, 0x4E, 0x44, + 0x59, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x7A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x59, 0x77, 0x4D, 0x7A, 0x49, 0x78, 0x4D, 0x6A, 0x4D, 0x31, 0x4F, 0x54, + 0x55, 0x35, 0x0A, 0x57, 0x6A, 0x42, 0x66, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x48, 0x51, 0x6A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x50, 0x55, + 0x32, 0x56, 0x6A, 0x64, 0x47, 0x6C, 0x6E, 0x62, 0x79, 0x42, 0x4D, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x31, 0x54, 0x5A, 0x57, 0x4E, 0x30, 0x0A, + 0x61, 0x57, 0x64, 0x76, 0x49, 0x46, 0x42, 0x31, 0x59, 0x6D, 0x78, 0x70, 0x59, 0x79, 0x42, 0x54, 0x5A, 0x58, 0x4A, 0x32, 0x5A, 0x58, 0x49, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x56, 0x75, 0x64, 0x47, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, + 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x46, 0x4E, 0x44, 0x59, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x0A, 0x67, 0x51, 0x51, + 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x52, 0x32, 0x2B, 0x70, 0x6D, 0x70, 0x62, 0x69, 0x44, 0x74, 0x2B, 0x64, 0x64, 0x33, 0x34, 0x77, 0x63, 0x37, 0x71, 0x4E, 0x73, 0x39, 0x58, 0x7A, 0x6A, 0x6F, 0x71, 0x31, 0x57, 0x6D, 0x56, 0x6B, 0x2F, + 0x57, 0x53, 0x4F, 0x72, 0x73, 0x66, 0x79, 0x32, 0x71, 0x77, 0x37, 0x4C, 0x46, 0x65, 0x65, 0x79, 0x5A, 0x59, 0x58, 0x38, 0x51, 0x65, 0x63, 0x63, 0x43, 0x57, 0x76, 0x6B, 0x45, 0x4E, 0x2F, 0x55, 0x30, 0x0A, 0x4E, 0x53, 0x74, 0x33, 0x7A, 0x6E, + 0x38, 0x67, 0x6A, 0x31, 0x4B, 0x6A, 0x41, 0x49, 0x6E, 0x73, 0x31, 0x61, 0x65, 0x69, 0x62, 0x56, 0x76, 0x6A, 0x53, 0x35, 0x4B, 0x54, 0x6F, 0x49, 0x44, 0x31, 0x41, 0x5A, 0x54, 0x63, 0x38, 0x47, 0x67, 0x48, 0x48, 0x73, 0x33, 0x75, 0x2F, 0x69, + 0x56, 0x53, 0x74, 0x53, 0x42, 0x44, 0x48, 0x42, 0x76, 0x2B, 0x36, 0x78, 0x6E, 0x4F, 0x51, 0x36, 0x4F, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x52, 0x49, + 0x74, 0x70, 0x4D, 0x57, 0x66, 0x46, 0x4C, 0x58, 0x79, 0x59, 0x34, 0x71, 0x70, 0x33, 0x57, 0x37, 0x75, 0x73, 0x4E, 0x77, 0x2F, 0x75, 0x70, 0x59, 0x54, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, + 0x41, 0x4D, 0x43, 0x41, 0x59, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x7A, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, + 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6E, 0x41, 0x44, 0x42, 0x6B, 0x41, 0x6A, 0x41, 0x6E, 0x37, 0x71, 0x52, 0x61, 0x71, 0x43, 0x47, 0x37, 0x36, 0x55, 0x65, 0x58, 0x6C, 0x49, 0x6D, 0x6C, 0x64, 0x43, 0x42, 0x74, 0x65, 0x55, 0x2F, 0x49, + 0x76, 0x5A, 0x4E, 0x65, 0x57, 0x42, 0x6A, 0x37, 0x4C, 0x52, 0x6F, 0x41, 0x61, 0x73, 0x6D, 0x34, 0x50, 0x64, 0x43, 0x6B, 0x54, 0x30, 0x52, 0x48, 0x0A, 0x6C, 0x41, 0x46, 0x57, 0x6F, 0x76, 0x67, 0x7A, 0x4A, 0x51, 0x78, 0x43, 0x33, 0x36, 0x6F, + 0x43, 0x4D, 0x42, 0x33, 0x71, 0x34, 0x53, 0x36, 0x49, 0x4C, 0x75, 0x48, 0x35, 0x70, 0x78, 0x30, 0x43, 0x4D, 0x6B, 0x37, 0x79, 0x6E, 0x32, 0x78, 0x56, 0x64, 0x4F, 0x4F, 0x75, 0x72, 0x76, 0x75, 0x6C, 0x47, 0x75, 0x37, 0x74, 0x30, 0x76, 0x7A, + 0x43, 0x41, 0x78, 0x48, 0x72, 0x52, 0x56, 0x78, 0x67, 0x45, 0x44, 0x31, 0x63, 0x66, 0x35, 0x6B, 0x44, 0x57, 0x32, 0x31, 0x55, 0x0A, 0x53, 0x41, 0x47, 0x4B, 0x63, 0x77, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, + 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x74, 0x69, 0x67, 0x6F, 0x20, 0x50, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, + 0x41, 0x75, 0x74, 0x68, 0x65, 0x6E, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x34, 0x36, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x69, 0x6A, 0x43, 0x43, 0x41, 0x33, 0x4B, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x64, 0x59, + 0x33, 0x39, 0x69, 0x36, 0x35, 0x38, 0x42, 0x77, 0x44, 0x36, 0x71, 0x53, 0x57, 0x6E, 0x34, 0x63, 0x65, 0x74, 0x46, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, + 0x42, 0x66, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x48, 0x51, 0x6A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x50, 0x55, 0x32, 0x56, 0x6A, 0x64, + 0x47, 0x6C, 0x6E, 0x62, 0x79, 0x42, 0x4D, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x79, 0x31, 0x54, 0x5A, 0x57, 0x4E, 0x30, 0x61, 0x57, 0x64, 0x76, 0x49, + 0x46, 0x42, 0x31, 0x59, 0x6D, 0x78, 0x70, 0x59, 0x79, 0x42, 0x54, 0x0A, 0x5A, 0x58, 0x4A, 0x32, 0x5A, 0x58, 0x49, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, 0x56, 0x75, 0x64, 0x47, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, + 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x53, 0x4E, 0x44, 0x59, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x7A, 0x49, 0x79, 0x4D, 0x44, 0x41, 0x77, 0x4D, 0x44, 0x41, 0x77, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x59, 0x77, + 0x4D, 0x7A, 0x49, 0x78, 0x4D, 0x6A, 0x4D, 0x31, 0x0A, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x42, 0x66, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x48, 0x51, 0x6A, 0x45, 0x59, 0x4D, 0x42, 0x59, + 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x50, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x47, 0x6C, 0x6E, 0x62, 0x79, 0x42, 0x4D, 0x61, 0x57, 0x31, 0x70, 0x64, 0x47, 0x56, 0x6B, 0x4D, 0x54, 0x59, 0x77, 0x4E, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x44, 0x45, 0x79, 0x31, 0x54, 0x0A, 0x5A, 0x57, 0x4E, 0x30, 0x61, 0x57, 0x64, 0x76, 0x49, 0x46, 0x42, 0x31, 0x59, 0x6D, 0x78, 0x70, 0x59, 0x79, 0x42, 0x54, 0x5A, 0x58, 0x4A, 0x32, 0x5A, 0x58, 0x49, 0x67, 0x51, 0x58, 0x56, 0x30, 0x61, 0x47, + 0x56, 0x75, 0x64, 0x47, 0x6C, 0x6A, 0x59, 0x58, 0x52, 0x70, 0x62, 0x32, 0x34, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x53, 0x4E, 0x44, 0x59, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, + 0x62, 0x33, 0x0A, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x43, 0x54, 0x76, 0x74, 0x55, 0x32, 0x55, 0x6E, 0x58, 0x59, 0x41, + 0x53, 0x4F, 0x67, 0x48, 0x45, 0x64, 0x43, 0x53, 0x65, 0x35, 0x6A, 0x74, 0x72, 0x63, 0x68, 0x2F, 0x63, 0x53, 0x56, 0x31, 0x55, 0x67, 0x72, 0x4A, 0x6E, 0x77, 0x55, 0x55, 0x78, 0x44, 0x61, 0x65, 0x66, 0x30, 0x72, 0x74, 0x79, 0x32, 0x6B, 0x0A, + 0x31, 0x43, 0x7A, 0x36, 0x36, 0x6A, 0x4C, 0x64, 0x53, 0x63, 0x4B, 0x35, 0x76, 0x51, 0x39, 0x49, 0x50, 0x58, 0x74, 0x61, 0x6D, 0x46, 0x53, 0x76, 0x6E, 0x6C, 0x30, 0x78, 0x64, 0x45, 0x38, 0x48, 0x2F, 0x46, 0x41, 0x68, 0x33, 0x61, 0x54, 0x50, + 0x61, 0x45, 0x38, 0x62, 0x45, 0x6D, 0x4E, 0x74, 0x4A, 0x5A, 0x6C, 0x4D, 0x4B, 0x70, 0x6E, 0x7A, 0x53, 0x44, 0x42, 0x68, 0x2B, 0x6F, 0x46, 0x38, 0x48, 0x71, 0x63, 0x49, 0x53, 0x74, 0x77, 0x2B, 0x4B, 0x78, 0x77, 0x66, 0x0A, 0x47, 0x45, 0x78, + 0x78, 0x71, 0x6A, 0x57, 0x4D, 0x72, 0x66, 0x68, 0x75, 0x36, 0x44, 0x74, 0x4B, 0x32, 0x65, 0x57, 0x55, 0x41, 0x74, 0x61, 0x4A, 0x68, 0x42, 0x4F, 0x71, 0x62, 0x63, 0x68, 0x50, 0x4D, 0x38, 0x78, 0x51, 0x6C, 0x6A, 0x65, 0x53, 0x4D, 0x39, 0x78, + 0x66, 0x69, 0x4F, 0x65, 0x66, 0x56, 0x4E, 0x6C, 0x49, 0x38, 0x4A, 0x68, 0x44, 0x31, 0x6D, 0x62, 0x39, 0x6E, 0x78, 0x63, 0x34, 0x51, 0x38, 0x55, 0x42, 0x55, 0x51, 0x76, 0x58, 0x34, 0x79, 0x4D, 0x50, 0x0A, 0x46, 0x46, 0x31, 0x62, 0x46, 0x4F, + 0x64, 0x4C, 0x76, 0x74, 0x33, 0x30, 0x79, 0x4E, 0x6F, 0x44, 0x4E, 0x39, 0x48, 0x57, 0x4F, 0x61, 0x45, 0x68, 0x55, 0x54, 0x43, 0x44, 0x73, 0x47, 0x33, 0x58, 0x4D, 0x45, 0x36, 0x57, 0x57, 0x35, 0x48, 0x77, 0x63, 0x43, 0x53, 0x72, 0x76, 0x30, + 0x57, 0x42, 0x5A, 0x45, 0x4D, 0x4E, 0x76, 0x53, 0x45, 0x36, 0x4C, 0x7A, 0x7A, 0x70, 0x6E, 0x67, 0x33, 0x4C, 0x49, 0x4C, 0x56, 0x43, 0x4A, 0x38, 0x7A, 0x61, 0x62, 0x35, 0x76, 0x75, 0x0A, 0x5A, 0x44, 0x43, 0x51, 0x4F, 0x63, 0x32, 0x54, 0x5A, + 0x59, 0x45, 0x68, 0x4D, 0x62, 0x55, 0x6A, 0x55, 0x44, 0x4D, 0x33, 0x49, 0x75, 0x4D, 0x34, 0x37, 0x66, 0x67, 0x78, 0x4D, 0x4D, 0x78, 0x46, 0x2F, 0x6D, 0x4C, 0x35, 0x30, 0x56, 0x30, 0x79, 0x65, 0x55, 0x4B, 0x48, 0x33, 0x32, 0x72, 0x4D, 0x56, + 0x68, 0x6C, 0x41, 0x54, 0x63, 0x36, 0x71, 0x75, 0x2F, 0x6D, 0x31, 0x64, 0x6B, 0x6D, 0x55, 0x38, 0x53, 0x66, 0x34, 0x6B, 0x61, 0x57, 0x44, 0x35, 0x51, 0x61, 0x7A, 0x0A, 0x59, 0x77, 0x36, 0x41, 0x33, 0x4F, 0x41, 0x53, 0x56, 0x59, 0x43, 0x6D, + 0x4F, 0x32, 0x61, 0x30, 0x4F, 0x59, 0x63, 0x74, 0x79, 0x50, 0x44, 0x51, 0x30, 0x52, 0x54, 0x70, 0x35, 0x41, 0x31, 0x4E, 0x44, 0x76, 0x5A, 0x64, 0x56, 0x33, 0x4C, 0x46, 0x4F, 0x78, 0x78, 0x48, 0x56, 0x70, 0x33, 0x69, 0x31, 0x66, 0x75, 0x42, + 0x59, 0x59, 0x7A, 0x4D, 0x54, 0x59, 0x43, 0x51, 0x4E, 0x46, 0x75, 0x33, 0x31, 0x78, 0x52, 0x31, 0x33, 0x4E, 0x67, 0x45, 0x53, 0x4A, 0x2F, 0x41, 0x0A, 0x77, 0x53, 0x69, 0x49, 0x74, 0x4F, 0x6B, 0x63, 0x79, 0x71, 0x65, 0x78, 0x38, 0x56, 0x61, + 0x33, 0x65, 0x30, 0x6C, 0x4D, 0x57, 0x65, 0x55, 0x67, 0x46, 0x61, 0x69, 0x45, 0x41, 0x69, 0x6E, 0x36, 0x4F, 0x4A, 0x52, 0x70, 0x6D, 0x6B, 0x6B, 0x47, 0x6A, 0x38, 0x30, 0x66, 0x65, 0x52, 0x51, 0x58, 0x45, 0x67, 0x79, 0x44, 0x65, 0x74, 0x34, + 0x66, 0x73, 0x5A, 0x66, 0x75, 0x2B, 0x5A, 0x64, 0x34, 0x4B, 0x4B, 0x54, 0x49, 0x52, 0x4A, 0x4C, 0x70, 0x66, 0x53, 0x59, 0x46, 0x0A, 0x70, 0x6C, 0x68, 0x79, 0x6D, 0x33, 0x6B, 0x54, 0x32, 0x42, 0x46, 0x66, 0x72, 0x73, 0x55, 0x34, 0x59, 0x6A, + 0x52, 0x6F, 0x73, 0x6F, 0x59, 0x77, 0x6A, 0x76, 0x69, 0x51, 0x59, 0x5A, 0x34, 0x79, 0x62, 0x50, 0x55, 0x48, 0x4E, 0x73, 0x32, 0x69, 0x54, 0x47, 0x37, 0x73, 0x69, 0x6A, 0x62, 0x74, 0x38, 0x75, 0x61, 0x5A, 0x46, 0x55, 0x52, 0x77, 0x77, 0x33, + 0x79, 0x38, 0x6E, 0x44, 0x6E, 0x41, 0x74, 0x4F, 0x46, 0x72, 0x39, 0x34, 0x4D, 0x6C, 0x49, 0x31, 0x66, 0x5A, 0x0A, 0x45, 0x6F, 0x44, 0x6C, 0x53, 0x66, 0x42, 0x31, 0x44, 0x2B, 0x2B, 0x4E, 0x36, 0x78, 0x79, 0x62, 0x56, 0x43, 0x69, 0x30, 0x49, + 0x54, 0x7A, 0x38, 0x66, 0x41, 0x72, 0x2F, 0x37, 0x33, 0x74, 0x72, 0x64, 0x66, 0x2B, 0x4C, 0x48, 0x61, 0x41, 0x5A, 0x42, 0x61, 0x76, 0x36, 0x2B, 0x43, 0x75, 0x42, 0x51, 0x75, 0x67, 0x34, 0x75, 0x72, 0x76, 0x37, 0x71, 0x76, 0x30, 0x39, 0x34, + 0x50, 0x50, 0x4B, 0x33, 0x30, 0x36, 0x58, 0x6C, 0x79, 0x6E, 0x74, 0x38, 0x78, 0x68, 0x57, 0x0A, 0x36, 0x61, 0x57, 0x57, 0x72, 0x4C, 0x33, 0x44, 0x6B, 0x4A, 0x69, 0x79, 0x34, 0x50, 0x6D, 0x69, 0x31, 0x4B, 0x5A, 0x48, 0x51, 0x33, 0x78, 0x74, + 0x7A, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x56, 0x6E, 0x4E, 0x59, 0x5A, 0x4A, 0x58, 0x35, 0x6B, 0x68, 0x71, 0x77, + 0x45, 0x69, 0x6F, 0x45, 0x59, 0x6E, 0x6D, 0x68, 0x51, 0x42, 0x57, 0x49, 0x0A, 0x49, 0x55, 0x6B, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x38, + 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, + 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x43, 0x39, 0x63, 0x0A, 0x6D, 0x54, 0x7A, 0x38, 0x42, 0x6C, 0x36, 0x4D, 0x6C, 0x43, 0x35, 0x77, 0x36, 0x74, 0x49, 0x79, 0x4D, 0x59, 0x32, 0x30, 0x38, 0x46, 0x48, 0x56, 0x76, 0x41, 0x72, 0x7A, 0x5A, 0x4A, + 0x38, 0x48, 0x58, 0x74, 0x58, 0x42, 0x63, 0x32, 0x68, 0x6B, 0x65, 0x71, 0x4B, 0x35, 0x44, 0x75, 0x6A, 0x35, 0x58, 0x59, 0x55, 0x74, 0x71, 0x44, 0x64, 0x46, 0x71, 0x69, 0x6A, 0x30, 0x6C, 0x67, 0x56, 0x51, 0x59, 0x4B, 0x6C, 0x4A, 0x66, 0x70, + 0x2F, 0x69, 0x6D, 0x54, 0x59, 0x70, 0x0A, 0x45, 0x30, 0x52, 0x48, 0x61, 0x70, 0x31, 0x56, 0x49, 0x44, 0x7A, 0x59, 0x6D, 0x2F, 0x45, 0x44, 0x4D, 0x72, 0x72, 0x61, 0x51, 0x4B, 0x46, 0x7A, 0x36, 0x6F, 0x4F, 0x68, 0x74, 0x30, 0x53, 0x6D, 0x44, + 0x70, 0x6B, 0x42, 0x6D, 0x2B, 0x53, 0x38, 0x66, 0x37, 0x34, 0x54, 0x6C, 0x48, 0x37, 0x4B, 0x70, 0x68, 0x35, 0x32, 0x67, 0x44, 0x59, 0x39, 0x68, 0x41, 0x61, 0x4C, 0x4D, 0x79, 0x5A, 0x6C, 0x62, 0x63, 0x70, 0x2B, 0x6E, 0x76, 0x34, 0x66, 0x6A, + 0x46, 0x67, 0x34, 0x0A, 0x65, 0x78, 0x71, 0x44, 0x73, 0x51, 0x2B, 0x38, 0x46, 0x78, 0x47, 0x37, 0x35, 0x67, 0x62, 0x4D, 0x59, 0x2F, 0x71, 0x42, 0x38, 0x6F, 0x46, 0x4D, 0x32, 0x67, 0x73, 0x51, 0x61, 0x36, 0x48, 0x36, 0x31, 0x53, 0x69, 0x6C, + 0x7A, 0x77, 0x5A, 0x41, 0x46, 0x76, 0x39, 0x37, 0x66, 0x52, 0x68, 0x65, 0x4F, 0x52, 0x4B, 0x6B, 0x55, 0x35, 0x35, 0x2B, 0x4D, 0x6B, 0x49, 0x51, 0x70, 0x69, 0x47, 0x52, 0x71, 0x52, 0x78, 0x4F, 0x46, 0x33, 0x79, 0x45, 0x76, 0x4A, 0x2B, 0x4D, + 0x0A, 0x30, 0x65, 0x6A, 0x66, 0x35, 0x6C, 0x47, 0x35, 0x4E, 0x6B, 0x63, 0x2F, 0x6B, 0x4C, 0x6E, 0x48, 0x76, 0x41, 0x4C, 0x63, 0x57, 0x78, 0x78, 0x50, 0x44, 0x6B, 0x6A, 0x42, 0x4A, 0x59, 0x4F, 0x63, 0x43, 0x6A, 0x2B, 0x65, 0x73, 0x51, 0x4D, + 0x7A, 0x45, 0x68, 0x6F, 0x6E, 0x72, 0x50, 0x63, 0x69, 0x62, 0x43, 0x54, 0x52, 0x41, 0x55, 0x48, 0x34, 0x57, 0x41, 0x50, 0x2B, 0x4A, 0x57, 0x67, 0x69, 0x48, 0x35, 0x70, 0x61, 0x50, 0x48, 0x78, 0x73, 0x6E, 0x6E, 0x56, 0x49, 0x0A, 0x38, 0x34, + 0x48, 0x78, 0x5A, 0x6D, 0x64, 0x75, 0x54, 0x49, 0x4C, 0x41, 0x37, 0x72, 0x70, 0x58, 0x44, 0x68, 0x6A, 0x76, 0x4C, 0x70, 0x72, 0x33, 0x45, 0x74, 0x69, 0x67, 0x61, 0x2B, 0x6B, 0x46, 0x70, 0x61, 0x48, 0x70, 0x61, 0x50, 0x69, 0x38, 0x54, 0x44, + 0x38, 0x53, 0x48, 0x6B, 0x58, 0x6F, 0x55, 0x73, 0x43, 0x6A, 0x76, 0x78, 0x49, 0x6E, 0x65, 0x62, 0x6E, 0x4D, 0x4D, 0x54, 0x7A, 0x44, 0x39, 0x6A, 0x6F, 0x69, 0x46, 0x67, 0x4F, 0x67, 0x79, 0x59, 0x39, 0x6D, 0x0A, 0x70, 0x46, 0x75, 0x69, 0x54, + 0x64, 0x61, 0x42, 0x4A, 0x51, 0x62, 0x70, 0x64, 0x71, 0x51, 0x41, 0x43, 0x6A, 0x37, 0x4C, 0x7A, 0x54, 0x57, 0x62, 0x34, 0x4F, 0x45, 0x34, 0x79, 0x32, 0x42, 0x54, 0x68, 0x69, 0x68, 0x43, 0x51, 0x52, 0x78, 0x45, 0x56, 0x2B, 0x69, 0x6F, 0x72, + 0x61, 0x74, 0x46, 0x34, 0x79, 0x55, 0x51, 0x76, 0x4E, 0x73, 0x2B, 0x5A, 0x55, 0x48, 0x37, 0x47, 0x36, 0x61, 0x58, 0x44, 0x2B, 0x75, 0x35, 0x64, 0x48, 0x6E, 0x35, 0x48, 0x72, 0x77, 0x64, 0x0A, 0x56, 0x77, 0x31, 0x48, 0x72, 0x38, 0x4D, 0x76, + 0x6E, 0x34, 0x64, 0x47, 0x70, 0x2B, 0x73, 0x6D, 0x57, 0x67, 0x39, 0x57, 0x59, 0x37, 0x56, 0x69, 0x59, 0x47, 0x34, 0x41, 0x2B, 0x2B, 0x4D, 0x6E, 0x45, 0x53, 0x4C, 0x6E, 0x2F, 0x70, 0x6D, 0x50, 0x4E, 0x50, 0x57, 0x35, 0x36, 0x4D, 0x4F, 0x52, + 0x63, 0x72, 0x33, 0x59, 0x77, 0x78, 0x36, 0x35, 0x4C, 0x76, 0x4B, 0x52, 0x52, 0x46, 0x48, 0x51, 0x56, 0x38, 0x30, 0x4D, 0x4E, 0x4E, 0x56, 0x49, 0x49, 0x62, 0x2F, 0x62, 0x0A, 0x45, 0x2F, 0x46, 0x6D, 0x4A, 0x55, 0x4E, 0x53, 0x30, 0x6E, 0x41, + 0x69, 0x4E, 0x73, 0x32, 0x66, 0x78, 0x42, 0x78, 0x31, 0x49, 0x4B, 0x31, 0x6A, 0x63, 0x6D, 0x4D, 0x47, 0x44, 0x77, 0x34, 0x6E, 0x7A, 0x74, 0x4A, 0x71, 0x44, 0x62, 0x79, 0x31, 0x4F, 0x52, 0x72, 0x70, 0x30, 0x58, 0x5A, 0x36, 0x30, 0x56, 0x7A, + 0x6B, 0x35, 0x30, 0x6C, 0x4A, 0x4C, 0x56, 0x55, 0x33, 0x61, 0x50, 0x41, 0x61, 0x4F, 0x70, 0x67, 0x2B, 0x56, 0x42, 0x65, 0x48, 0x56, 0x4F, 0x6D, 0x6D, 0x0A, 0x4A, 0x31, 0x43, 0x4A, 0x65, 0x79, 0x41, 0x76, 0x50, 0x2F, 0x2B, 0x2F, 0x6F, 0x59, + 0x74, 0x4B, 0x52, 0x35, 0x6A, 0x2F, 0x4B, 0x33, 0x74, 0x4A, 0x50, 0x73, 0x4D, 0x70, 0x52, 0x6D, 0x41, 0x59, 0x51, 0x71, 0x73, 0x7A, 0x4B, 0x62, 0x72, 0x41, 0x4B, 0x62, 0x6B, 0x54, 0x69, 0x64, 0x4F, 0x49, 0x69, 0x6A, 0x6C, 0x42, 0x4F, 0x38, + 0x6E, 0x39, 0x70, 0x75, 0x30, 0x66, 0x39, 0x47, 0x42, 0x6A, 0x33, 0x39, 0x49, 0x74, 0x56, 0x51, 0x47, 0x4C, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, + 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x53, 0x4C, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x52, 0x53, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x32, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x69, 0x54, 0x43, 0x43, 0x41, 0x33, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x62, 0x37, 0x37, 0x61, 0x72, 0x58, 0x4F, 0x39, + 0x43, 0x45, 0x44, 0x69, 0x69, 0x30, 0x32, 0x2B, 0x31, 0x50, 0x64, 0x62, 0x6B, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x4F, 0x4D, 0x51, 0x73, 0x77, + 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x56, 0x55, 0x7A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x50, 0x55, 0x31, 0x4E, 0x4D, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, + 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x78, 0x54, 0x55, 0x30, 0x77, 0x75, 0x59, 0x32, 0x39, 0x74, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, + 0x53, 0x55, 0x30, 0x45, 0x67, 0x0A, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x67, 0x79, 0x4E, 0x54, 0x45, 0x32, 0x4D, 0x7A, + 0x51, 0x79, 0x4D, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x32, 0x4D, 0x44, 0x67, 0x78, 0x4F, 0x54, 0x45, 0x32, 0x4D, 0x7A, 0x51, 0x79, 0x4D, 0x56, 0x6F, 0x77, 0x54, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, + 0x4D, 0x43, 0x0A, 0x56, 0x56, 0x4D, 0x78, 0x47, 0x44, 0x41, 0x57, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, 0x31, 0x4E, 0x54, 0x54, 0x43, 0x42, 0x44, 0x62, 0x33, 0x4A, 0x77, 0x62, 0x33, 0x4A, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, + 0x6A, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x63, 0x55, 0x31, 0x4E, 0x4D, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x42, 0x55, 0x54, 0x46, 0x4D, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x76, 0x0A, + 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x6A, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, + 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4E, 0x43, 0x6B, 0x43, 0x58, 0x4A, 0x50, 0x51, 0x49, 0x67, 0x53, 0x59, 0x54, 0x34, 0x31, 0x49, 0x35, 0x37, 0x75, 0x0A, 0x39, 0x6E, 0x54, + 0x50, 0x4C, 0x33, 0x74, 0x59, 0x50, 0x63, 0x34, 0x38, 0x44, 0x52, 0x41, 0x6F, 0x6B, 0x43, 0x2B, 0x58, 0x39, 0x34, 0x78, 0x49, 0x32, 0x4B, 0x44, 0x59, 0x4A, 0x62, 0x46, 0x4D, 0x73, 0x42, 0x46, 0x4D, 0x46, 0x33, 0x4E, 0x51, 0x30, 0x43, 0x4A, + 0x4B, 0x59, 0x37, 0x75, 0x42, 0x30, 0x79, 0x6C, 0x75, 0x31, 0x62, 0x55, 0x4A, 0x50, 0x69, 0x59, 0x59, 0x66, 0x37, 0x49, 0x53, 0x66, 0x35, 0x4F, 0x59, 0x74, 0x36, 0x2F, 0x77, 0x4E, 0x72, 0x2F, 0x79, 0x0A, 0x37, 0x68, 0x69, 0x65, 0x6E, 0x44, + 0x74, 0x53, 0x78, 0x55, 0x63, 0x5A, 0x58, 0x58, 0x54, 0x7A, 0x5A, 0x47, 0x62, 0x56, 0x58, 0x63, 0x64, 0x6F, 0x74, 0x4C, 0x38, 0x62, 0x48, 0x41, 0x61, 0x6A, 0x76, 0x49, 0x39, 0x41, 0x49, 0x37, 0x59, 0x65, 0x78, 0x6F, 0x53, 0x39, 0x55, 0x63, + 0x51, 0x62, 0x4F, 0x63, 0x47, 0x56, 0x30, 0x69, 0x6E, 0x73, 0x53, 0x36, 0x35, 0x37, 0x4C, 0x62, 0x38, 0x35, 0x2F, 0x62, 0x52, 0x69, 0x33, 0x70, 0x5A, 0x37, 0x51, 0x63, 0x61, 0x63, 0x0A, 0x6F, 0x4F, 0x41, 0x47, 0x63, 0x76, 0x76, 0x77, 0x42, + 0x35, 0x63, 0x4A, 0x4F, 0x59, 0x46, 0x30, 0x72, 0x2F, 0x63, 0x30, 0x57, 0x52, 0x46, 0x58, 0x43, 0x73, 0x4A, 0x62, 0x77, 0x53, 0x54, 0x30, 0x4D, 0x58, 0x4D, 0x77, 0x67, 0x73, 0x61, 0x64, 0x75, 0x67, 0x4C, 0x33, 0x50, 0x6E, 0x78, 0x45, 0x58, + 0x34, 0x4D, 0x4E, 0x38, 0x2F, 0x48, 0x64, 0x49, 0x47, 0x6B, 0x57, 0x43, 0x56, 0x44, 0x69, 0x31, 0x46, 0x57, 0x32, 0x34, 0x49, 0x42, 0x79, 0x64, 0x6D, 0x35, 0x4D, 0x0A, 0x52, 0x37, 0x64, 0x31, 0x56, 0x56, 0x6D, 0x30, 0x55, 0x33, 0x54, 0x5A, + 0x6C, 0x4D, 0x5A, 0x42, 0x72, 0x56, 0x69, 0x4B, 0x4D, 0x57, 0x59, 0x50, 0x48, 0x71, 0x49, 0x62, 0x4B, 0x55, 0x42, 0x4F, 0x4C, 0x39, 0x39, 0x37, 0x35, 0x68, 0x59, 0x73, 0x4C, 0x66, 0x79, 0x2F, 0x37, 0x50, 0x4F, 0x30, 0x2B, 0x72, 0x34, 0x59, + 0x39, 0x70, 0x74, 0x4A, 0x31, 0x4F, 0x34, 0x46, 0x62, 0x74, 0x6B, 0x30, 0x38, 0x35, 0x7A, 0x78, 0x37, 0x41, 0x47, 0x4C, 0x30, 0x53, 0x44, 0x47, 0x0A, 0x44, 0x36, 0x43, 0x31, 0x76, 0x42, 0x64, 0x4F, 0x53, 0x48, 0x74, 0x52, 0x77, 0x76, 0x7A, + 0x70, 0x58, 0x47, 0x6B, 0x33, 0x52, 0x32, 0x61, 0x7A, 0x61, 0x50, 0x67, 0x56, 0x4B, 0x50, 0x43, 0x35, 0x30, 0x36, 0x51, 0x56, 0x7A, 0x46, 0x70, 0x50, 0x75, 0x6C, 0x4A, 0x77, 0x6F, 0x78, 0x4A, 0x46, 0x33, 0x63, 0x61, 0x36, 0x54, 0x76, 0x76, + 0x43, 0x30, 0x50, 0x65, 0x6F, 0x55, 0x69, 0x64, 0x74, 0x62, 0x6E, 0x6D, 0x31, 0x6A, 0x50, 0x78, 0x37, 0x6A, 0x4D, 0x45, 0x57, 0x0A, 0x54, 0x4F, 0x36, 0x41, 0x66, 0x37, 0x37, 0x77, 0x64, 0x72, 0x35, 0x42, 0x55, 0x78, 0x49, 0x7A, 0x72, 0x6C, + 0x6F, 0x34, 0x51, 0x71, 0x76, 0x58, 0x44, 0x7A, 0x35, 0x42, 0x6A, 0x58, 0x59, 0x48, 0x4D, 0x74, 0x57, 0x72, 0x69, 0x66, 0x5A, 0x4F, 0x5A, 0x39, 0x6D, 0x78, 0x51, 0x6E, 0x55, 0x6A, 0x62, 0x76, 0x50, 0x4E, 0x51, 0x72, 0x4C, 0x38, 0x56, 0x66, + 0x56, 0x54, 0x68, 0x78, 0x63, 0x37, 0x77, 0x44, 0x4E, 0x59, 0x38, 0x56, 0x4C, 0x53, 0x2B, 0x59, 0x43, 0x6B, 0x0A, 0x38, 0x4F, 0x6A, 0x77, 0x4F, 0x34, 0x73, 0x34, 0x7A, 0x4B, 0x54, 0x47, 0x6B, 0x48, 0x38, 0x50, 0x6E, 0x50, 0x32, 0x4C, 0x30, + 0x61, 0x50, 0x50, 0x32, 0x6F, 0x4F, 0x6E, 0x61, 0x63, 0x6C, 0x51, 0x4E, 0x74, 0x56, 0x63, 0x42, 0x64, 0x49, 0x4B, 0x51, 0x58, 0x54, 0x62, 0x59, 0x78, 0x45, 0x33, 0x77, 0x61, 0x57, 0x67, 0x6C, 0x6B, 0x73, 0x65, 0x6A, 0x42, 0x59, 0x53, 0x64, + 0x36, 0x36, 0x55, 0x4E, 0x48, 0x73, 0x65, 0x66, 0x38, 0x4A, 0x6D, 0x41, 0x4F, 0x53, 0x71, 0x0A, 0x67, 0x2B, 0x71, 0x4B, 0x6B, 0x4B, 0x33, 0x4F, 0x4E, 0x6B, 0x52, 0x4E, 0x30, 0x56, 0x48, 0x70, 0x76, 0x42, 0x2F, 0x7A, 0x61, 0x67, 0x58, 0x39, + 0x77, 0x48, 0x51, 0x66, 0x4A, 0x52, 0x6C, 0x41, 0x55, 0x57, 0x37, 0x71, 0x67, 0x6C, 0x46, 0x41, 0x33, 0x35, 0x75, 0x35, 0x43, 0x43, 0x6F, 0x47, 0x41, 0x74, 0x55, 0x6A, 0x48, 0x42, 0x50, 0x57, 0x36, 0x64, 0x76, 0x62, 0x78, 0x72, 0x42, 0x36, + 0x79, 0x33, 0x73, 0x6E, 0x6D, 0x2F, 0x76, 0x67, 0x31, 0x55, 0x59, 0x6B, 0x0A, 0x37, 0x52, 0x42, 0x4C, 0x59, 0x30, 0x75, 0x6C, 0x42, 0x59, 0x2B, 0x36, 0x75, 0x42, 0x30, 0x72, 0x70, 0x76, 0x71, 0x52, 0x34, 0x70, 0x4A, 0x53, 0x76, 0x65, 0x7A, + 0x72, 0x5A, 0x35, 0x64, 0x74, 0x6D, 0x69, 0x32, 0x66, 0x67, 0x54, 0x49, 0x46, 0x5A, 0x7A, 0x4C, 0x37, 0x53, 0x41, 0x67, 0x2F, 0x32, 0x53, 0x57, 0x34, 0x42, 0x43, 0x55, 0x76, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, + 0x68, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x0A, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, + 0x41, 0x55, 0x2B, 0x79, 0x34, 0x33, 0x37, 0x75, 0x4F, 0x45, 0x65, 0x69, 0x63, 0x75, 0x7A, 0x52, 0x6B, 0x31, 0x73, 0x54, 0x4E, 0x38, 0x2F, 0x39, 0x52, 0x45, 0x51, 0x72, 0x6B, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, + 0x59, 0x45, 0x46, 0x50, 0x73, 0x75, 0x0A, 0x4E, 0x2B, 0x37, 0x6A, 0x68, 0x48, 0x6F, 0x6E, 0x4C, 0x73, 0x30, 0x5A, 0x4E, 0x62, 0x45, 0x7A, 0x66, 0x50, 0x2F, 0x55, 0x52, 0x45, 0x4B, 0x35, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, + 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x6A, + 0x59, 0x6C, 0x74, 0x0A, 0x68, 0x45, 0x55, 0x59, 0x38, 0x55, 0x2B, 0x7A, 0x6F, 0x4F, 0x39, 0x6F, 0x70, 0x4D, 0x41, 0x64, 0x72, 0x44, 0x43, 0x38, 0x5A, 0x32, 0x61, 0x77, 0x6D, 0x73, 0x32, 0x32, 0x71, 0x79, 0x49, 0x5A, 0x5A, 0x74, 0x4D, 0x37, + 0x51, 0x62, 0x55, 0x51, 0x6E, 0x52, 0x43, 0x36, 0x63, 0x6D, 0x34, 0x70, 0x4A, 0x43, 0x41, 0x63, 0x41, 0x5A, 0x6C, 0x69, 0x30, 0x35, 0x62, 0x67, 0x34, 0x76, 0x73, 0x4D, 0x51, 0x74, 0x66, 0x68, 0x57, 0x73, 0x53, 0x57, 0x54, 0x56, 0x54, 0x4E, + 0x0A, 0x6A, 0x38, 0x70, 0x44, 0x55, 0x2F, 0x30, 0x71, 0x75, 0x4F, 0x72, 0x34, 0x5A, 0x63, 0x6F, 0x42, 0x77, 0x71, 0x31, 0x67, 0x61, 0x41, 0x61, 0x66, 0x4F, 0x52, 0x70, 0x52, 0x32, 0x65, 0x43, 0x4E, 0x4A, 0x76, 0x6B, 0x4C, 0x54, 0x71, 0x56, + 0x54, 0x4A, 0x58, 0x6F, 0x6A, 0x70, 0x42, 0x7A, 0x4F, 0x43, 0x42, 0x76, 0x66, 0x52, 0x34, 0x69, 0x79, 0x72, 0x54, 0x37, 0x67, 0x4A, 0x34, 0x65, 0x4C, 0x53, 0x59, 0x77, 0x66, 0x71, 0x55, 0x64, 0x59, 0x65, 0x35, 0x62, 0x79, 0x0A, 0x69, 0x42, + 0x30, 0x59, 0x72, 0x72, 0x50, 0x52, 0x70, 0x67, 0x71, 0x55, 0x2B, 0x74, 0x76, 0x54, 0x35, 0x54, 0x67, 0x4B, 0x61, 0x33, 0x6B, 0x53, 0x4D, 0x2F, 0x74, 0x4B, 0x57, 0x54, 0x63, 0x57, 0x51, 0x41, 0x36, 0x37, 0x33, 0x76, 0x57, 0x4A, 0x44, 0x50, + 0x46, 0x73, 0x30, 0x2F, 0x64, 0x52, 0x61, 0x31, 0x34, 0x31, 0x39, 0x64, 0x76, 0x41, 0x4A, 0x75, 0x6F, 0x53, 0x63, 0x30, 0x36, 0x70, 0x6B, 0x5A, 0x43, 0x6D, 0x46, 0x38, 0x4E, 0x73, 0x4C, 0x7A, 0x6A, 0x55, 0x0A, 0x6F, 0x33, 0x4B, 0x55, 0x51, + 0x79, 0x78, 0x69, 0x34, 0x55, 0x35, 0x63, 0x4D, 0x6A, 0x32, 0x39, 0x54, 0x48, 0x30, 0x5A, 0x52, 0x36, 0x4C, 0x44, 0x53, 0x65, 0x65, 0x57, 0x50, 0x34, 0x2B, 0x61, 0x30, 0x7A, 0x76, 0x6B, 0x45, 0x64, 0x69, 0x4C, 0x41, 0x39, 0x7A, 0x32, 0x74, + 0x6D, 0x42, 0x56, 0x47, 0x4B, 0x61, 0x42, 0x55, 0x66, 0x50, 0x68, 0x71, 0x42, 0x56, 0x71, 0x36, 0x2B, 0x41, 0x4C, 0x38, 0x42, 0x51, 0x78, 0x31, 0x72, 0x6D, 0x4D, 0x52, 0x54, 0x71, 0x6F, 0x0A, 0x45, 0x4E, 0x6A, 0x77, 0x75, 0x53, 0x66, 0x72, + 0x39, 0x38, 0x74, 0x36, 0x37, 0x77, 0x56, 0x79, 0x6C, 0x72, 0x58, 0x45, 0x6A, 0x35, 0x5A, 0x7A, 0x78, 0x4F, 0x68, 0x57, 0x63, 0x35, 0x79, 0x38, 0x61, 0x56, 0x46, 0x6A, 0x76, 0x4F, 0x39, 0x6E, 0x48, 0x45, 0x4D, 0x61, 0x58, 0x33, 0x63, 0x5A, + 0x48, 0x78, 0x6A, 0x34, 0x48, 0x43, 0x55, 0x70, 0x2B, 0x55, 0x6D, 0x5A, 0x4B, 0x62, 0x61, 0x53, 0x50, 0x61, 0x4B, 0x44, 0x4E, 0x37, 0x45, 0x67, 0x6B, 0x61, 0x69, 0x62, 0x0A, 0x4D, 0x4F, 0x6C, 0x71, 0x62, 0x4C, 0x51, 0x6A, 0x6B, 0x32, 0x55, + 0x45, 0x71, 0x78, 0x48, 0x7A, 0x44, 0x68, 0x31, 0x54, 0x4A, 0x45, 0x6C, 0x54, 0x48, 0x61, 0x45, 0x2F, 0x6E, 0x55, 0x69, 0x53, 0x45, 0x65, 0x4A, 0x39, 0x44, 0x55, 0x2F, 0x31, 0x31, 0x37, 0x32, 0x69, 0x57, 0x44, 0x35, 0x34, 0x6E, 0x52, 0x34, + 0x66, 0x4B, 0x2F, 0x34, 0x68, 0x75, 0x78, 0x6F, 0x54, 0x74, 0x72, 0x45, 0x6F, 0x5A, 0x50, 0x32, 0x77, 0x41, 0x67, 0x44, 0x48, 0x62, 0x49, 0x43, 0x69, 0x0A, 0x76, 0x52, 0x5A, 0x51, 0x49, 0x41, 0x39, 0x79, 0x67, 0x56, 0x2F, 0x4D, 0x6C, 0x50, + 0x2B, 0x37, 0x6D, 0x65, 0x61, 0x36, 0x6B, 0x4D, 0x76, 0x71, 0x2B, 0x63, 0x59, 0x4D, 0x77, 0x71, 0x37, 0x46, 0x47, 0x63, 0x34, 0x7A, 0x6F, 0x57, 0x74, 0x63, 0x75, 0x33, 0x35, 0x38, 0x4E, 0x46, 0x63, 0x58, 0x72, 0x66, 0x41, 0x2F, 0x72, 0x73, + 0x33, 0x71, 0x72, 0x35, 0x6E, 0x73, 0x4C, 0x46, 0x52, 0x2B, 0x6A, 0x4D, 0x34, 0x75, 0x45, 0x6C, 0x5A, 0x49, 0x37, 0x78, 0x63, 0x37, 0x0A, 0x50, 0x30, 0x70, 0x65, 0x59, 0x4E, 0x4C, 0x63, 0x64, 0x44, 0x61, 0x38, 0x70, 0x55, 0x4E, 0x6A, 0x79, + 0x77, 0x39, 0x62, 0x6F, 0x77, 0x4A, 0x57, 0x43, 0x5A, 0x34, 0x6B, 0x4C, 0x4F, 0x47, 0x47, 0x67, 0x59, 0x7A, 0x2B, 0x71, 0x78, 0x63, 0x73, 0x2B, 0x73, 0x6A, 0x69, 0x4D, 0x68, 0x6F, 0x36, 0x2F, 0x34, 0x55, 0x49, 0x79, 0x59, 0x4F, 0x66, 0x38, + 0x6B, 0x70, 0x49, 0x45, 0x46, 0x52, 0x33, 0x4E, 0x2B, 0x32, 0x69, 0x76, 0x45, 0x43, 0x2B, 0x35, 0x42, 0x42, 0x30, 0x0A, 0x39, 0x2B, 0x52, 0x62, 0x75, 0x37, 0x6E, 0x7A, 0x69, 0x66, 0x6D, 0x50, 0x51, 0x64, 0x6A, 0x48, 0x35, 0x46, 0x43, 0x51, + 0x4E, 0x59, 0x41, 0x2B, 0x48, 0x4C, 0x68, 0x4E, 0x6B, 0x4E, 0x50, 0x55, 0x39, 0x38, 0x4F, 0x77, 0x6F, 0x58, 0x36, 0x45, 0x79, 0x6E, 0x65, 0x53, 0x4D, 0x53, 0x79, 0x34, 0x6B, 0x4C, 0x47, 0x43, 0x65, 0x6E, 0x52, 0x4F, 0x6D, 0x78, 0x4D, 0x6D, + 0x74, 0x4E, 0x56, 0x51, 0x5A, 0x6C, 0x52, 0x34, 0x72, 0x6D, 0x41, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, + 0x53, 0x53, 0x4C, 0x2E, 0x63, 0x6F, 0x6D, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x32, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4F, 0x6A, 0x43, 0x43, 0x41, 0x63, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x46, 0x41, 0x50, 0x31, 0x71, 0x2F, 0x73, 0x33, 0x69, 0x78, 0x64, 0x41, 0x57, 0x2B, + 0x4A, 0x44, 0x73, 0x71, 0x58, 0x52, 0x78, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x4F, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, + 0x4A, 0x56, 0x0A, 0x55, 0x7A, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x50, 0x55, 0x31, 0x4E, 0x4D, 0x49, 0x45, 0x4E, 0x76, 0x63, 0x6E, 0x42, 0x76, 0x63, 0x6D, 0x46, 0x30, 0x61, 0x57, 0x39, 0x75, 0x4D, + 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x78, 0x54, 0x55, 0x30, 0x77, 0x75, 0x59, 0x32, 0x39, 0x74, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x0A, + 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x79, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x67, 0x79, 0x4E, 0x54, 0x45, 0x32, 0x4D, 0x7A, 0x4D, 0x30, 0x4F, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x32, + 0x4D, 0x44, 0x67, 0x78, 0x4F, 0x54, 0x45, 0x32, 0x4D, 0x7A, 0x4D, 0x30, 0x4E, 0x31, 0x6F, 0x77, 0x54, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x56, 0x56, 0x4D, 0x78, 0x0A, 0x47, 0x44, 0x41, + 0x57, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x44, 0x31, 0x4E, 0x54, 0x54, 0x43, 0x42, 0x44, 0x62, 0x33, 0x4A, 0x77, 0x62, 0x33, 0x4A, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6A, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, + 0x45, 0x41, 0x77, 0x77, 0x63, 0x55, 0x31, 0x4E, 0x4D, 0x4C, 0x6D, 0x4E, 0x76, 0x62, 0x53, 0x42, 0x55, 0x54, 0x46, 0x4D, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x0A, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, + 0x41, 0x79, 0x4D, 0x6A, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x45, 0x55, 0x70, 0x4E, 0x58, + 0x50, 0x36, 0x77, 0x72, 0x67, 0x6A, 0x7A, 0x68, 0x52, 0x39, 0x71, 0x4C, 0x46, 0x4E, 0x6F, 0x46, 0x73, 0x32, 0x37, 0x69, 0x6F, 0x73, 0x55, 0x38, 0x4E, 0x67, 0x43, 0x54, 0x57, 0x79, 0x0A, 0x4A, 0x47, 0x59, 0x6D, 0x61, 0x63, 0x43, 0x7A, 0x6C, + 0x64, 0x5A, 0x64, 0x6B, 0x6B, 0x41, 0x5A, 0x44, 0x73, 0x61, 0x6C, 0x45, 0x33, 0x44, 0x30, 0x37, 0x78, 0x4A, 0x52, 0x4B, 0x46, 0x33, 0x6E, 0x7A, 0x4C, 0x33, 0x35, 0x50, 0x49, 0x58, 0x42, 0x7A, 0x35, 0x53, 0x51, 0x79, 0x53, 0x76, 0x4F, 0x6B, + 0x6B, 0x4A, 0x59, 0x57, 0x57, 0x66, 0x39, 0x6C, 0x43, 0x63, 0x51, 0x5A, 0x49, 0x78, 0x50, 0x42, 0x4C, 0x46, 0x4E, 0x53, 0x65, 0x52, 0x37, 0x54, 0x35, 0x76, 0x31, 0x0A, 0x35, 0x77, 0x6A, 0x34, 0x41, 0x34, 0x6A, 0x33, 0x70, 0x38, 0x4F, 0x53, + 0x53, 0x78, 0x6C, 0x55, 0x67, 0x61, 0x4E, 0x6A, 0x4D, 0x47, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, + 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x53, 0x4A, 0x6A, 0x79, 0x2B, 0x6A, 0x36, 0x43, 0x75, 0x67, 0x46, 0x46, 0x52, 0x37, 0x0A, 0x38, 0x31, 0x61, 0x34, 0x4A, 0x6C, 0x39, 0x6E, 0x4F, 0x41, 0x75, 0x63, 0x30, 0x44, 0x41, + 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x69, 0x59, 0x38, 0x76, 0x6F, 0x2B, 0x67, 0x72, 0x6F, 0x42, 0x52, 0x55, 0x65, 0x2F, 0x4E, 0x57, 0x75, 0x43, 0x5A, 0x66, 0x5A, 0x7A, 0x67, 0x4C, 0x6E, 0x4E, 0x41, + 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x0A, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, + 0x67, 0x41, 0x4D, 0x47, 0x55, 0x43, 0x4D, 0x46, 0x58, 0x6A, 0x49, 0x6C, 0x62, 0x70, 0x31, 0x35, 0x49, 0x6B, 0x57, 0x45, 0x38, 0x65, 0x6C, 0x44, 0x49, 0x50, 0x44, 0x41, 0x49, 0x32, 0x77, 0x76, 0x32, 0x73, 0x64, 0x44, 0x4A, 0x4F, 0x34, 0x66, + 0x73, 0x63, 0x67, 0x49, 0x69, 0x6A, 0x7A, 0x50, 0x76, 0x58, 0x36, 0x79, 0x76, 0x2F, 0x4E, 0x33, 0x33, 0x77, 0x0A, 0x37, 0x64, 0x65, 0x65, 0x64, 0x57, 0x6F, 0x31, 0x64, 0x6C, 0x4A, 0x46, 0x34, 0x41, 0x49, 0x78, 0x41, 0x4D, 0x65, 0x4E, 0x62, + 0x30, 0x49, 0x67, 0x6A, 0x37, 0x36, 0x32, 0x54, 0x56, 0x6E, 0x74, 0x64, 0x30, 0x30, 0x70, 0x78, 0x43, 0x41, 0x67, 0x52, 0x57, 0x53, 0x47, 0x4F, 0x6C, 0x44, 0x47, 0x78, 0x4B, 0x30, 0x74, 0x6B, 0x2F, 0x55, 0x59, 0x66, 0x58, 0x4C, 0x74, 0x71, + 0x63, 0x2F, 0x45, 0x72, 0x46, 0x63, 0x32, 0x4B, 0x41, 0x68, 0x6C, 0x33, 0x7A, 0x78, 0x35, 0x0A, 0x5A, 0x6E, 0x36, 0x67, 0x36, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, + 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x74, 0x6F, 0x73, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x45, 0x43, 0x43, + 0x20, 0x54, 0x4C, 0x53, 0x20, 0x32, 0x30, 0x32, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, + 0x46, 0x54, 0x43, 0x43, 0x41, 0x5A, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x50, 0x5A, 0x67, 0x37, 0x70, 0x6D, 0x59, 0x39, 0x6B, 0x47, 0x50, 0x33, 0x66, 0x69, 0x5A, 0x58, 0x4F, 0x41, 0x54, 0x76, 0x41, 0x44, 0x41, 0x4B, + 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x4D, 0x4D, 0x53, 0x34, 0x77, 0x4C, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x56, 0x42, 0x0A, 0x64, 0x47, 0x39, 0x7A, 0x49, 0x46, 0x52, + 0x79, 0x64, 0x58, 0x4E, 0x30, 0x5A, 0x57, 0x52, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x41, 0x79, 0x4D, 0x44, 0x49, + 0x78, 0x4D, 0x51, 0x30, 0x77, 0x43, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x52, 0x42, 0x64, 0x47, 0x39, 0x7A, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x0A, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x45, 0x52, 0x54, + 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x54, 0x41, 0x30, 0x4D, 0x6A, 0x49, 0x77, 0x4F, 0x54, 0x49, 0x32, 0x4D, 0x6A, 0x4E, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x54, 0x41, 0x30, 0x4D, 0x54, 0x63, 0x77, 0x4F, 0x54, 0x49, 0x32, 0x4D, 0x6A, + 0x4A, 0x61, 0x4D, 0x45, 0x77, 0x78, 0x4C, 0x6A, 0x41, 0x73, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x4A, 0x55, 0x46, 0x30, 0x62, 0x33, 0x4D, 0x67, 0x0A, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x6C, 0x5A, 0x46, 0x4A, 0x76, 0x62, + 0x33, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x56, 0x45, 0x78, 0x54, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x6A, 0x45, 0x78, 0x44, 0x54, 0x41, 0x4C, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x6F, 0x4D, 0x42, 0x45, 0x46, 0x30, 0x62, 0x33, 0x4D, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x0A, 0x41, 0x6B, 0x52, 0x46, 0x4D, 0x48, 0x59, 0x77, 0x45, 0x41, 0x59, 0x48, 0x4B, 0x6F, 0x5A, 0x49, + 0x7A, 0x6A, 0x30, 0x43, 0x41, 0x51, 0x59, 0x46, 0x4B, 0x34, 0x45, 0x45, 0x41, 0x43, 0x49, 0x44, 0x59, 0x67, 0x41, 0x45, 0x6C, 0x6F, 0x5A, 0x59, 0x4B, 0x44, 0x63, 0x4B, 0x5A, 0x39, 0x43, 0x67, 0x33, 0x69, 0x51, 0x5A, 0x47, 0x65, 0x48, 0x6B, + 0x42, 0x51, 0x63, 0x66, 0x6C, 0x2B, 0x33, 0x6F, 0x5A, 0x49, 0x4B, 0x35, 0x39, 0x73, 0x52, 0x78, 0x55, 0x4D, 0x36, 0x4B, 0x0A, 0x44, 0x50, 0x2F, 0x58, 0x74, 0x58, 0x61, 0x37, 0x6F, 0x57, 0x79, 0x54, 0x62, 0x49, 0x4F, 0x69, 0x61, 0x47, 0x36, + 0x6C, 0x32, 0x62, 0x34, 0x73, 0x69, 0x4A, 0x56, 0x42, 0x7A, 0x56, 0x33, 0x64, 0x73, 0x63, 0x71, 0x44, 0x59, 0x34, 0x50, 0x4D, 0x77, 0x4C, 0x35, 0x30, 0x32, 0x65, 0x43, 0x64, 0x70, 0x4F, 0x35, 0x4B, 0x54, 0x6C, 0x62, 0x67, 0x6D, 0x43, 0x6C, + 0x42, 0x6B, 0x31, 0x49, 0x51, 0x31, 0x53, 0x51, 0x34, 0x41, 0x6A, 0x4A, 0x6E, 0x38, 0x5A, 0x51, 0x53, 0x0A, 0x62, 0x2B, 0x2F, 0x58, 0x78, 0x64, 0x34, 0x75, 0x2F, 0x52, 0x6D, 0x41, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, + 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x32, 0x4B, 0x43, 0x58, 0x57, 0x66, 0x65, + 0x42, 0x6D, 0x6D, 0x6E, 0x6F, 0x4A, 0x73, 0x6D, 0x6F, 0x37, 0x6A, 0x6A, 0x50, 0x58, 0x0A, 0x4E, 0x74, 0x4E, 0x50, 0x6F, 0x6A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, + 0x59, 0x59, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x44, 0x61, 0x41, 0x41, 0x77, 0x5A, 0x51, 0x49, 0x77, 0x57, 0x35, 0x6B, 0x70, 0x38, 0x35, 0x77, 0x78, 0x74, 0x6F, 0x6C, 0x72, 0x62, + 0x4E, 0x61, 0x39, 0x64, 0x2B, 0x46, 0x38, 0x35, 0x31, 0x46, 0x2B, 0x0A, 0x75, 0x44, 0x72, 0x4E, 0x6F, 0x7A, 0x5A, 0x66, 0x66, 0x50, 0x63, 0x38, 0x64, 0x7A, 0x37, 0x6B, 0x55, 0x4B, 0x32, 0x6F, 0x35, 0x39, 0x4A, 0x5A, 0x44, 0x43, 0x61, 0x4F, + 0x4D, 0x44, 0x74, 0x75, 0x43, 0x43, 0x72, 0x43, 0x70, 0x31, 0x72, 0x49, 0x41, 0x6A, 0x45, 0x41, 0x6D, 0x65, 0x4D, 0x4D, 0x35, 0x36, 0x50, 0x44, 0x72, 0x39, 0x4E, 0x4A, 0x4C, 0x6B, 0x61, 0x43, 0x49, 0x32, 0x5A, 0x64, 0x79, 0x51, 0x41, 0x55, + 0x45, 0x76, 0x30, 0x34, 0x39, 0x4F, 0x47, 0x59, 0x0A, 0x61, 0x33, 0x63, 0x70, 0x65, 0x74, 0x73, 0x6B, 0x7A, 0x32, 0x56, 0x41, 0x76, 0x39, 0x4C, 0x63, 0x6A, 0x42, 0x48, 0x6F, 0x39, 0x48, 0x31, 0x2F, 0x49, 0x49, 0x53, 0x70, 0x51, 0x75, 0x51, + 0x6F, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x41, 0x74, 0x6F, 0x73, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x65, + 0x64, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x52, 0x53, 0x41, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x32, 0x30, 0x32, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x5A, 0x44, 0x43, 0x43, 0x41, 0x30, 0x79, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x55, 0x39, 0x58, 0x50, 0x35, 0x68, 0x6D, + 0x54, 0x43, 0x2F, 0x73, 0x72, 0x42, 0x52, 0x4C, 0x59, 0x77, 0x69, 0x71, 0x69, 0x70, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x4D, 0x4D, 0x53, 0x34, + 0x77, 0x4C, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x0A, 0x44, 0x43, 0x56, 0x42, 0x64, 0x47, 0x39, 0x7A, 0x49, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x5A, 0x57, 0x52, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, + 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x78, 0x4D, 0x51, 0x30, 0x77, 0x43, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x41, 0x52, 0x42, 0x64, 0x47, + 0x39, 0x7A, 0x4D, 0x51, 0x73, 0x77, 0x0A, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x45, 0x52, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x54, 0x41, 0x30, 0x4D, 0x6A, 0x49, 0x77, 0x4F, 0x54, 0x49, 0x78, 0x4D, + 0x54, 0x42, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x54, 0x41, 0x30, 0x4D, 0x54, 0x63, 0x77, 0x4F, 0x54, 0x49, 0x78, 0x4D, 0x44, 0x6C, 0x61, 0x4D, 0x45, 0x77, 0x78, 0x4C, 0x6A, 0x41, 0x73, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x4A, + 0x55, 0x46, 0x30, 0x0A, 0x62, 0x33, 0x4D, 0x67, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x6C, 0x5A, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, + 0x56, 0x45, 0x78, 0x54, 0x49, 0x44, 0x49, 0x77, 0x4D, 0x6A, 0x45, 0x78, 0x44, 0x54, 0x41, 0x4C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x42, 0x45, 0x46, 0x30, 0x62, 0x33, 0x4D, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, + 0x0A, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x52, 0x46, 0x4D, 0x49, 0x49, 0x43, 0x49, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x38, + 0x41, 0x4D, 0x49, 0x49, 0x43, 0x43, 0x67, 0x4B, 0x43, 0x41, 0x67, 0x45, 0x41, 0x74, 0x6F, 0x41, 0x4F, 0x78, 0x48, 0x6D, 0x39, 0x42, 0x59, 0x78, 0x39, 0x73, 0x4B, 0x4F, 0x64, 0x54, 0x53, 0x4A, 0x4E, 0x79, 0x2F, 0x42, 0x42, 0x0A, 0x6C, 0x30, + 0x31, 0x5A, 0x34, 0x4E, 0x48, 0x2B, 0x56, 0x6F, 0x79, 0x58, 0x38, 0x74, 0x65, 0x39, 0x6A, 0x32, 0x79, 0x33, 0x49, 0x34, 0x39, 0x66, 0x31, 0x63, 0x54, 0x59, 0x51, 0x63, 0x76, 0x79, 0x41, 0x68, 0x35, 0x78, 0x35, 0x65, 0x6E, 0x32, 0x58, 0x73, + 0x73, 0x49, 0x4B, 0x6C, 0x34, 0x77, 0x38, 0x69, 0x31, 0x6D, 0x78, 0x34, 0x51, 0x62, 0x5A, 0x46, 0x63, 0x34, 0x6E, 0x58, 0x55, 0x74, 0x56, 0x73, 0x59, 0x76, 0x59, 0x65, 0x2B, 0x57, 0x2F, 0x43, 0x42, 0x47, 0x0A, 0x76, 0x65, 0x76, 0x55, 0x65, + 0x7A, 0x38, 0x2F, 0x66, 0x45, 0x63, 0x34, 0x42, 0x4B, 0x6B, 0x62, 0x71, 0x6C, 0x4C, 0x66, 0x45, 0x7A, 0x66, 0x54, 0x46, 0x52, 0x56, 0x4F, 0x76, 0x56, 0x39, 0x38, 0x72, 0x36, 0x31, 0x6A, 0x78, 0x33, 0x6E, 0x63, 0x43, 0x48, 0x76, 0x56, 0x6F, + 0x4F, 0x58, 0x33, 0x57, 0x33, 0x57, 0x73, 0x67, 0x46, 0x57, 0x5A, 0x6B, 0x6D, 0x47, 0x62, 0x7A, 0x53, 0x6F, 0x58, 0x66, 0x64, 0x75, 0x50, 0x39, 0x4C, 0x56, 0x71, 0x36, 0x68, 0x64, 0x4B, 0x0A, 0x5A, 0x43, 0x68, 0x6D, 0x46, 0x53, 0x6C, 0x73, + 0x41, 0x76, 0x46, 0x72, 0x31, 0x62, 0x71, 0x6A, 0x4D, 0x39, 0x78, 0x61, 0x5A, 0x36, 0x63, 0x46, 0x34, 0x72, 0x39, 0x6C, 0x74, 0x68, 0x61, 0x77, 0x45, 0x4F, 0x33, 0x4E, 0x55, 0x44, 0x50, 0x4A, 0x63, 0x46, 0x44, 0x73, 0x47, 0x59, 0x36, 0x77, + 0x78, 0x2F, 0x4A, 0x30, 0x57, 0x32, 0x74, 0x45, 0x78, 0x6E, 0x32, 0x57, 0x75, 0x5A, 0x67, 0x49, 0x57, 0x57, 0x62, 0x65, 0x4B, 0x51, 0x47, 0x62, 0x39, 0x43, 0x70, 0x74, 0x0A, 0x30, 0x78, 0x55, 0x36, 0x6B, 0x47, 0x70, 0x6E, 0x38, 0x62, 0x52, + 0x72, 0x5A, 0x74, 0x6B, 0x68, 0x36, 0x38, 0x72, 0x5A, 0x59, 0x6E, 0x78, 0x47, 0x45, 0x46, 0x7A, 0x65, 0x64, 0x55, 0x6C, 0x6E, 0x6E, 0x6B, 0x4C, 0x35, 0x2F, 0x6E, 0x57, 0x70, 0x6F, 0x36, 0x33, 0x2F, 0x64, 0x67, 0x70, 0x6E, 0x51, 0x4F, 0x50, + 0x46, 0x39, 0x34, 0x33, 0x48, 0x68, 0x5A, 0x70, 0x5A, 0x6E, 0x6D, 0x4B, 0x61, 0x61, 0x75, 0x31, 0x46, 0x68, 0x35, 0x68, 0x6E, 0x73, 0x74, 0x56, 0x4B, 0x0A, 0x50, 0x4E, 0x65, 0x30, 0x4F, 0x77, 0x41, 0x4E, 0x77, 0x49, 0x38, 0x66, 0x34, 0x55, + 0x44, 0x45, 0x72, 0x6D, 0x77, 0x68, 0x33, 0x45, 0x6C, 0x2B, 0x66, 0x73, 0x71, 0x79, 0x6A, 0x57, 0x32, 0x32, 0x76, 0x35, 0x4D, 0x76, 0x6F, 0x56, 0x77, 0x2B, 0x6A, 0x38, 0x72, 0x74, 0x67, 0x49, 0x35, 0x59, 0x34, 0x64, 0x74, 0x58, 0x7A, 0x34, + 0x55, 0x32, 0x4F, 0x4C, 0x4A, 0x78, 0x70, 0x41, 0x6D, 0x4D, 0x6B, 0x6F, 0x6B, 0x49, 0x69, 0x45, 0x6A, 0x78, 0x51, 0x47, 0x4D, 0x59, 0x0A, 0x73, 0x6C, 0x75, 0x4D, 0x57, 0x75, 0x50, 0x44, 0x30, 0x78, 0x65, 0x71, 0x71, 0x78, 0x6D, 0x6A, 0x4C, + 0x42, 0x76, 0x6B, 0x31, 0x63, 0x62, 0x69, 0x5A, 0x6E, 0x72, 0x58, 0x67, 0x68, 0x6D, 0x6D, 0x4F, 0x78, 0x59, 0x73, 0x4C, 0x33, 0x47, 0x48, 0x58, 0x30, 0x57, 0x65, 0x6C, 0x58, 0x4F, 0x54, 0x77, 0x6B, 0x4B, 0x42, 0x49, 0x52, 0x4F, 0x57, 0x31, + 0x35, 0x32, 0x37, 0x6B, 0x32, 0x67, 0x56, 0x2B, 0x70, 0x32, 0x6B, 0x48, 0x59, 0x7A, 0x79, 0x67, 0x65, 0x42, 0x59, 0x0A, 0x42, 0x72, 0x33, 0x4A, 0x74, 0x75, 0x50, 0x32, 0x69, 0x56, 0x32, 0x4A, 0x2B, 0x61, 0x78, 0x45, 0x6F, 0x63, 0x74, 0x72, + 0x2B, 0x68, 0x62, 0x78, 0x78, 0x31, 0x41, 0x39, 0x4A, 0x4E, 0x72, 0x33, 0x77, 0x2B, 0x53, 0x48, 0x31, 0x56, 0x62, 0x78, 0x54, 0x35, 0x41, 0x77, 0x2B, 0x6B, 0x55, 0x4A, 0x57, 0x64, 0x6F, 0x30, 0x7A, 0x75, 0x41, 0x54, 0x48, 0x41, 0x52, 0x38, + 0x41, 0x4E, 0x53, 0x62, 0x68, 0x71, 0x52, 0x41, 0x76, 0x4E, 0x6E, 0x63, 0x54, 0x46, 0x64, 0x2B, 0x0A, 0x72, 0x72, 0x63, 0x7A, 0x74, 0x6C, 0x35, 0x32, 0x34, 0x57, 0x57, 0x4C, 0x5A, 0x74, 0x2B, 0x4E, 0x79, 0x74, 0x65, 0x59, 0x72, 0x38, 0x34, + 0x32, 0x6D, 0x49, 0x79, 0x63, 0x67, 0x35, 0x6B, 0x44, 0x63, 0x50, 0x4F, 0x76, 0x64, 0x4F, 0x33, 0x47, 0x44, 0x6A, 0x62, 0x6E, 0x76, 0x65, 0x7A, 0x42, 0x63, 0x36, 0x65, 0x55, 0x57, 0x73, 0x75, 0x53, 0x5A, 0x49, 0x4B, 0x6D, 0x41, 0x4D, 0x46, + 0x77, 0x6F, 0x57, 0x34, 0x73, 0x4B, 0x65, 0x46, 0x59, 0x56, 0x2B, 0x78, 0x61, 0x0A, 0x66, 0x4A, 0x6C, 0x72, 0x4A, 0x61, 0x53, 0x51, 0x4F, 0x6F, 0x44, 0x30, 0x49, 0x4A, 0x32, 0x61, 0x7A, 0x73, 0x63, 0x74, 0x2B, 0x62, 0x4A, 0x4C, 0x4B, 0x5A, + 0x57, 0x44, 0x36, 0x54, 0x57, 0x4E, 0x70, 0x30, 0x6C, 0x49, 0x70, 0x77, 0x39, 0x4D, 0x47, 0x5A, 0x48, 0x51, 0x39, 0x62, 0x38, 0x51, 0x34, 0x48, 0x45, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, 0x77, + 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x0A, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x64, 0x45, 0x6D, 0x5A, 0x30, + 0x66, 0x2B, 0x30, 0x65, 0x6D, 0x68, 0x46, 0x64, 0x63, 0x4E, 0x2B, 0x74, 0x4E, 0x7A, 0x4D, 0x7A, 0x6A, 0x6B, 0x7A, 0x32, 0x67, 0x67, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, + 0x67, 0x47, 0x47, 0x4D, 0x41, 0x30, 0x47, 0x0A, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x44, 0x41, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x41, 0x6A, 0x51, 0x31, 0x4D, 0x6B, 0x59, 0x6C, 0x78, 0x74, + 0x2F, 0x54, 0x37, 0x43, 0x7A, 0x31, 0x55, 0x41, 0x62, 0x4D, 0x56, 0x57, 0x69, 0x4C, 0x6B, 0x4F, 0x33, 0x54, 0x72, 0x69, 0x4A, 0x51, 0x32, 0x56, 0x53, 0x70, 0x66, 0x4B, 0x67, 0x49, 0x6E, 0x75, 0x4B, 0x73, 0x31, 0x6C, 0x2B, 0x4E, 0x73, 0x57, + 0x34, 0x41, 0x6D, 0x53, 0x0A, 0x34, 0x42, 0x6A, 0x48, 0x65, 0x4A, 0x69, 0x37, 0x38, 0x2B, 0x78, 0x43, 0x55, 0x76, 0x75, 0x70, 0x70, 0x49, 0x4C, 0x58, 0x54, 0x64, 0x69, 0x4B, 0x2F, 0x4F, 0x52, 0x4F, 0x2F, 0x61, 0x75, 0x51, 0x78, 0x44, 0x68, + 0x31, 0x4D, 0x6F, 0x53, 0x66, 0x2F, 0x37, 0x4F, 0x77, 0x4B, 0x77, 0x49, 0x7A, 0x4E, 0x73, 0x41, 0x51, 0x6B, 0x47, 0x38, 0x64, 0x6E, 0x4B, 0x2F, 0x68, 0x61, 0x5A, 0x50, 0x73, 0x6F, 0x30, 0x55, 0x76, 0x46, 0x4A, 0x2F, 0x31, 0x54, 0x43, 0x70, + 0x6C, 0x0A, 0x51, 0x33, 0x49, 0x4D, 0x39, 0x38, 0x50, 0x34, 0x6C, 0x59, 0x73, 0x55, 0x38, 0x34, 0x55, 0x67, 0x59, 0x74, 0x31, 0x55, 0x55, 0x39, 0x30, 0x73, 0x33, 0x42, 0x69, 0x56, 0x61, 0x55, 0x2B, 0x44, 0x52, 0x33, 0x42, 0x41, 0x4D, 0x31, + 0x68, 0x33, 0x45, 0x67, 0x79, 0x69, 0x36, 0x31, 0x49, 0x78, 0x48, 0x6B, 0x7A, 0x4A, 0x71, 0x4D, 0x37, 0x46, 0x37, 0x38, 0x50, 0x52, 0x72, 0x65, 0x42, 0x72, 0x41, 0x77, 0x41, 0x30, 0x4A, 0x72, 0x52, 0x55, 0x49, 0x54, 0x57, 0x58, 0x0A, 0x41, + 0x64, 0x78, 0x66, 0x47, 0x2F, 0x46, 0x38, 0x35, 0x31, 0x58, 0x36, 0x4C, 0x57, 0x68, 0x33, 0x65, 0x39, 0x4E, 0x70, 0x7A, 0x4E, 0x4D, 0x4F, 0x61, 0x37, 0x70, 0x4E, 0x64, 0x6B, 0x54, 0x57, 0x77, 0x68, 0x57, 0x61, 0x4A, 0x75, 0x79, 0x77, 0x78, + 0x66, 0x57, 0x37, 0x30, 0x58, 0x70, 0x30, 0x77, 0x6D, 0x7A, 0x4E, 0x78, 0x62, 0x56, 0x65, 0x39, 0x6B, 0x7A, 0x6D, 0x57, 0x79, 0x32, 0x42, 0x32, 0x37, 0x4F, 0x33, 0x4F, 0x70, 0x65, 0x65, 0x37, 0x63, 0x39, 0x47, 0x0A, 0x73, 0x6C, 0x41, 0x39, + 0x68, 0x47, 0x43, 0x5A, 0x63, 0x62, 0x55, 0x7A, 0x74, 0x56, 0x64, 0x46, 0x35, 0x6B, 0x4A, 0x48, 0x64, 0x57, 0x6F, 0x4F, 0x73, 0x41, 0x67, 0x4D, 0x72, 0x72, 0x33, 0x65, 0x39, 0x37, 0x73, 0x50, 0x57, 0x44, 0x32, 0x50, 0x41, 0x7A, 0x48, 0x6F, + 0x50, 0x59, 0x4A, 0x51, 0x79, 0x69, 0x39, 0x65, 0x44, 0x46, 0x32, 0x30, 0x6C, 0x37, 0x34, 0x67, 0x4E, 0x41, 0x66, 0x30, 0x78, 0x42, 0x4C, 0x68, 0x37, 0x74, 0x65, 0x77, 0x32, 0x56, 0x6B, 0x74, 0x0A, 0x61, 0x66, 0x63, 0x78, 0x42, 0x50, 0x54, + 0x79, 0x2B, 0x61, 0x76, 0x35, 0x45, 0x7A, 0x48, 0x34, 0x41, 0x58, 0x63, 0x4F, 0x50, 0x55, 0x49, 0x6A, 0x4A, 0x73, 0x79, 0x61, 0x63, 0x6D, 0x64, 0x52, 0x49, 0x58, 0x72, 0x4D, 0x50, 0x49, 0x57, 0x6F, 0x36, 0x69, 0x46, 0x71, 0x4F, 0x39, 0x74, + 0x61, 0x50, 0x4B, 0x55, 0x30, 0x6E, 0x70, 0x72, 0x41, 0x4C, 0x4E, 0x2B, 0x41, 0x6E, 0x43, 0x6E, 0x67, 0x33, 0x33, 0x65, 0x55, 0x30, 0x61, 0x4B, 0x41, 0x51, 0x76, 0x39, 0x71, 0x0A, 0x54, 0x46, 0x73, 0x52, 0x30, 0x50, 0x58, 0x4E, 0x6F, 0x72, + 0x36, 0x75, 0x7A, 0x46, 0x46, 0x63, 0x77, 0x39, 0x56, 0x55, 0x65, 0x77, 0x79, 0x75, 0x31, 0x72, 0x6B, 0x47, 0x64, 0x34, 0x44, 0x69, 0x37, 0x77, 0x63, 0x61, 0x61, 0x4D, 0x78, 0x5A, 0x55, 0x61, 0x31, 0x2B, 0x58, 0x47, 0x64, 0x72, 0x75, 0x64, + 0x76, 0x69, 0x42, 0x30, 0x4A, 0x62, 0x75, 0x41, 0x45, 0x46, 0x57, 0x44, 0x6C, 0x4E, 0x35, 0x4C, 0x75, 0x59, 0x6F, 0x37, 0x45, 0x79, 0x37, 0x4E, 0x6D, 0x6A, 0x0A, 0x31, 0x6D, 0x2B, 0x55, 0x49, 0x2F, 0x38, 0x37, 0x74, 0x79, 0x6C, 0x6C, 0x35, + 0x67, 0x66, 0x70, 0x37, 0x37, 0x59, 0x5A, 0x36, 0x75, 0x66, 0x43, 0x4F, 0x42, 0x30, 0x79, 0x69, 0x4A, 0x41, 0x38, 0x45, 0x79, 0x74, 0x75, 0x7A, 0x4F, 0x2B, 0x72, 0x64, 0x77, 0x59, 0x30, 0x64, 0x34, 0x52, 0x50, 0x63, 0x75, 0x53, 0x42, 0x68, + 0x50, 0x6D, 0x35, 0x64, 0x44, 0x54, 0x65, 0x64, 0x6B, 0x2B, 0x53, 0x4B, 0x6C, 0x4F, 0x78, 0x4A, 0x54, 0x6E, 0x62, 0x50, 0x50, 0x2F, 0x6C, 0x0A, 0x50, 0x71, 0x59, 0x4F, 0x35, 0x57, 0x75, 0x65, 0x2F, 0x39, 0x76, 0x73, 0x4C, 0x33, 0x53, 0x44, + 0x33, 0x34, 0x36, 0x30, 0x73, 0x36, 0x6E, 0x65, 0x46, 0x45, 0x33, 0x2F, 0x4D, 0x61, 0x4E, 0x46, 0x63, 0x79, 0x54, 0x36, 0x6C, 0x53, 0x6E, 0x4D, 0x45, 0x70, 0x63, 0x45, 0x6F, 0x6A, 0x69, 0x32, 0x6A, 0x62, 0x44, 0x77, 0x4E, 0x2F, 0x7A, 0x49, + 0x49, 0x58, 0x38, 0x2F, 0x73, 0x79, 0x51, 0x62, 0x50, 0x59, 0x74, 0x75, 0x7A, 0x45, 0x32, 0x77, 0x46, 0x67, 0x32, 0x57, 0x0A, 0x48, 0x59, 0x4D, 0x66, 0x52, 0x73, 0x43, 0x62, 0x76, 0x55, 0x4F, 0x5A, 0x35, 0x38, 0x53, 0x57, 0x4C, 0x73, 0x35, + 0x66, 0x79, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x41, 0x73, + 0x69, 0x61, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x47, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, + 0x46, 0x70, 0x54, 0x43, 0x43, 0x41, 0x34, 0x32, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x5A, 0x50, 0x59, 0x4F, 0x5A, 0x58, 0x64, 0x68, 0x61, 0x71, 0x73, 0x37, 0x74, 0x4F, 0x71, 0x46, 0x68, 0x4C, 0x75, 0x78, 0x69, 0x62, 0x68, + 0x78, 0x6B, 0x77, 0x38, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x77, 0x57, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, + 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, + 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x4A, 0x44, 0x41, 0x69, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x0A, 0x47, 0x31, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, + 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x48, 0x4D, 0x7A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x54, 0x41, 0x31, 0x4D, + 0x6A, 0x41, 0x77, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x54, 0x6C, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4E, 0x6A, 0x41, 0x31, 0x4D, 0x54, 0x6B, 0x77, 0x4D, 0x6A, 0x45, 0x77, 0x0A, 0x4D, 0x54, 0x6C, 0x61, 0x4D, 0x46, 0x6F, 0x78, 0x43, 0x7A, 0x41, 0x4A, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x4F, 0x4D, 0x53, 0x55, 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x78, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x45, 0x46, 0x7A, 0x61, 0x57, 0x45, 0x67, + 0x56, 0x47, 0x56, 0x6A, 0x61, 0x47, 0x35, 0x76, 0x62, 0x47, 0x39, 0x6E, 0x61, 0x57, 0x56, 0x7A, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x0A, 0x4D, 0x53, 0x51, 0x77, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x74, + 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x45, 0x46, 0x7A, 0x61, 0x57, 0x45, 0x67, 0x52, 0x32, 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x52, 0x7A, 0x4D, 0x77, 0x67, 0x67, 0x49, + 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x0A, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, + 0x44, 0x41, 0x4D, 0x59, 0x4A, 0x68, 0x6B, 0x75, 0x53, 0x55, 0x47, 0x77, 0x6F, 0x71, 0x5A, 0x64, 0x43, 0x2B, 0x42, 0x71, 0x6D, 0x48, 0x4F, 0x31, 0x45, 0x53, 0x36, 0x6E, 0x42, 0x42, 0x72, 0x75, 0x4C, 0x37, 0x64, 0x4F, 0x6F, 0x4B, 0x6A, 0x62, + 0x6D, 0x7A, 0x54, 0x4E, 0x79, 0x50, 0x74, 0x78, 0x4E, 0x53, 0x54, 0x31, 0x51, 0x59, 0x34, 0x53, 0x78, 0x7A, 0x0A, 0x6C, 0x5A, 0x48, 0x46, 0x5A, 0x6A, 0x74, 0x71, 0x7A, 0x36, 0x78, 0x6A, 0x62, 0x59, 0x64, 0x54, 0x38, 0x50, 0x66, 0x78, 0x4F, + 0x62, 0x65, 0x67, 0x51, 0x32, 0x4F, 0x77, 0x78, 0x41, 0x4E, 0x64, 0x56, 0x36, 0x6E, 0x6E, 0x52, 0x4D, 0x37, 0x45, 0x6F, 0x59, 0x4E, 0x6C, 0x39, 0x6C, 0x41, 0x2B, 0x73, 0x58, 0x34, 0x57, 0x75, 0x44, 0x71, 0x4B, 0x41, 0x74, 0x43, 0x57, 0x48, + 0x77, 0x44, 0x4E, 0x42, 0x53, 0x48, 0x76, 0x42, 0x6D, 0x33, 0x64, 0x49, 0x5A, 0x77, 0x5A, 0x0A, 0x51, 0x30, 0x57, 0x68, 0x78, 0x65, 0x69, 0x41, 0x79, 0x73, 0x4B, 0x74, 0x51, 0x47, 0x49, 0x58, 0x42, 0x73, 0x61, 0x71, 0x76, 0x50, 0x50, 0x57, + 0x35, 0x76, 0x78, 0x51, 0x66, 0x6D, 0x5A, 0x43, 0x48, 0x7A, 0x79, 0x4C, 0x70, 0x6E, 0x6C, 0x35, 0x68, 0x6B, 0x41, 0x31, 0x6E, 0x79, 0x44, 0x76, 0x50, 0x2B, 0x75, 0x4C, 0x52, 0x78, 0x2B, 0x50, 0x6A, 0x73, 0x58, 0x55, 0x6A, 0x72, 0x59, 0x73, + 0x79, 0x55, 0x51, 0x45, 0x34, 0x39, 0x52, 0x44, 0x64, 0x54, 0x2F, 0x56, 0x0A, 0x50, 0x36, 0x38, 0x63, 0x7A, 0x48, 0x35, 0x47, 0x58, 0x36, 0x7A, 0x66, 0x5A, 0x42, 0x43, 0x4B, 0x37, 0x30, 0x62, 0x77, 0x6B, 0x50, 0x41, 0x50, 0x4C, 0x66, 0x53, + 0x49, 0x43, 0x37, 0x45, 0x70, 0x71, 0x71, 0x2B, 0x46, 0x71, 0x6B, 0x6C, 0x59, 0x71, 0x4C, 0x39, 0x6A, 0x6F, 0x44, 0x69, 0x52, 0x35, 0x72, 0x50, 0x6D, 0x64, 0x32, 0x6A, 0x45, 0x2B, 0x53, 0x6F, 0x5A, 0x68, 0x4C, 0x73, 0x4F, 0x34, 0x66, 0x57, + 0x76, 0x69, 0x65, 0x79, 0x6C, 0x4C, 0x31, 0x41, 0x67, 0x0A, 0x64, 0x42, 0x34, 0x53, 0x51, 0x58, 0x4D, 0x65, 0x4A, 0x4E, 0x6E, 0x4B, 0x7A, 0x69, 0x79, 0x68, 0x57, 0x54, 0x58, 0x41, 0x79, 0x42, 0x31, 0x47, 0x4A, 0x32, 0x46, 0x61, 0x6A, 0x2F, + 0x6C, 0x4E, 0x30, 0x33, 0x4A, 0x35, 0x5A, 0x68, 0x36, 0x66, 0x46, 0x5A, 0x41, 0x68, 0x4C, 0x66, 0x33, 0x74, 0x69, 0x31, 0x5A, 0x77, 0x41, 0x30, 0x70, 0x4A, 0x50, 0x6E, 0x39, 0x70, 0x4D, 0x52, 0x4A, 0x70, 0x78, 0x78, 0x35, 0x63, 0x79, 0x6E, + 0x6F, 0x54, 0x69, 0x2B, 0x6A, 0x6D, 0x0A, 0x39, 0x57, 0x41, 0x50, 0x7A, 0x4A, 0x4D, 0x73, 0x68, 0x48, 0x2F, 0x78, 0x2F, 0x47, 0x72, 0x38, 0x6D, 0x30, 0x65, 0x64, 0x32, 0x36, 0x32, 0x49, 0x50, 0x66, 0x4E, 0x32, 0x64, 0x54, 0x50, 0x58, 0x53, + 0x36, 0x54, 0x49, 0x69, 0x2F, 0x6E, 0x31, 0x51, 0x31, 0x68, 0x50, 0x79, 0x38, 0x67, 0x44, 0x56, 0x49, 0x2B, 0x6C, 0x68, 0x58, 0x67, 0x45, 0x47, 0x76, 0x4E, 0x7A, 0x38, 0x74, 0x65, 0x48, 0x48, 0x55, 0x47, 0x66, 0x35, 0x39, 0x67, 0x58, 0x7A, + 0x68, 0x71, 0x63, 0x0A, 0x44, 0x30, 0x72, 0x38, 0x33, 0x45, 0x52, 0x6F, 0x56, 0x47, 0x6A, 0x69, 0x51, 0x54, 0x7A, 0x2B, 0x4C, 0x49, 0x53, 0x47, 0x4E, 0x7A, 0x7A, 0x4E, 0x50, 0x79, 0x2B, 0x69, 0x32, 0x2B, 0x66, 0x33, 0x56, 0x41, 0x4E, 0x66, + 0x57, 0x64, 0x50, 0x33, 0x6B, 0x58, 0x6A, 0x48, 0x69, 0x33, 0x64, 0x71, 0x46, 0x75, 0x56, 0x4A, 0x68, 0x5A, 0x42, 0x46, 0x63, 0x6E, 0x41, 0x76, 0x6B, 0x56, 0x33, 0x34, 0x50, 0x6D, 0x56, 0x41, 0x43, 0x78, 0x6D, 0x5A, 0x79, 0x53, 0x59, 0x67, + 0x0A, 0x57, 0x6D, 0x6A, 0x42, 0x4E, 0x62, 0x39, 0x50, 0x70, 0x31, 0x48, 0x78, 0x32, 0x42, 0x45, 0x72, 0x57, 0x2B, 0x43, 0x61, 0x6E, 0x69, 0x67, 0x37, 0x43, 0x6A, 0x6F, 0x4B, 0x48, 0x38, 0x47, 0x42, 0x35, 0x53, 0x37, 0x77, 0x70, 0x72, 0x6C, + 0x70, 0x70, 0x59, 0x69, 0x55, 0x35, 0x6D, 0x73, 0x54, 0x66, 0x39, 0x46, 0x6B, 0x50, 0x7A, 0x32, 0x63, 0x63, 0x45, 0x62, 0x6C, 0x6F, 0x6F, 0x56, 0x37, 0x57, 0x49, 0x51, 0x6E, 0x33, 0x4D, 0x53, 0x41, 0x50, 0x6D, 0x65, 0x61, 0x0A, 0x6D, 0x73, + 0x65, 0x61, 0x4D, 0x51, 0x34, 0x77, 0x37, 0x4F, 0x59, 0x58, 0x51, 0x4A, 0x58, 0x5A, 0x52, 0x65, 0x30, 0x42, 0x6C, 0x71, 0x71, 0x2F, 0x44, 0x50, 0x4E, 0x4C, 0x30, 0x57, 0x50, 0x33, 0x45, 0x31, 0x6A, 0x41, 0x75, 0x50, 0x50, 0x36, 0x5A, 0x39, + 0x32, 0x62, 0x66, 0x57, 0x31, 0x4B, 0x2F, 0x7A, 0x4A, 0x4D, 0x74, 0x53, 0x55, 0x37, 0x2F, 0x78, 0x78, 0x6E, 0x44, 0x34, 0x55, 0x69, 0x57, 0x51, 0x57, 0x52, 0x6B, 0x55, 0x46, 0x33, 0x67, 0x64, 0x43, 0x46, 0x0A, 0x54, 0x49, 0x63, 0x51, 0x63, + 0x66, 0x2B, 0x65, 0x51, 0x78, 0x75, 0x75, 0x6C, 0x58, 0x55, 0x74, 0x67, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, 0x32, 0x4D, 0x77, 0x59, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, + 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x49, 0x77, 0x51, 0x59, 0x4D, 0x42, 0x61, 0x41, 0x46, 0x45, 0x44, 0x6B, 0x35, 0x50, 0x49, 0x6A, 0x0A, 0x37, 0x7A, 0x6A, 0x4B, 0x73, 0x4B, 0x35, 0x58, + 0x66, 0x2F, 0x49, 0x68, 0x4D, 0x42, 0x59, 0x30, 0x32, 0x37, 0x79, 0x53, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x41, 0x35, 0x4F, 0x54, 0x79, 0x49, 0x2B, 0x38, 0x34, 0x79, 0x72, 0x43, 0x75, + 0x56, 0x33, 0x2F, 0x79, 0x49, 0x54, 0x41, 0x57, 0x4E, 0x4E, 0x75, 0x38, 0x6B, 0x6A, 0x41, 0x4F, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x0A, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x51, 0x59, + 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x43, 0x59, 0x37, 0x55, 0x65, 0x46, 0x4E, 0x4F, 0x50, 0x4D, 0x79, 0x47, 0x4C, 0x53, 0x30, 0x58, 0x75, 0x46, + 0x6C, 0x58, 0x73, 0x53, 0x55, 0x54, 0x39, 0x53, 0x6E, 0x59, 0x61, 0x50, 0x34, 0x77, 0x4D, 0x38, 0x7A, 0x41, 0x51, 0x4C, 0x70, 0x77, 0x36, 0x6F, 0x31, 0x0A, 0x44, 0x2F, 0x47, 0x55, 0x45, 0x33, 0x64, 0x33, 0x4E, 0x5A, 0x34, 0x74, 0x56, 0x6C, + 0x46, 0x45, 0x62, 0x75, 0x48, 0x47, 0x4C, 0x69, 0x67, 0x65, 0x2F, 0x39, 0x72, 0x73, 0x52, 0x38, 0x32, 0x58, 0x52, 0x42, 0x66, 0x33, 0x34, 0x45, 0x7A, 0x43, 0x34, 0x58, 0x78, 0x38, 0x4D, 0x6E, 0x70, 0x6D, 0x79, 0x46, 0x71, 0x32, 0x58, 0x46, + 0x4E, 0x46, 0x56, 0x31, 0x70, 0x46, 0x31, 0x41, 0x57, 0x5A, 0x4C, 0x79, 0x34, 0x6A, 0x56, 0x65, 0x35, 0x6A, 0x61, 0x4E, 0x2F, 0x54, 0x0A, 0x47, 0x33, 0x69, 0x6E, 0x45, 0x70, 0x51, 0x47, 0x41, 0x48, 0x55, 0x4E, 0x63, 0x6F, 0x54, 0x70, 0x4C, + 0x72, 0x78, 0x61, 0x61, 0x74, 0x58, 0x65, 0x4C, 0x31, 0x6E, 0x48, 0x6F, 0x2B, 0x7A, 0x53, 0x68, 0x32, 0x62, 0x62, 0x74, 0x31, 0x53, 0x31, 0x4A, 0x4B, 0x76, 0x30, 0x51, 0x33, 0x6A, 0x62, 0x53, 0x77, 0x54, 0x45, 0x62, 0x39, 0x33, 0x6D, 0x50, + 0x6D, 0x59, 0x2B, 0x4B, 0x66, 0x4A, 0x4C, 0x61, 0x48, 0x45, 0x69, 0x68, 0x36, 0x44, 0x34, 0x73, 0x54, 0x4E, 0x6A, 0x0A, 0x64, 0x75, 0x4D, 0x4E, 0x68, 0x58, 0x4A, 0x45, 0x49, 0x6C, 0x55, 0x2F, 0x48, 0x48, 0x7A, 0x70, 0x2F, 0x4C, 0x67, 0x56, + 0x36, 0x46, 0x4C, 0x36, 0x71, 0x6A, 0x36, 0x6A, 0x49, 0x54, 0x6B, 0x31, 0x64, 0x49, 0x6D, 0x6D, 0x61, 0x73, 0x49, 0x35, 0x2B, 0x6E, 0x6A, 0x50, 0x74, 0x71, 0x7A, 0x6E, 0x35, 0x39, 0x5A, 0x57, 0x2F, 0x79, 0x4F, 0x53, 0x4C, 0x6C, 0x41, 0x4C, + 0x71, 0x62, 0x55, 0x48, 0x4D, 0x2F, 0x51, 0x34, 0x58, 0x36, 0x52, 0x4A, 0x70, 0x73, 0x74, 0x6C, 0x0A, 0x63, 0x48, 0x62, 0x6F, 0x43, 0x6F, 0x57, 0x41, 0x53, 0x7A, 0x59, 0x39, 0x4D, 0x2F, 0x65, 0x56, 0x56, 0x48, 0x55, 0x6C, 0x32, 0x71, 0x7A, + 0x45, 0x63, 0x34, 0x4A, 0x6C, 0x36, 0x56, 0x4C, 0x31, 0x58, 0x50, 0x30, 0x34, 0x6C, 0x51, 0x4A, 0x71, 0x61, 0x54, 0x44, 0x46, 0x48, 0x41, 0x70, 0x58, 0x42, 0x36, 0x34, 0x69, 0x70, 0x43, 0x7A, 0x35, 0x78, 0x55, 0x47, 0x33, 0x75, 0x4F, 0x79, + 0x66, 0x54, 0x30, 0x67, 0x41, 0x2B, 0x51, 0x45, 0x45, 0x56, 0x63, 0x79, 0x73, 0x0A, 0x2B, 0x54, 0x49, 0x78, 0x78, 0x48, 0x57, 0x56, 0x42, 0x71, 0x42, 0x2F, 0x30, 0x59, 0x30, 0x6E, 0x33, 0x62, 0x4F, 0x70, 0x70, 0x48, 0x4B, 0x48, 0x2F, 0x6C, + 0x6D, 0x4C, 0x6D, 0x6E, 0x70, 0x30, 0x46, 0x74, 0x30, 0x57, 0x70, 0x57, 0x49, 0x70, 0x36, 0x7A, 0x71, 0x57, 0x33, 0x49, 0x75, 0x6E, 0x61, 0x46, 0x6E, 0x54, 0x36, 0x33, 0x65, 0x52, 0x4F, 0x66, 0x6A, 0x58, 0x79, 0x39, 0x6D, 0x50, 0x58, 0x31, + 0x6F, 0x6E, 0x41, 0x58, 0x31, 0x64, 0x61, 0x42, 0x6C, 0x69, 0x0A, 0x32, 0x4D, 0x6A, 0x4E, 0x39, 0x4C, 0x64, 0x79, 0x52, 0x37, 0x35, 0x62, 0x6C, 0x38, 0x37, 0x79, 0x72, 0x61, 0x4B, 0x5A, 0x6B, 0x36, 0x32, 0x55, 0x79, 0x35, 0x50, 0x32, 0x45, + 0x67, 0x6D, 0x56, 0x74, 0x71, 0x76, 0x58, 0x4F, 0x39, 0x41, 0x2F, 0x45, 0x63, 0x73, 0x77, 0x46, 0x69, 0x35, 0x35, 0x67, 0x4F, 0x52, 0x6E, 0x67, 0x53, 0x31, 0x64, 0x37, 0x58, 0x42, 0x34, 0x74, 0x6D, 0x42, 0x5A, 0x72, 0x4F, 0x46, 0x64, 0x52, + 0x57, 0x4F, 0x50, 0x79, 0x4E, 0x39, 0x79, 0x0A, 0x61, 0x46, 0x76, 0x71, 0x48, 0x62, 0x67, 0x42, 0x38, 0x58, 0x37, 0x37, 0x35, 0x34, 0x71, 0x7A, 0x34, 0x31, 0x53, 0x67, 0x4F, 0x41, 0x6E, 0x67, 0x50, 0x4E, 0x35, 0x43, 0x38, 0x73, 0x4C, 0x74, + 0x4C, 0x70, 0x76, 0x7A, 0x48, 0x7A, 0x57, 0x32, 0x4E, 0x74, 0x6A, 0x6A, 0x67, 0x4B, 0x47, 0x4C, 0x7A, 0x5A, 0x6C, 0x6B, 0x44, 0x38, 0x4B, 0x71, 0x71, 0x37, 0x48, 0x4B, 0x39, 0x57, 0x2B, 0x65, 0x51, 0x34, 0x32, 0x45, 0x56, 0x4A, 0x6D, 0x7A, + 0x62, 0x73, 0x41, 0x53, 0x0A, 0x5A, 0x74, 0x68, 0x77, 0x45, 0x50, 0x45, 0x47, 0x4E, 0x54, 0x4E, 0x44, 0x71, 0x4A, 0x77, 0x75, 0x75, 0x68, 0x51, 0x78, 0x7A, 0x68, 0x42, 0x2F, 0x48, 0x49, 0x62, 0x6A, 0x6A, 0x39, 0x4C, 0x56, 0x2B, 0x48, 0x66, + 0x73, 0x6D, 0x36, 0x76, 0x78, 0x4C, 0x32, 0x50, 0x5A, 0x51, 0x6C, 0x2F, 0x67, 0x5A, 0x34, 0x46, 0x6B, 0x6B, 0x66, 0x47, 0x58, 0x4C, 0x2F, 0x78, 0x75, 0x4A, 0x76, 0x59, 0x7A, 0x2B, 0x4E, 0x4F, 0x31, 0x2B, 0x4D, 0x52, 0x69, 0x71, 0x7A, 0x46, + 0x52, 0x0A, 0x4A, 0x51, 0x4A, 0x36, 0x2B, 0x4E, 0x31, 0x72, 0x5A, 0x64, 0x56, 0x74, 0x54, 0x54, 0x44, 0x49, 0x5A, 0x62, 0x70, 0x6F, 0x46, 0x47, 0x57, 0x73, 0x4A, 0x77, 0x74, 0x30, 0x69, 0x76, 0x4B, 0x48, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x41, 0x73, 0x69, 0x61, 0x20, 0x47, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x20, 0x52, + 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x47, 0x34, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x56, 0x54, 0x43, 0x43, 0x41, 0x64, 0x79, 0x67, 0x41, 0x77, + 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x54, 0x79, 0x4E, 0x6B, 0x75, 0x49, 0x36, 0x58, 0x59, 0x35, 0x37, 0x47, 0x55, 0x34, 0x48, 0x42, 0x64, 0x6B, 0x37, 0x4C, 0x4B, 0x6E, 0x51, 0x56, 0x31, 0x74, 0x63, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, + 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x77, 0x57, 0x6A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, + 0x41, 0x6F, 0x4D, 0x48, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, + 0x79, 0x34, 0x78, 0x4A, 0x44, 0x41, 0x69, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x47, 0x31, 0x52, 0x79, 0x0A, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, + 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x42, 0x48, 0x4E, 0x44, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x54, 0x41, 0x31, 0x4D, 0x6A, 0x41, 0x77, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x6A, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x30, + 0x4E, 0x6A, 0x41, 0x31, 0x4D, 0x54, 0x6B, 0x77, 0x4D, 0x6A, 0x45, 0x77, 0x4D, 0x6A, 0x4A, 0x61, 0x0A, 0x4D, 0x46, 0x6F, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x4F, 0x4D, 0x53, 0x55, + 0x77, 0x49, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x44, 0x42, 0x78, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x45, 0x46, 0x7A, 0x61, 0x57, 0x45, 0x67, 0x56, 0x47, 0x56, 0x6A, 0x61, 0x47, 0x35, 0x76, 0x62, 0x47, 0x39, 0x6E, 0x61, 0x57, 0x56, + 0x7A, 0x4C, 0x43, 0x42, 0x4A, 0x62, 0x6D, 0x4D, 0x75, 0x4D, 0x53, 0x51, 0x77, 0x0A, 0x49, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x74, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x45, 0x46, 0x7A, 0x61, 0x57, 0x45, 0x67, 0x52, 0x32, + 0x78, 0x76, 0x59, 0x6D, 0x46, 0x73, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x52, 0x7A, 0x51, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, + 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x0A, 0x41, 0x41, 0x54, 0x78, 0x73, 0x38, 0x30, 0x34, 0x35, 0x43, 0x56, 0x44, 0x35, 0x64, 0x34, 0x5A, 0x43, 0x62, 0x75, 0x42, 0x65, 0x61, 0x49, 0x56, 0x58, 0x78, 0x56, 0x6A, 0x41, + 0x64, 0x37, 0x43, 0x71, 0x39, 0x32, 0x7A, 0x70, 0x68, 0x74, 0x6E, 0x53, 0x34, 0x43, 0x44, 0x72, 0x35, 0x6E, 0x4C, 0x72, 0x42, 0x66, 0x62, 0x4B, 0x35, 0x62, 0x4B, 0x66, 0x46, 0x4A, 0x56, 0x34, 0x68, 0x72, 0x68, 0x50, 0x56, 0x62, 0x77, 0x4C, + 0x78, 0x59, 0x49, 0x2B, 0x68, 0x57, 0x38, 0x0A, 0x6D, 0x37, 0x74, 0x48, 0x35, 0x6A, 0x2F, 0x75, 0x71, 0x4F, 0x46, 0x4D, 0x6A, 0x50, 0x58, 0x54, 0x4E, 0x76, 0x6B, 0x34, 0x58, 0x61, 0x74, 0x77, 0x6D, 0x6B, 0x63, 0x4E, 0x34, 0x6F, 0x46, 0x42, + 0x42, 0x75, 0x74, 0x4A, 0x2B, 0x62, 0x41, 0x70, 0x33, 0x54, 0x50, 0x73, 0x55, 0x4B, 0x56, 0x2F, 0x65, 0x53, 0x6D, 0x34, 0x49, 0x4A, 0x69, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, + 0x2F, 0x77, 0x51, 0x46, 0x0A, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, 0x55, 0x70, 0x62, 0x74, 0x4B, 0x6C, 0x38, 0x36, 0x7A, 0x4B, 0x33, 0x2B, + 0x6B, 0x4D, 0x64, 0x36, 0x58, 0x67, 0x31, 0x6D, 0x44, 0x70, 0x6D, 0x39, 0x78, 0x79, 0x39, 0x34, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4B, 0x57, 0x37, 0x53, 0x70, 0x66, 0x4F, 0x73, 0x79, 0x74, + 0x2F, 0x0A, 0x70, 0x44, 0x48, 0x65, 0x6C, 0x34, 0x4E, 0x5A, 0x67, 0x36, 0x5A, 0x76, 0x63, 0x63, 0x76, 0x65, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, + 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6E, 0x41, 0x44, 0x42, 0x6B, 0x41, 0x6A, 0x42, 0x65, 0x38, 0x75, 0x73, 0x47, 0x7A, 0x45, 0x6B, 0x78, 0x6E, 0x30, 0x41, 0x41, 0x0A, 0x62, + 0x62, 0x64, 0x2B, 0x4E, 0x76, 0x42, 0x4E, 0x45, 0x55, 0x2F, 0x7A, 0x79, 0x34, 0x6B, 0x36, 0x4C, 0x48, 0x69, 0x52, 0x55, 0x4B, 0x4E, 0x62, 0x77, 0x4D, 0x70, 0x31, 0x4A, 0x76, 0x4B, 0x2F, 0x6B, 0x46, 0x30, 0x4C, 0x67, 0x6F, 0x78, 0x67, 0x4B, + 0x4A, 0x2F, 0x47, 0x63, 0x4A, 0x70, 0x6F, 0x35, 0x50, 0x45, 0x43, 0x4D, 0x46, 0x78, 0x59, 0x44, 0x6C, 0x5A, 0x32, 0x7A, 0x31, 0x6A, 0x44, 0x31, 0x78, 0x43, 0x4D, 0x75, 0x6F, 0x36, 0x75, 0x34, 0x37, 0x78, 0x6B, 0x0A, 0x64, 0x55, 0x66, 0x46, + 0x56, 0x5A, 0x44, 0x6A, 0x2F, 0x62, 0x70, 0x56, 0x36, 0x77, 0x66, 0x45, 0x55, 0x36, 0x73, 0x33, 0x71, 0x65, 0x34, 0x68, 0x73, 0x69, 0x46, 0x62, 0x59, 0x49, 0x38, 0x39, 0x4D, 0x76, 0x48, 0x56, 0x49, 0x35, 0x54, 0x57, 0x57, 0x41, 0x3D, 0x3D, + 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x65, 0x6C, 0x65, 0x6B, 0x6F, 0x6D, 0x20, 0x53, 0x65, 0x63, 0x75, + 0x72, 0x69, 0x74, 0x79, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x32, 0x30, 0x32, 0x30, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x51, 0x6A, 0x43, 0x43, 0x41, 0x63, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x4E, 0x6A, 0x71, 0x57, 0x6A, 0x4D, 0x6C, 0x63, 0x73, 0x6C, 0x6A, 0x4E, 0x30, 0x41, + 0x46, 0x64, 0x78, 0x65, 0x56, 0x58, 0x41, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x6A, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, + 0x4A, 0x45, 0x0A, 0x52, 0x54, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x65, 0x52, 0x47, 0x56, 0x31, 0x64, 0x48, 0x4E, 0x6A, 0x61, 0x47, 0x55, 0x67, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x57, 0x74, 0x76, 0x62, + 0x53, 0x42, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4D, 0x53, 0x73, 0x77, 0x4B, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x4A, 0x55, 0x5A, 0x57, 0x78, 0x6C, 0x0A, + 0x61, 0x32, 0x39, 0x74, 0x49, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x77, + 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x77, 0x4D, 0x44, 0x67, 0x79, 0x4E, 0x54, 0x41, 0x33, 0x4E, 0x44, 0x67, 0x79, 0x4D, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x31, 0x4D, 0x44, 0x67, 0x79, 0x4E, 0x54, 0x49, 0x7A, 0x0A, 0x4E, 0x54, 0x6B, + 0x31, 0x4F, 0x56, 0x6F, 0x77, 0x59, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x6B, 0x52, + 0x6C, 0x64, 0x58, 0x52, 0x7A, 0x59, 0x32, 0x68, 0x6C, 0x49, 0x46, 0x52, 0x6C, 0x62, 0x47, 0x56, 0x72, 0x62, 0x32, 0x30, 0x67, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x70, 0x64, 0x48, 0x6B, 0x67, 0x0A, 0x52, 0x32, 0x31, 0x69, 0x53, 0x44, + 0x45, 0x72, 0x4D, 0x43, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x69, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x57, 0x74, 0x76, 0x62, 0x53, 0x42, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x55, 0x54, 0x46, + 0x4D, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x44, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x0A, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, + 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4D, 0x36, 0x2F, 0x2F, 0x6C, 0x65, 0x6F, 0x76, 0x39, 0x57, 0x71, 0x39, 0x78, 0x43, 0x61, 0x7A, 0x62, 0x7A, 0x52, 0x45, 0x61, 0x4B, 0x39, 0x5A, 0x30, 0x4C, 0x4D, 0x6B, + 0x4F, 0x73, 0x56, 0x47, 0x4A, 0x44, 0x5A, 0x6F, 0x73, 0x30, 0x4D, 0x4B, 0x69, 0x58, 0x72, 0x50, 0x6B, 0x2F, 0x4F, 0x74, 0x64, 0x4B, 0x50, 0x44, 0x2F, 0x4D, 0x31, 0x0A, 0x32, 0x6B, 0x4F, 0x4C, 0x41, 0x6F, 0x43, 0x2B, 0x62, 0x31, 0x45, 0x6B, + 0x48, 0x51, 0x39, 0x72, 0x4B, 0x38, 0x71, 0x66, 0x77, 0x6D, 0x39, 0x51, 0x4D, 0x75, 0x55, 0x33, 0x49, 0x4C, 0x59, 0x67, 0x2F, 0x34, 0x67, 0x4E, 0x44, 0x32, 0x31, 0x4A, 0x75, 0x39, 0x73, 0x47, 0x70, 0x49, 0x65, 0x51, 0x6B, 0x70, 0x54, 0x30, + 0x43, 0x64, 0x44, 0x50, 0x66, 0x38, 0x69, 0x41, 0x43, 0x38, 0x47, 0x58, 0x73, 0x37, 0x73, 0x31, 0x4A, 0x38, 0x6E, 0x43, 0x47, 0x36, 0x4E, 0x43, 0x0A, 0x4D, 0x45, 0x41, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, + 0x45, 0x46, 0x4F, 0x4E, 0x79, 0x7A, 0x47, 0x36, 0x56, 0x6D, 0x55, 0x65, 0x78, 0x35, 0x72, 0x4E, 0x68, 0x54, 0x4E, 0x48, 0x4C, 0x71, 0x2B, 0x4F, 0x36, 0x7A, 0x64, 0x36, 0x66, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, + 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, + 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x63, 0x41, 0x4D, 0x47, 0x51, 0x43, 0x4D, 0x48, 0x56, 0x53, 0x69, 0x37, 0x65, 0x6B, 0x45, 0x45, 0x2B, 0x75, 0x53, 0x68, 0x43, 0x4C, 0x73, 0x6F, 0x52, 0x62, 0x51, 0x75, + 0x48, 0x6D, 0x4B, 0x6A, 0x59, 0x43, 0x32, 0x71, 0x42, 0x75, 0x47, 0x54, 0x38, 0x6C, 0x76, 0x39, 0x70, 0x5A, 0x0A, 0x4D, 0x6F, 0x37, 0x6B, 0x2B, 0x35, 0x44, 0x63, 0x6B, 0x32, 0x54, 0x4F, 0x72, 0x62, 0x52, 0x42, 0x52, 0x32, 0x44, 0x69, 0x7A, + 0x36, 0x66, 0x4C, 0x48, 0x67, 0x49, 0x77, 0x4E, 0x30, 0x47, 0x4D, 0x5A, 0x74, 0x39, 0x42, 0x61, 0x39, 0x61, 0x44, 0x41, 0x45, 0x48, 0x39, 0x4C, 0x31, 0x72, 0x33, 0x55, 0x4C, 0x52, 0x6E, 0x30, 0x53, 0x79, 0x6F, 0x63, 0x64, 0x64, 0x44, 0x79, + 0x70, 0x77, 0x6E, 0x4A, 0x4A, 0x47, 0x44, 0x53, 0x41, 0x33, 0x50, 0x7A, 0x66, 0x64, 0x55, 0x0A, 0x67, 0x61, 0x2F, 0x73, 0x66, 0x2B, 0x52, 0x6E, 0x32, 0x37, 0x69, 0x51, 0x37, 0x74, 0x30, 0x6C, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, + 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x65, 0x6C, 0x65, 0x6B, 0x6F, 0x6D, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x20, 0x54, 0x4C, 0x53, + 0x20, 0x52, 0x53, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x32, 0x30, 0x32, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, + 0x49, 0x46, 0x73, 0x7A, 0x43, 0x43, 0x41, 0x35, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x49, 0x5A, 0x78, 0x55, 0x4C, 0x65, 0x6A, 0x32, 0x37, 0x48, 0x46, 0x33, 0x2B, 0x6B, 0x37, 0x6F, 0x77, 0x33, 0x42, 0x58, 0x6C, 0x7A, + 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x6A, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x45, 0x52, + 0x54, 0x45, 0x6E, 0x4D, 0x43, 0x55, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x65, 0x52, 0x47, 0x56, 0x31, 0x64, 0x48, 0x4E, 0x6A, 0x61, 0x47, 0x55, 0x67, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x57, 0x74, 0x76, 0x62, 0x53, 0x42, 0x54, 0x5A, + 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x48, 0x62, 0x57, 0x4A, 0x49, 0x4D, 0x53, 0x73, 0x77, 0x4B, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x43, 0x4A, 0x55, 0x0A, 0x5A, 0x57, 0x78, 0x6C, 0x61, 0x32, 0x39, 0x74, + 0x49, 0x46, 0x4E, 0x6C, 0x59, 0x33, 0x56, 0x79, 0x61, 0x58, 0x52, 0x35, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x7A, 0x4D, 0x42, 0x34, 0x58, + 0x44, 0x54, 0x49, 0x7A, 0x4D, 0x44, 0x4D, 0x79, 0x4F, 0x44, 0x45, 0x79, 0x4D, 0x54, 0x59, 0x30, 0x4E, 0x56, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x34, 0x4D, 0x44, 0x4D, 0x79, 0x0A, 0x4E, 0x7A, 0x49, 0x7A, 0x4E, 0x54, 0x6B, 0x31, 0x4F, 0x56, 0x6F, + 0x77, 0x59, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x4A, 0x7A, 0x41, 0x6C, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x48, 0x6B, 0x52, 0x6C, 0x64, 0x58, 0x52, + 0x7A, 0x59, 0x32, 0x68, 0x6C, 0x49, 0x46, 0x52, 0x6C, 0x62, 0x47, 0x56, 0x72, 0x62, 0x32, 0x30, 0x67, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x70, 0x0A, 0x64, 0x48, 0x6B, 0x67, 0x52, 0x32, 0x31, 0x69, 0x53, 0x44, 0x45, 0x72, 0x4D, 0x43, + 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x69, 0x56, 0x47, 0x56, 0x73, 0x5A, 0x57, 0x74, 0x76, 0x62, 0x53, 0x42, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x6C, 0x30, 0x65, 0x53, 0x42, 0x55, 0x54, 0x46, 0x4D, 0x67, 0x55, 0x6C, + 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x7A, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x0A, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, + 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4F, 0x30, 0x31, 0x6F, 0x59, 0x47, 0x41, 0x38, 0x38, 0x74, 0x4B, 0x61, 0x56, 0x76, 0x43, 0x2B, 0x31, 0x47, 0x44, 0x72, + 0x69, 0x62, 0x39, 0x34, 0x57, 0x37, 0x7A, 0x67, 0x52, 0x4A, 0x39, 0x63, 0x55, 0x44, 0x2F, 0x68, 0x33, 0x56, 0x43, 0x0A, 0x4B, 0x53, 0x48, 0x74, 0x67, 0x56, 0x49, 0x73, 0x33, 0x78, 0x4C, 0x42, 0x47, 0x59, 0x53, 0x4A, 0x77, 0x62, 0x33, 0x46, + 0x4B, 0x4E, 0x58, 0x56, 0x53, 0x32, 0x78, 0x45, 0x31, 0x6B, 0x7A, 0x62, 0x42, 0x35, 0x5A, 0x4B, 0x56, 0x58, 0x72, 0x4B, 0x4E, 0x6F, 0x49, 0x45, 0x4E, 0x71, 0x69, 0x6C, 0x2F, 0x43, 0x66, 0x32, 0x53, 0x66, 0x48, 0x56, 0x63, 0x70, 0x36, 0x52, + 0x2B, 0x53, 0x50, 0x57, 0x63, 0x48, 0x75, 0x37, 0x39, 0x5A, 0x76, 0x42, 0x37, 0x4A, 0x50, 0x50, 0x0A, 0x47, 0x65, 0x70, 0x6C, 0x66, 0x6F, 0x68, 0x77, 0x6F, 0x48, 0x50, 0x38, 0x39, 0x76, 0x2B, 0x31, 0x56, 0x6D, 0x4C, 0x68, 0x63, 0x32, 0x6F, + 0x30, 0x6D, 0x44, 0x36, 0x43, 0x75, 0x4B, 0x79, 0x56, 0x55, 0x2F, 0x51, 0x42, 0x6F, 0x43, 0x63, 0x48, 0x63, 0x71, 0x4D, 0x41, 0x55, 0x36, 0x44, 0x6B, 0x73, 0x71, 0x75, 0x44, 0x4F, 0x46, 0x63, 0x7A, 0x4A, 0x5A, 0x53, 0x66, 0x76, 0x6B, 0x67, + 0x64, 0x6D, 0x4F, 0x47, 0x6A, 0x75, 0x70, 0x35, 0x63, 0x7A, 0x51, 0x52, 0x78, 0x0A, 0x55, 0x58, 0x31, 0x31, 0x65, 0x4B, 0x76, 0x7A, 0x57, 0x61, 0x72, 0x45, 0x34, 0x47, 0x43, 0x2B, 0x6A, 0x34, 0x4E, 0x53, 0x75, 0x48, 0x55, 0x61, 0x51, 0x54, + 0x58, 0x74, 0x76, 0x50, 0x4D, 0x36, 0x59, 0x2B, 0x6D, 0x70, 0x46, 0x45, 0x58, 0x58, 0x35, 0x6C, 0x4C, 0x52, 0x62, 0x74, 0x4C, 0x65, 0x76, 0x4F, 0x50, 0x31, 0x43, 0x7A, 0x76, 0x6D, 0x34, 0x4D, 0x53, 0x39, 0x51, 0x32, 0x51, 0x54, 0x70, 0x73, + 0x37, 0x30, 0x6D, 0x44, 0x64, 0x73, 0x69, 0x70, 0x57, 0x6F, 0x0A, 0x6C, 0x38, 0x68, 0x48, 0x44, 0x2F, 0x42, 0x65, 0x45, 0x49, 0x76, 0x6E, 0x48, 0x52, 0x7A, 0x2B, 0x73, 0x54, 0x75, 0x67, 0x42, 0x54, 0x4E, 0x6F, 0x42, 0x55, 0x47, 0x43, 0x77, + 0x51, 0x4D, 0x72, 0x41, 0x63, 0x6A, 0x6E, 0x6A, 0x30, 0x32, 0x72, 0x36, 0x4C, 0x58, 0x32, 0x7A, 0x57, 0x74, 0x45, 0x74, 0x65, 0x66, 0x64, 0x69, 0x2B, 0x7A, 0x71, 0x4A, 0x62, 0x51, 0x41, 0x49, 0x6C, 0x64, 0x4E, 0x73, 0x4C, 0x47, 0x79, 0x4D, + 0x63, 0x45, 0x57, 0x7A, 0x76, 0x2F, 0x39, 0x0A, 0x46, 0x49, 0x53, 0x33, 0x52, 0x2F, 0x71, 0x79, 0x38, 0x58, 0x44, 0x65, 0x32, 0x34, 0x74, 0x73, 0x4E, 0x6C, 0x69, 0x6B, 0x66, 0x4C, 0x4D, 0x52, 0x30, 0x63, 0x4E, 0x33, 0x66, 0x31, 0x2B, 0x32, + 0x4A, 0x65, 0x41, 0x4E, 0x78, 0x64, 0x4B, 0x7A, 0x2B, 0x62, 0x69, 0x34, 0x64, 0x39, 0x73, 0x33, 0x63, 0x58, 0x46, 0x48, 0x34, 0x32, 0x41, 0x59, 0x54, 0x79, 0x53, 0x32, 0x64, 0x54, 0x64, 0x34, 0x75, 0x61, 0x4E, 0x69, 0x72, 0x37, 0x33, 0x4A, + 0x63, 0x6F, 0x34, 0x76, 0x0A, 0x7A, 0x4C, 0x75, 0x75, 0x32, 0x2B, 0x51, 0x56, 0x55, 0x68, 0x6B, 0x48, 0x4D, 0x2F, 0x74, 0x71, 0x74, 0x79, 0x31, 0x4C, 0x6B, 0x43, 0x69, 0x43, 0x63, 0x2F, 0x34, 0x59, 0x69, 0x7A, 0x57, 0x4E, 0x32, 0x36, 0x63, + 0x45, 0x61, 0x72, 0x37, 0x71, 0x77, 0x55, 0x30, 0x32, 0x4F, 0x78, 0x59, 0x32, 0x6B, 0x54, 0x4C, 0x76, 0x74, 0x6B, 0x43, 0x4A, 0x6B, 0x55, 0x50, 0x67, 0x38, 0x71, 0x4B, 0x72, 0x42, 0x43, 0x37, 0x6D, 0x38, 0x6B, 0x77, 0x4F, 0x46, 0x6A, 0x51, + 0x67, 0x0A, 0x72, 0x49, 0x66, 0x42, 0x4C, 0x58, 0x37, 0x4A, 0x5A, 0x6B, 0x63, 0x58, 0x46, 0x42, 0x47, 0x6B, 0x38, 0x2F, 0x65, 0x68, 0x4A, 0x49, 0x6D, 0x72, 0x32, 0x42, 0x72, 0x49, 0x6F, 0x56, 0x79, 0x78, 0x6F, 0x2F, 0x65, 0x4D, 0x62, 0x63, + 0x67, 0x42, 0x79, 0x55, 0x2F, 0x4A, 0x37, 0x4D, 0x54, 0x38, 0x72, 0x46, 0x45, 0x7A, 0x30, 0x63, 0x69, 0x44, 0x30, 0x63, 0x6D, 0x66, 0x48, 0x64, 0x52, 0x48, 0x4E, 0x43, 0x6B, 0x2B, 0x79, 0x37, 0x41, 0x4F, 0x2B, 0x6F, 0x4D, 0x4C, 0x0A, 0x4B, + 0x46, 0x6A, 0x6C, 0x4B, 0x64, 0x77, 0x2F, 0x66, 0x4B, 0x69, 0x66, 0x79, 0x62, 0x59, 0x4B, 0x75, 0x36, 0x62, 0x6F, 0x52, 0x68, 0x59, 0x50, 0x6C, 0x75, 0x56, 0x37, 0x35, 0x47, 0x70, 0x36, 0x53, 0x47, 0x31, 0x32, 0x6D, 0x41, 0x57, 0x6C, 0x33, + 0x47, 0x30, 0x65, 0x51, 0x68, 0x35, 0x43, 0x32, 0x68, 0x72, 0x67, 0x55, 0x76, 0x65, 0x31, 0x67, 0x38, 0x41, 0x61, 0x65, 0x33, 0x67, 0x31, 0x4C, 0x44, 0x6A, 0x31, 0x48, 0x2F, 0x31, 0x4A, 0x6F, 0x79, 0x37, 0x53, 0x0A, 0x57, 0x57, 0x4F, 0x2F, + 0x67, 0x4C, 0x43, 0x4D, 0x6B, 0x33, 0x50, 0x4C, 0x4E, 0x61, 0x61, 0x5A, 0x6C, 0x53, 0x4A, 0x68, 0x5A, 0x51, 0x4E, 0x67, 0x2B, 0x79, 0x2B, 0x54, 0x53, 0x2F, 0x71, 0x61, 0x6E, 0x49, 0x41, 0x37, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, + 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, + 0x55, 0x74, 0x71, 0x65, 0x58, 0x67, 0x6A, 0x31, 0x30, 0x68, 0x5A, 0x76, 0x33, 0x50, 0x4A, 0x2B, 0x54, 0x6D, 0x70, 0x56, 0x35, 0x64, 0x56, 0x4B, 0x4D, 0x62, 0x55, 0x63, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, + 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x53, 0x32, 0x0A, 0x70, 0x35, 0x65, 0x43, 0x50, 0x58, 0x53, 0x46, 0x6D, 0x2F, + 0x63, 0x38, 0x6E, 0x35, 0x4F, 0x61, 0x6C, 0x58, 0x6C, 0x31, 0x55, 0x6F, 0x78, 0x74, 0x52, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, + 0x45, 0x41, 0x71, 0x4D, 0x78, 0x68, 0x70, 0x72, 0x35, 0x31, 0x6E, 0x68, 0x56, 0x51, 0x70, 0x47, 0x76, 0x37, 0x71, 0x48, 0x42, 0x46, 0x66, 0x4C, 0x70, 0x2B, 0x0A, 0x73, 0x56, 0x72, 0x38, 0x57, 0x79, 0x50, 0x36, 0x43, 0x6E, 0x66, 0x34, 0x6D, + 0x48, 0x47, 0x43, 0x44, 0x47, 0x33, 0x67, 0x58, 0x6B, 0x61, 0x71, 0x6B, 0x2F, 0x51, 0x65, 0x6F, 0x4D, 0x50, 0x68, 0x6B, 0x39, 0x74, 0x4C, 0x72, 0x62, 0x4B, 0x6D, 0x58, 0x61, 0x75, 0x77, 0x31, 0x47, 0x4C, 0x4C, 0x58, 0x72, 0x74, 0x6D, 0x39, + 0x53, 0x33, 0x75, 0x6C, 0x30, 0x41, 0x38, 0x59, 0x75, 0x74, 0x65, 0x31, 0x68, 0x54, 0x57, 0x6A, 0x4F, 0x4B, 0x57, 0x69, 0x30, 0x46, 0x70, 0x0A, 0x6B, 0x7A, 0x58, 0x6D, 0x75, 0x5A, 0x6C, 0x72, 0x59, 0x72, 0x53, 0x68, 0x46, 0x32, 0x59, 0x30, + 0x70, 0x6D, 0x74, 0x6A, 0x78, 0x72, 0x6C, 0x4F, 0x38, 0x69, 0x4C, 0x70, 0x57, 0x41, 0x31, 0x57, 0x51, 0x64, 0x48, 0x36, 0x44, 0x45, 0x72, 0x77, 0x4D, 0x38, 0x30, 0x37, 0x75, 0x32, 0x30, 0x68, 0x4F, 0x71, 0x36, 0x4F, 0x63, 0x72, 0x58, 0x44, + 0x53, 0x76, 0x76, 0x70, 0x66, 0x65, 0x57, 0x78, 0x6D, 0x34, 0x62, 0x75, 0x34, 0x75, 0x42, 0x39, 0x74, 0x50, 0x63, 0x79, 0x0A, 0x2F, 0x53, 0x4B, 0x45, 0x38, 0x59, 0x58, 0x4A, 0x4E, 0x33, 0x6E, 0x70, 0x74, 0x54, 0x2B, 0x2F, 0x58, 0x4F, 0x52, + 0x30, 0x73, 0x6F, 0x38, 0x52, 0x59, 0x67, 0x44, 0x64, 0x47, 0x47, 0x61, 0x68, 0x32, 0x58, 0x73, 0x6A, 0x58, 0x2F, 0x47, 0x4F, 0x31, 0x57, 0x66, 0x6F, 0x56, 0x4E, 0x70, 0x62, 0x4F, 0x6D, 0x73, 0x32, 0x62, 0x2F, 0x6D, 0x42, 0x73, 0x54, 0x4E, + 0x48, 0x4D, 0x33, 0x64, 0x41, 0x2B, 0x56, 0x4B, 0x71, 0x33, 0x64, 0x53, 0x44, 0x7A, 0x34, 0x56, 0x34, 0x0A, 0x6D, 0x5A, 0x71, 0x54, 0x75, 0x58, 0x4E, 0x6E, 0x51, 0x6B, 0x59, 0x52, 0x49, 0x65, 0x72, 0x2B, 0x43, 0x71, 0x6B, 0x62, 0x47, 0x6D, + 0x56, 0x70, 0x73, 0x34, 0x2B, 0x75, 0x46, 0x72, 0x62, 0x32, 0x53, 0x31, 0x61, 0x79, 0x4C, 0x66, 0x6D, 0x6C, 0x79, 0x4F, 0x77, 0x37, 0x59, 0x71, 0x50, 0x74, 0x61, 0x39, 0x42, 0x4F, 0x31, 0x55, 0x41, 0x4A, 0x70, 0x42, 0x2B, 0x59, 0x31, 0x7A, + 0x71, 0x6C, 0x6B, 0x6C, 0x6B, 0x67, 0x35, 0x4C, 0x42, 0x39, 0x7A, 0x56, 0x74, 0x7A, 0x0A, 0x61, 0x4C, 0x31, 0x74, 0x78, 0x4B, 0x49, 0x54, 0x44, 0x6D, 0x63, 0x5A, 0x75, 0x49, 0x31, 0x43, 0x66, 0x6D, 0x77, 0x4D, 0x6D, 0x6D, 0x36, 0x67, 0x4A, + 0x43, 0x33, 0x56, 0x52, 0x52, 0x76, 0x63, 0x78, 0x41, 0x49, 0x55, 0x2F, 0x6F, 0x56, 0x62, 0x5A, 0x5A, 0x66, 0x4B, 0x54, 0x70, 0x42, 0x51, 0x43, 0x48, 0x70, 0x43, 0x4E, 0x66, 0x6E, 0x71, 0x77, 0x6D, 0x62, 0x55, 0x2B, 0x41, 0x47, 0x75, 0x48, + 0x72, 0x53, 0x2B, 0x77, 0x36, 0x6A, 0x76, 0x2F, 0x6E, 0x61, 0x61, 0x0A, 0x6F, 0x71, 0x59, 0x66, 0x52, 0x76, 0x61, 0x45, 0x37, 0x66, 0x7A, 0x62, 0x7A, 0x73, 0x51, 0x43, 0x7A, 0x6E, 0x64, 0x49, 0x4C, 0x49, 0x79, 0x79, 0x37, 0x4D, 0x4D, 0x41, + 0x6F, 0x2B, 0x77, 0x73, 0x56, 0x52, 0x6A, 0x42, 0x66, 0x68, 0x6E, 0x75, 0x34, 0x53, 0x2F, 0x79, 0x72, 0x59, 0x4F, 0x62, 0x6E, 0x71, 0x73, 0x5A, 0x33, 0x38, 0x61, 0x4B, 0x4C, 0x34, 0x78, 0x33, 0x35, 0x62, 0x63, 0x46, 0x37, 0x44, 0x76, 0x42, + 0x37, 0x4C, 0x36, 0x47, 0x73, 0x34, 0x61, 0x38, 0x0A, 0x77, 0x50, 0x66, 0x63, 0x35, 0x2B, 0x70, 0x62, 0x72, 0x72, 0x4C, 0x4D, 0x74, 0x54, 0x57, 0x47, 0x53, 0x39, 0x44, 0x69, 0x50, 0x37, 0x62, 0x59, 0x2B, 0x41, 0x34, 0x41, 0x37, 0x6C, 0x33, + 0x6A, 0x39, 0x34, 0x31, 0x59, 0x2F, 0x38, 0x2B, 0x4C, 0x4E, 0x2B, 0x6C, 0x6A, 0x58, 0x32, 0x37, 0x33, 0x43, 0x58, 0x45, 0x32, 0x77, 0x68, 0x4A, 0x64, 0x56, 0x2F, 0x4C, 0x49, 0x74, 0x4D, 0x33, 0x7A, 0x37, 0x67, 0x4C, 0x66, 0x45, 0x64, 0x78, + 0x71, 0x75, 0x56, 0x65, 0x45, 0x0A, 0x48, 0x56, 0x6C, 0x4E, 0x6A, 0x4D, 0x37, 0x49, 0x44, 0x69, 0x50, 0x43, 0x74, 0x79, 0x61, 0x61, 0x45, 0x42, 0x52, 0x78, 0x2F, 0x70, 0x4F, 0x79, 0x69, 0x72, 0x69, 0x41, 0x38, 0x41, 0x34, 0x51, 0x6E, 0x74, + 0x4F, 0x6F, 0x55, 0x41, 0x77, 0x33, 0x67, 0x69, 0x2F, 0x71, 0x34, 0x49, 0x71, 0x64, 0x34, 0x53, 0x77, 0x35, 0x2F, 0x37, 0x57, 0x30, 0x63, 0x77, 0x44, 0x6B, 0x39, 0x30, 0x69, 0x6D, 0x63, 0x36, 0x79, 0x2F, 0x73, 0x74, 0x35, 0x33, 0x42, 0x49, + 0x65, 0x30, 0x0A, 0x6F, 0x38, 0x32, 0x62, 0x4E, 0x53, 0x51, 0x33, 0x2B, 0x70, 0x43, 0x54, 0x45, 0x34, 0x46, 0x43, 0x78, 0x70, 0x67, 0x6D, 0x64, 0x54, 0x64, 0x6D, 0x51, 0x52, 0x43, 0x73, 0x75, 0x2F, 0x57, 0x55, 0x34, 0x38, 0x49, 0x78, 0x4B, + 0x36, 0x33, 0x6E, 0x49, 0x31, 0x62, 0x4D, 0x4E, 0x53, 0x57, 0x53, 0x73, 0x31, 0x41, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x0A, 0x46, 0x49, 0x52, 0x4D, 0x41, 0x50, 0x52, 0x4F, 0x46, 0x45, 0x53, 0x49, 0x4F, 0x4E, 0x41, 0x4C, 0x20, 0x43, 0x41, 0x20, 0x52, 0x4F, 0x4F, 0x54, 0x2D, 0x41, 0x20, 0x57, 0x45, 0x42, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x65, 0x6A, 0x43, 0x43, 0x41, 0x67, 0x43, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x4D, 0x5A, 0x63, 0x68, 0x37, 0x61, 0x2B, + 0x4A, 0x51, 0x6E, 0x38, 0x31, 0x51, 0x59, 0x65, 0x68, 0x5A, 0x31, 0x5A, 0x4D, 0x62, 0x54, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x75, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, + 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x46, 0x0A, 0x55, 0x7A, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x54, 0x52, 0x6D, 0x6C, 0x79, 0x62, 0x57, 0x46, 0x77, 0x63, 0x6D, 0x39, 0x6D, 0x5A, 0x58, + 0x4E, 0x70, 0x62, 0x32, 0x35, 0x68, 0x62, 0x43, 0x42, 0x54, 0x51, 0x54, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x59, 0x51, 0x77, 0x50, 0x56, 0x6B, 0x46, 0x55, 0x52, 0x56, 0x4D, 0x74, 0x51, 0x54, 0x59, 0x79, 0x4E, 0x6A, + 0x4D, 0x30, 0x4D, 0x44, 0x59, 0x34, 0x0A, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x35, 0x47, 0x53, 0x56, 0x4A, 0x4E, 0x51, 0x56, 0x42, 0x53, 0x54, 0x30, 0x5A, 0x46, 0x55, 0x30, 0x6C, 0x50, 0x54, + 0x6B, 0x46, 0x4D, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x50, 0x54, 0x31, 0x51, 0x74, 0x51, 0x53, 0x42, 0x58, 0x52, 0x55, 0x49, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x49, 0x77, 0x4E, 0x44, 0x41, 0x32, 0x4D, 0x44, 0x6B, 0x77, 0x4D, + 0x54, 0x4D, 0x32, 0x0A, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x63, 0x77, 0x4D, 0x7A, 0x4D, 0x78, 0x4D, 0x44, 0x6B, 0x77, 0x4D, 0x54, 0x4D, 0x32, 0x57, 0x6A, 0x42, 0x75, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, + 0x45, 0x77, 0x4A, 0x46, 0x55, 0x7A, 0x45, 0x63, 0x4D, 0x42, 0x6F, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x54, 0x52, 0x6D, 0x6C, 0x79, 0x62, 0x57, 0x46, 0x77, 0x63, 0x6D, 0x39, 0x6D, 0x5A, 0x58, 0x4E, 0x70, 0x62, 0x32, 0x35, 0x68, + 0x0A, 0x62, 0x43, 0x42, 0x54, 0x51, 0x54, 0x45, 0x59, 0x4D, 0x42, 0x59, 0x47, 0x41, 0x31, 0x55, 0x45, 0x59, 0x51, 0x77, 0x50, 0x56, 0x6B, 0x46, 0x55, 0x52, 0x56, 0x4D, 0x74, 0x51, 0x54, 0x59, 0x79, 0x4E, 0x6A, 0x4D, 0x30, 0x4D, 0x44, 0x59, + 0x34, 0x4D, 0x53, 0x63, 0x77, 0x4A, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x44, 0x42, 0x35, 0x47, 0x53, 0x56, 0x4A, 0x4E, 0x51, 0x56, 0x42, 0x53, 0x54, 0x30, 0x5A, 0x46, 0x55, 0x30, 0x6C, 0x50, 0x54, 0x6B, 0x46, 0x4D, 0x0A, 0x49, 0x45, + 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x50, 0x54, 0x31, 0x51, 0x74, 0x51, 0x53, 0x42, 0x58, 0x52, 0x55, 0x49, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, + 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x52, 0x48, 0x55, 0x2B, 0x6F, 0x73, 0x45, 0x61, 0x52, 0x33, 0x78, 0x79, 0x72, 0x71, 0x38, 0x39, 0x5A, 0x66, 0x65, 0x39, 0x4D, 0x45, 0x6B, 0x56, 0x7A, 0x36, 0x0A, 0x69, 0x4D, 0x59, 0x69, 0x75, + 0x59, 0x4D, 0x51, 0x59, 0x6E, 0x65, 0x45, 0x4D, 0x79, 0x33, 0x70, 0x41, 0x34, 0x6A, 0x55, 0x34, 0x44, 0x50, 0x33, 0x37, 0x58, 0x63, 0x73, 0x53, 0x6D, 0x44, 0x71, 0x35, 0x47, 0x2B, 0x74, 0x62, 0x62, 0x54, 0x34, 0x54, 0x49, 0x71, 0x6B, 0x35, + 0x42, 0x2F, 0x4B, 0x36, 0x6B, 0x38, 0x34, 0x53, 0x69, 0x36, 0x43, 0x63, 0x79, 0x76, 0x48, 0x5A, 0x70, 0x73, 0x4B, 0x6A, 0x45, 0x43, 0x63, 0x66, 0x49, 0x72, 0x32, 0x38, 0x6A, 0x6C, 0x67, 0x0A, 0x73, 0x74, 0x37, 0x4C, 0x37, 0x4C, 0x6A, 0x6B, + 0x62, 0x2B, 0x71, 0x62, 0x58, 0x62, 0x64, 0x54, 0x6B, 0x42, 0x67, 0x79, 0x56, 0x63, 0x55, 0x67, 0x74, 0x35, 0x53, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, + 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, 0x55, 0x6B, 0x2B, 0x46, 0x44, 0x0A, 0x59, 0x31, 0x77, 0x38, 0x6E, 0x64, 0x59, 0x6E, 0x38, 0x31, 0x4C, + 0x73, 0x46, 0x37, 0x4B, 0x70, 0x72, 0x79, 0x7A, 0x33, 0x64, 0x76, 0x67, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4A, 0x50, 0x68, 0x51, 0x32, 0x4E, 0x63, 0x50, 0x4A, 0x33, 0x57, 0x4A, 0x2F, 0x4E, + 0x53, 0x37, 0x42, 0x65, 0x79, 0x71, 0x61, 0x38, 0x73, 0x39, 0x33, 0x62, 0x34, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4B, 0x42, 0x67, + 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x41, 0x44, 0x42, 0x6C, 0x41, 0x6A, 0x41, 0x64, 0x66, 0x4B, 0x52, 0x37, 0x77, 0x34, 0x6C, 0x31, 0x4D, 0x2B, 0x45, 0x37, 0x71, 0x55, 0x57, 0x2F, 0x52, 0x75, + 0x6E, 0x70, 0x6F, 0x64, 0x33, 0x4A, 0x49, 0x68, 0x61, 0x33, 0x52, 0x78, 0x45, 0x4C, 0x32, 0x4A, 0x71, 0x36, 0x38, 0x63, 0x67, 0x4C, 0x0A, 0x63, 0x46, 0x42, 0x54, 0x41, 0x70, 0x46, 0x77, 0x68, 0x56, 0x6D, 0x70, 0x48, 0x71, 0x54, 0x6D, 0x36, + 0x69, 0x4D, 0x78, 0x6F, 0x41, 0x41, 0x43, 0x4D, 0x51, 0x44, 0x39, 0x34, 0x76, 0x69, 0x7A, 0x72, 0x78, 0x61, 0x35, 0x48, 0x6E, 0x50, 0x45, 0x6C, 0x75, 0x50, 0x42, 0x4D, 0x42, 0x6E, 0x59, 0x66, 0x75, 0x62, 0x44, 0x6C, 0x39, 0x34, 0x63, 0x54, + 0x37, 0x69, 0x4A, 0x4C, 0x7A, 0x50, 0x72, 0x53, 0x41, 0x38, 0x5A, 0x39, 0x34, 0x64, 0x47, 0x58, 0x53, 0x61, 0x51, 0x0A, 0x70, 0x59, 0x58, 0x46, 0x75, 0x58, 0x71, 0x55, 0x50, 0x6F, 0x65, 0x6F, 0x76, 0x51, 0x41, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, + 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x57, 0x43, 0x41, 0x20, 0x43, 0x59, 0x42, 0x45, 0x52, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, + 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x6A, 0x54, 0x43, 0x43, 0x41, 0x33, 0x57, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x51, 0x41, 0x45, 0x30, 0x6A, 0x4D, 0x49, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x54, 0x7A, 0x79, 0x78, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, 0x51, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x55, 0x56, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x56, 0x45, 0x46, 0x4A, 0x56, 0x30, 0x46, 0x4F, 0x4C, 0x55, 0x4E, 0x42, 0x4D, + 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x77, 0x64, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x52, 0x73, 0x77, 0x47, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4A, 0x55, 0x56, + 0x30, 0x4E, 0x42, 0x0A, 0x49, 0x45, 0x4E, 0x5A, 0x51, 0x6B, 0x56, 0x53, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x49, 0x78, 0x4D, 0x54, 0x49, 0x79, 0x4D, 0x44, 0x59, 0x31, + 0x4E, 0x44, 0x49, 0x35, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x63, 0x78, 0x4D, 0x54, 0x49, 0x79, 0x4D, 0x54, 0x55, 0x31, 0x4F, 0x54, 0x55, 0x35, 0x57, 0x6A, 0x42, 0x51, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, + 0x0A, 0x45, 0x77, 0x4A, 0x55, 0x56, 0x7A, 0x45, 0x53, 0x4D, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4A, 0x56, 0x45, 0x46, 0x4A, 0x56, 0x30, 0x46, 0x4F, 0x4C, 0x55, 0x4E, 0x42, 0x4D, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, + 0x44, 0x56, 0x51, 0x51, 0x4C, 0x45, 0x77, 0x64, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x52, 0x73, 0x77, 0x47, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x4A, 0x55, 0x56, 0x30, 0x4E, 0x42, 0x0A, 0x49, 0x45, + 0x4E, 0x5A, 0x51, 0x6B, 0x56, 0x53, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, + 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x47, 0x2B, 0x4D, 0x6F, 0x65, 0x32, 0x51, 0x6B, 0x67, 0x66, 0x68, 0x31, 0x73, 0x0A, 0x54, 0x73, 0x36, 0x50, 0x34, + 0x30, 0x63, 0x7A, 0x52, 0x4A, 0x7A, 0x48, 0x79, 0x57, 0x6D, 0x71, 0x4F, 0x6C, 0x74, 0x34, 0x37, 0x6E, 0x44, 0x53, 0x6B, 0x76, 0x67, 0x45, 0x73, 0x31, 0x4A, 0x53, 0x48, 0x57, 0x64, 0x79, 0x4B, 0x4B, 0x48, 0x66, 0x69, 0x31, 0x32, 0x56, 0x43, + 0x76, 0x37, 0x71, 0x7A, 0x65, 0x33, 0x33, 0x4B, 0x63, 0x37, 0x77, 0x62, 0x33, 0x2B, 0x73, 0x7A, 0x54, 0x33, 0x76, 0x73, 0x78, 0x78, 0x46, 0x61, 0x76, 0x63, 0x6F, 0x6B, 0x50, 0x46, 0x68, 0x0A, 0x56, 0x38, 0x55, 0x4D, 0x78, 0x4B, 0x4E, 0x51, + 0x58, 0x64, 0x37, 0x55, 0x74, 0x63, 0x73, 0x5A, 0x79, 0x6F, 0x43, 0x35, 0x64, 0x63, 0x34, 0x70, 0x7A, 0x74, 0x4B, 0x46, 0x49, 0x75, 0x77, 0x43, 0x59, 0x38, 0x78, 0x45, 0x4D, 0x43, 0x44, 0x61, 0x36, 0x70, 0x46, 0x62, 0x56, 0x75, 0x59, 0x64, + 0x48, 0x4E, 0x57, 0x64, 0x5A, 0x73, 0x63, 0x2F, 0x33, 0x34, 0x62, 0x4B, 0x53, 0x31, 0x50, 0x45, 0x32, 0x59, 0x32, 0x79, 0x48, 0x65, 0x72, 0x34, 0x33, 0x43, 0x64, 0x54, 0x0A, 0x6F, 0x30, 0x66, 0x68, 0x59, 0x63, 0x78, 0x39, 0x74, 0x62, 0x44, + 0x34, 0x37, 0x6E, 0x4F, 0x52, 0x78, 0x63, 0x35, 0x7A, 0x62, 0x38, 0x37, 0x75, 0x45, 0x42, 0x38, 0x61, 0x42, 0x73, 0x2F, 0x70, 0x4A, 0x32, 0x44, 0x46, 0x54, 0x78, 0x6E, 0x6B, 0x36, 0x38, 0x34, 0x69, 0x4A, 0x6B, 0x58, 0x58, 0x59, 0x4A, 0x6E, + 0x64, 0x7A, 0x6B, 0x38, 0x33, 0x34, 0x48, 0x2F, 0x6E, 0x59, 0x36, 0x32, 0x77, 0x75, 0x46, 0x6D, 0x34, 0x30, 0x41, 0x5A, 0x6F, 0x4E, 0x57, 0x44, 0x54, 0x0A, 0x4E, 0x71, 0x35, 0x78, 0x51, 0x77, 0x54, 0x78, 0x61, 0x57, 0x56, 0x34, 0x66, 0x50, + 0x4D, 0x66, 0x38, 0x38, 0x6F, 0x6F, 0x6E, 0x31, 0x6F, 0x67, 0x6C, 0x57, 0x61, 0x30, 0x7A, 0x62, 0x66, 0x75, 0x6A, 0x33, 0x69, 0x6B, 0x52, 0x52, 0x6A, 0x70, 0x4A, 0x69, 0x2B, 0x4E, 0x6D, 0x79, 0x6B, 0x6F, 0x73, 0x61, 0x53, 0x33, 0x4F, 0x6D, + 0x32, 0x35, 0x31, 0x42, 0x77, 0x34, 0x63, 0x6B, 0x56, 0x59, 0x73, 0x56, 0x37, 0x72, 0x38, 0x43, 0x69, 0x62, 0x74, 0x34, 0x4C, 0x4B, 0x0A, 0x2F, 0x63, 0x2F, 0x57, 0x4D, 0x77, 0x2B, 0x66, 0x2B, 0x35, 0x65, 0x65, 0x73, 0x52, 0x79, 0x63, 0x6E, + 0x75, 0x70, 0x66, 0x58, 0x74, 0x75, 0x71, 0x33, 0x56, 0x54, 0x70, 0x4D, 0x43, 0x45, 0x6F, 0x62, 0x59, 0x35, 0x35, 0x38, 0x33, 0x57, 0x53, 0x6A, 0x43, 0x62, 0x2B, 0x33, 0x4D, 0x58, 0x32, 0x77, 0x37, 0x44, 0x66, 0x52, 0x46, 0x6C, 0x44, 0x6F, + 0x37, 0x59, 0x44, 0x4B, 0x50, 0x59, 0x49, 0x4D, 0x4B, 0x6F, 0x4E, 0x4D, 0x2B, 0x48, 0x76, 0x6E, 0x4B, 0x6B, 0x48, 0x0A, 0x49, 0x75, 0x4E, 0x5A, 0x57, 0x30, 0x43, 0x50, 0x32, 0x6F, 0x69, 0x33, 0x61, 0x51, 0x69, 0x6F, 0x74, 0x79, 0x4D, 0x75, + 0x52, 0x41, 0x6C, 0x5A, 0x4E, 0x31, 0x76, 0x48, 0x34, 0x78, 0x66, 0x79, 0x49, 0x75, 0x74, 0x75, 0x4F, 0x56, 0x4C, 0x46, 0x33, 0x6C, 0x53, 0x6E, 0x6D, 0x4D, 0x6C, 0x4C, 0x49, 0x4A, 0x58, 0x63, 0x52, 0x6F, 0x6C, 0x66, 0x74, 0x42, 0x4C, 0x35, + 0x68, 0x53, 0x6D, 0x4F, 0x36, 0x38, 0x67, 0x6E, 0x46, 0x53, 0x44, 0x41, 0x53, 0x39, 0x54, 0x4D, 0x0A, 0x66, 0x41, 0x78, 0x73, 0x4E, 0x41, 0x77, 0x6D, 0x6D, 0x79, 0x59, 0x78, 0x70, 0x6A, 0x79, 0x6E, 0x39, 0x74, 0x6E, 0x51, 0x53, 0x36, 0x4A, + 0x6B, 0x2F, 0x7A, 0x75, 0x5A, 0x51, 0x58, 0x4C, 0x42, 0x34, 0x48, 0x43, 0x58, 0x38, 0x53, 0x53, 0x37, 0x4B, 0x38, 0x52, 0x30, 0x49, 0x72, 0x47, 0x73, 0x61, 0x79, 0x49, 0x79, 0x4A, 0x4E, 0x4E, 0x34, 0x4B, 0x73, 0x44, 0x41, 0x6F, 0x53, 0x2F, + 0x78, 0x55, 0x67, 0x58, 0x4A, 0x50, 0x2B, 0x39, 0x32, 0x5A, 0x75, 0x4A, 0x46, 0x0A, 0x32, 0x41, 0x30, 0x39, 0x72, 0x5A, 0x58, 0x49, 0x78, 0x34, 0x6B, 0x6D, 0x79, 0x41, 0x2B, 0x75, 0x70, 0x77, 0x4D, 0x75, 0x2B, 0x38, 0x46, 0x66, 0x2B, 0x69, + 0x44, 0x68, 0x63, 0x4B, 0x32, 0x77, 0x5A, 0x53, 0x41, 0x33, 0x4D, 0x32, 0x43, 0x77, 0x31, 0x61, 0x2F, 0x58, 0x44, 0x42, 0x7A, 0x43, 0x6B, 0x48, 0x44, 0x58, 0x53, 0x68, 0x69, 0x38, 0x66, 0x67, 0x47, 0x77, 0x73, 0x4F, 0x73, 0x56, 0x48, 0x6B, + 0x51, 0x47, 0x7A, 0x61, 0x52, 0x50, 0x36, 0x41, 0x7A, 0x52, 0x0A, 0x77, 0x79, 0x41, 0x51, 0x34, 0x56, 0x52, 0x6C, 0x6E, 0x72, 0x5A, 0x52, 0x30, 0x42, 0x70, 0x32, 0x61, 0x30, 0x4A, 0x61, 0x57, 0x48, 0x59, 0x30, 0x36, 0x72, 0x63, 0x33, 0x47, + 0x61, 0x34, 0x75, 0x64, 0x66, 0x6D, 0x57, 0x35, 0x63, 0x46, 0x5A, 0x39, 0x35, 0x52, 0x58, 0x4B, 0x53, 0x57, 0x4E, 0x4F, 0x6B, 0x79, 0x72, 0x54, 0x5A, 0x70, 0x42, 0x30, 0x46, 0x38, 0x6D, 0x41, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6F, + 0x32, 0x4D, 0x77, 0x59, 0x54, 0x41, 0x4F, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x38, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x41, 0x4D, 0x43, 0x41, 0x51, 0x59, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, + 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x53, 0x64, 0x68, 0x57, 0x45, 0x55, 0x66, 0x4D, 0x46, 0x69, 0x62, 0x35, 0x64, 0x6F, + 0x35, 0x45, 0x38, 0x33, 0x0A, 0x51, 0x4F, 0x47, 0x74, 0x34, 0x41, 0x31, 0x57, 0x4E, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x6E, 0x59, 0x56, 0x68, 0x46, 0x48, 0x7A, 0x42, 0x59, 0x6D, 0x2B, + 0x58, 0x61, 0x4F, 0x52, 0x50, 0x4E, 0x30, 0x44, 0x68, 0x72, 0x65, 0x41, 0x4E, 0x56, 0x6A, 0x63, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, + 0x42, 0x0A, 0x41, 0x47, 0x53, 0x50, 0x65, 0x73, 0x52, 0x69, 0x44, 0x72, 0x57, 0x49, 0x7A, 0x4C, 0x6A, 0x48, 0x68, 0x67, 0x36, 0x68, 0x53, 0x68, 0x62, 0x4E, 0x63, 0x41, 0x75, 0x33, 0x70, 0x34, 0x55, 0x4C, 0x73, 0x33, 0x61, 0x32, 0x44, 0x36, + 0x66, 0x2F, 0x43, 0x49, 0x73, 0x4C, 0x4A, 0x63, 0x2B, 0x6F, 0x31, 0x49, 0x4E, 0x31, 0x4B, 0x72, 0x69, 0x57, 0x69, 0x4C, 0x62, 0x37, 0x33, 0x79, 0x30, 0x74, 0x74, 0x47, 0x6C, 0x54, 0x49, 0x54, 0x56, 0x58, 0x31, 0x6F, 0x6C, 0x4E, 0x0A, 0x63, + 0x37, 0x39, 0x70, 0x6A, 0x33, 0x43, 0x6A, 0x59, 0x63, 0x79, 0x61, 0x32, 0x78, 0x36, 0x61, 0x34, 0x43, 0x44, 0x34, 0x62, 0x4C, 0x75, 0x62, 0x49, 0x70, 0x31, 0x64, 0x68, 0x44, 0x47, 0x61, 0x4C, 0x49, 0x72, 0x64, 0x61, 0x71, 0x48, 0x58, 0x4B, + 0x47, 0x6E, 0x4B, 0x2F, 0x6E, 0x5A, 0x56, 0x65, 0x6B, 0x5A, 0x6E, 0x36, 0x38, 0x78, 0x44, 0x69, 0x42, 0x61, 0x69, 0x41, 0x39, 0x61, 0x35, 0x46, 0x2F, 0x67, 0x5A, 0x62, 0x47, 0x30, 0x6A, 0x41, 0x6E, 0x2F, 0x78, 0x0A, 0x58, 0x39, 0x41, 0x4B, + 0x4B, 0x53, 0x4D, 0x37, 0x30, 0x61, 0x6F, 0x4B, 0x37, 0x61, 0x6B, 0x58, 0x4A, 0x6C, 0x51, 0x4B, 0x54, 0x63, 0x4B, 0x6C, 0x54, 0x66, 0x6A, 0x46, 0x2F, 0x62, 0x69, 0x42, 0x7A, 0x79, 0x73, 0x73, 0x65, 0x4B, 0x4E, 0x6E, 0x54, 0x4B, 0x6B, 0x48, + 0x6D, 0x76, 0x50, 0x66, 0x58, 0x76, 0x74, 0x38, 0x39, 0x59, 0x6E, 0x4E, 0x64, 0x4A, 0x64, 0x68, 0x45, 0x47, 0x6F, 0x48, 0x4B, 0x34, 0x46, 0x61, 0x30, 0x6F, 0x36, 0x33, 0x35, 0x79, 0x44, 0x52, 0x0A, 0x49, 0x47, 0x34, 0x6B, 0x71, 0x49, 0x51, + 0x6E, 0x6F, 0x56, 0x65, 0x73, 0x71, 0x6C, 0x56, 0x59, 0x4C, 0x39, 0x7A, 0x5A, 0x79, 0x76, 0x70, 0x6F, 0x42, 0x4A, 0x37, 0x74, 0x52, 0x43, 0x54, 0x35, 0x64, 0x45, 0x41, 0x37, 0x49, 0x7A, 0x4F, 0x72, 0x67, 0x31, 0x6F, 0x59, 0x4A, 0x6B, 0x4B, + 0x32, 0x62, 0x56, 0x53, 0x31, 0x46, 0x6D, 0x41, 0x77, 0x62, 0x4C, 0x47, 0x67, 0x2B, 0x4C, 0x68, 0x42, 0x6F, 0x46, 0x31, 0x4A, 0x53, 0x64, 0x4A, 0x6C, 0x42, 0x54, 0x72, 0x71, 0x0A, 0x2F, 0x70, 0x31, 0x68, 0x76, 0x49, 0x62, 0x5A, 0x76, 0x39, + 0x37, 0x54, 0x75, 0x6A, 0x71, 0x78, 0x66, 0x33, 0x36, 0x53, 0x4E, 0x49, 0x37, 0x4A, 0x41, 0x47, 0x37, 0x63, 0x6D, 0x4C, 0x33, 0x63, 0x37, 0x49, 0x41, 0x46, 0x72, 0x51, 0x49, 0x39, 0x33, 0x32, 0x58, 0x74, 0x43, 0x77, 0x50, 0x33, 0x39, 0x78, + 0x61, 0x45, 0x42, 0x44, 0x47, 0x36, 0x6B, 0x35, 0x54, 0x59, 0x38, 0x68, 0x4C, 0x34, 0x69, 0x75, 0x4F, 0x2F, 0x51, 0x71, 0x2B, 0x6E, 0x31, 0x4D, 0x30, 0x52, 0x0A, 0x46, 0x78, 0x62, 0x49, 0x51, 0x68, 0x30, 0x55, 0x71, 0x45, 0x4C, 0x32, 0x30, + 0x6B, 0x43, 0x47, 0x6F, 0x45, 0x38, 0x6A, 0x79, 0x70, 0x5A, 0x46, 0x56, 0x6D, 0x41, 0x47, 0x7A, 0x62, 0x64, 0x56, 0x41, 0x61, 0x59, 0x42, 0x6C, 0x47, 0x58, 0x2B, 0x62, 0x67, 0x55, 0x4A, 0x75, 0x72, 0x53, 0x6B, 0x71, 0x75, 0x4C, 0x76, 0x57, + 0x4C, 0x36, 0x39, 0x4A, 0x31, 0x62, 0x59, 0x37, 0x33, 0x4E, 0x78, 0x57, 0x30, 0x51, 0x7A, 0x38, 0x70, 0x70, 0x79, 0x36, 0x72, 0x42, 0x65, 0x0A, 0x50, 0x6D, 0x36, 0x70, 0x55, 0x6C, 0x76, 0x73, 0x63, 0x47, 0x32, 0x31, 0x68, 0x34, 0x38, 0x33, + 0x58, 0x6A, 0x79, 0x4D, 0x6E, 0x4D, 0x37, 0x6B, 0x38, 0x4D, 0x34, 0x4D, 0x5A, 0x30, 0x48, 0x4D, 0x7A, 0x76, 0x61, 0x41, 0x71, 0x30, 0x37, 0x4D, 0x54, 0x46, 0x62, 0x31, 0x77, 0x57, 0x46, 0x5A, 0x6B, 0x37, 0x51, 0x2B, 0x70, 0x74, 0x71, 0x34, + 0x4E, 0x78, 0x4B, 0x66, 0x4B, 0x6A, 0x4C, 0x6A, 0x69, 0x37, 0x67, 0x68, 0x37, 0x4D, 0x4D, 0x72, 0x5A, 0x51, 0x7A, 0x76, 0x0A, 0x49, 0x74, 0x36, 0x49, 0x4B, 0x54, 0x74, 0x4D, 0x31, 0x2F, 0x72, 0x2B, 0x74, 0x2B, 0x46, 0x48, 0x76, 0x70, 0x77, + 0x2B, 0x50, 0x6F, 0x50, 0x37, 0x55, 0x56, 0x33, 0x31, 0x61, 0x50, 0x63, 0x75, 0x49, 0x59, 0x58, 0x63, 0x76, 0x2F, 0x46, 0x61, 0x34, 0x6E, 0x7A, 0x58, 0x78, 0x65, 0x53, 0x44, 0x77, 0x57, 0x72, 0x72, 0x75, 0x6F, 0x42, 0x61, 0x33, 0x6C, 0x77, + 0x74, 0x63, 0x48, 0x62, 0x34, 0x79, 0x4F, 0x57, 0x48, 0x68, 0x38, 0x71, 0x67, 0x6E, 0x61, 0x48, 0x6C, 0x0A, 0x49, 0x68, 0x49, 0x6E, 0x44, 0x30, 0x51, 0x39, 0x48, 0x57, 0x7A, 0x71, 0x31, 0x4D, 0x4B, 0x4C, 0x4C, 0x32, 0x39, 0x35, 0x71, 0x33, + 0x39, 0x51, 0x70, 0x73, 0x51, 0x5A, 0x70, 0x36, 0x46, 0x36, 0x74, 0x35, 0x62, 0x35, 0x77, 0x52, 0x39, 0x69, 0x57, 0x71, 0x4A, 0x44, 0x42, 0x30, 0x42, 0x65, 0x4A, 0x73, 0x61, 0x73, 0x37, 0x61, 0x35, 0x77, 0x46, 0x73, 0x57, 0x71, 0x79, 0x6E, + 0x4B, 0x4B, 0x54, 0x62, 0x44, 0x50, 0x41, 0x59, 0x73, 0x44, 0x50, 0x32, 0x37, 0x58, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x31, 0x32, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x44, 0x63, 0x6A, 0x43, 0x43, + 0x41, 0x6C, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x5A, 0x76, 0x6E, 0x48, 0x77, 0x61, 0x2F, 0x73, 0x77, 0x6C, 0x47, 0x30, 0x37, 0x56, 0x4F, 0x58, 0x35, 0x75, 0x61, 0x43, 0x77, 0x79, 0x73, 0x63, 0x6B, 0x42, 0x59, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x55, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x6C, 0x41, + 0x78, 0x49, 0x7A, 0x41, 0x68, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x6B, 0x4E, 0x35, 0x59, 0x6D, 0x56, 0x79, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x53, 0x6D, 0x46, 0x77, 0x59, 0x57, 0x34, 0x67, 0x51, 0x32, 0x38, + 0x75, 0x4C, 0x43, 0x42, 0x4D, 0x64, 0x47, 0x51, 0x75, 0x4D, 0x52, 0x30, 0x77, 0x47, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x52, 0x54, 0x0A, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, + 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x78, 0x4D, 0x6A, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x44, 0x41, 0x30, 0x4D, 0x44, 0x67, 0x77, 0x4E, 0x54, 0x4D, 0x32, 0x4E, 0x44, 0x5A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4D, 0x44, + 0x41, 0x30, 0x4D, 0x44, 0x67, 0x77, 0x4E, 0x54, 0x4D, 0x32, 0x4E, 0x44, 0x5A, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x70, 0x51, 0x4D, 0x53, 0x4D, 0x77, 0x49, + 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x70, 0x44, 0x65, 0x57, 0x4A, 0x6C, 0x63, 0x6E, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x70, 0x68, 0x63, 0x47, 0x46, 0x75, 0x49, 0x45, 0x4E, 0x76, 0x4C, 0x69, 0x77, 0x67, 0x54, + 0x48, 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x64, 0x4D, 0x42, 0x73, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x55, 0x0A, 0x55, 0x32, 0x56, 0x6A, 0x64, 0x58, 0x4A, 0x6C, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, + 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x54, 0x49, 0x77, 0x67, 0x67, 0x45, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x44, 0x77, 0x41, 0x77, + 0x67, 0x67, 0x45, 0x4B, 0x41, 0x6F, 0x49, 0x42, 0x41, 0x51, 0x43, 0x36, 0x4F, 0x63, 0x45, 0x33, 0x0A, 0x65, 0x6D, 0x68, 0x46, 0x4B, 0x78, 0x53, 0x30, 0x36, 0x2B, 0x51, 0x54, 0x36, 0x31, 0x64, 0x31, 0x49, 0x30, 0x32, 0x50, 0x4A, 0x43, 0x30, + 0x57, 0x36, 0x4B, 0x36, 0x4F, 0x79, 0x58, 0x32, 0x6B, 0x56, 0x7A, 0x73, 0x71, 0x64, 0x69, 0x55, 0x7A, 0x67, 0x32, 0x7A, 0x71, 0x4D, 0x6F, 0x71, 0x55, 0x6D, 0x30, 0x34, 0x38, 0x6C, 0x75, 0x54, 0x39, 0x55, 0x62, 0x2B, 0x5A, 0x79, 0x5A, 0x4E, + 0x2B, 0x76, 0x2F, 0x6D, 0x74, 0x70, 0x37, 0x4A, 0x49, 0x4B, 0x77, 0x63, 0x63, 0x0A, 0x4A, 0x2F, 0x56, 0x4D, 0x76, 0x48, 0x41, 0x53, 0x64, 0x36, 0x53, 0x46, 0x56, 0x4C, 0x58, 0x39, 0x6B, 0x48, 0x72, 0x6B, 0x6F, 0x2B, 0x52, 0x52, 0x57, 0x41, + 0x50, 0x4E, 0x45, 0x48, 0x6C, 0x35, 0x37, 0x6D, 0x75, 0x54, 0x48, 0x32, 0x53, 0x4F, 0x61, 0x32, 0x53, 0x72, 0x6F, 0x78, 0x50, 0x6A, 0x63, 0x66, 0x35, 0x39, 0x71, 0x35, 0x7A, 0x64, 0x4A, 0x31, 0x4D, 0x33, 0x73, 0x36, 0x6F, 0x59, 0x77, 0x6C, + 0x6B, 0x6D, 0x37, 0x46, 0x73, 0x66, 0x30, 0x75, 0x5A, 0x6C, 0x0A, 0x66, 0x4F, 0x2B, 0x54, 0x76, 0x64, 0x68, 0x59, 0x58, 0x41, 0x76, 0x41, 0x34, 0x32, 0x56, 0x76, 0x50, 0x4D, 0x66, 0x4B, 0x57, 0x65, 0x50, 0x2B, 0x62, 0x6C, 0x2B, 0x73, 0x67, + 0x37, 0x37, 0x39, 0x58, 0x53, 0x56, 0x4F, 0x4B, 0x69, 0x6B, 0x37, 0x31, 0x67, 0x75, 0x72, 0x46, 0x7A, 0x4A, 0x34, 0x70, 0x4F, 0x45, 0x2B, 0x6C, 0x45, 0x61, 0x2B, 0x59, 0x6D, 0x36, 0x62, 0x33, 0x6B, 0x61, 0x6F, 0x73, 0x52, 0x62, 0x6E, 0x68, + 0x57, 0x37, 0x30, 0x43, 0x45, 0x42, 0x46, 0x0A, 0x45, 0x61, 0x43, 0x65, 0x56, 0x45, 0x53, 0x45, 0x39, 0x39, 0x67, 0x32, 0x7A, 0x76, 0x56, 0x51, 0x52, 0x39, 0x77, 0x73, 0x4D, 0x4A, 0x76, 0x75, 0x77, 0x50, 0x57, 0x57, 0x30, 0x76, 0x34, 0x4A, + 0x68, 0x73, 0x63, 0x47, 0x57, 0x61, 0x35, 0x50, 0x72, 0x6F, 0x34, 0x52, 0x6D, 0x48, 0x76, 0x7A, 0x43, 0x31, 0x4B, 0x71, 0x59, 0x69, 0x61, 0x71, 0x49, 0x64, 0x2B, 0x4F, 0x4A, 0x54, 0x4E, 0x35, 0x6C, 0x78, 0x5A, 0x4A, 0x6A, 0x66, 0x55, 0x2B, + 0x31, 0x55, 0x65, 0x66, 0x0A, 0x4E, 0x7A, 0x46, 0x4A, 0x4D, 0x33, 0x49, 0x46, 0x54, 0x51, 0x79, 0x32, 0x56, 0x59, 0x7A, 0x78, 0x56, 0x34, 0x2B, 0x4B, 0x68, 0x39, 0x47, 0x74, 0x78, 0x52, 0x45, 0x53, 0x4F, 0x61, 0x43, 0x74, 0x41, 0x67, 0x4D, + 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, + 0x50, 0x0A, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x52, 0x58, 0x4E, 0x50, 0x4E, 0x30, 0x7A, 0x77, 0x52, 0x4C, 0x31, 0x53, + 0x58, 0x6D, 0x38, 0x55, 0x43, 0x32, 0x4C, 0x45, 0x7A, 0x5A, 0x4C, 0x65, 0x6D, 0x67, 0x72, 0x54, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x0A, 0x41, + 0x51, 0x45, 0x41, 0x50, 0x72, 0x76, 0x62, 0x46, 0x78, 0x62, 0x53, 0x38, 0x68, 0x51, 0x42, 0x49, 0x43, 0x77, 0x34, 0x67, 0x30, 0x75, 0x74, 0x76, 0x73, 0x71, 0x46, 0x65, 0x70, 0x71, 0x32, 0x6D, 0x32, 0x75, 0x6D, 0x34, 0x66, 0x79, 0x6C, 0x4F, + 0x71, 0x79, 0x74, 0x74, 0x43, 0x67, 0x36, 0x72, 0x39, 0x63, 0x42, 0x67, 0x30, 0x6B, 0x72, 0x59, 0x36, 0x4C, 0x64, 0x6D, 0x6D, 0x51, 0x4F, 0x6D, 0x46, 0x78, 0x76, 0x33, 0x59, 0x36, 0x37, 0x69, 0x6C, 0x51, 0x69, 0x0A, 0x4C, 0x55, 0x6F, 0x54, + 0x38, 0x36, 0x35, 0x41, 0x51, 0x39, 0x74, 0x50, 0x6B, 0x62, 0x65, 0x47, 0x47, 0x75, 0x77, 0x41, 0x74, 0x45, 0x47, 0x42, 0x70, 0x45, 0x2F, 0x36, 0x61, 0x6F, 0x75, 0x49, 0x73, 0x33, 0x59, 0x49, 0x63, 0x69, 0x70, 0x4A, 0x51, 0x4D, 0x50, 0x54, + 0x77, 0x34, 0x57, 0x4A, 0x6D, 0x42, 0x43, 0x6C, 0x6E, 0x57, 0x38, 0x5A, 0x74, 0x37, 0x76, 0x50, 0x65, 0x6D, 0x56, 0x56, 0x32, 0x7A, 0x66, 0x72, 0x50, 0x49, 0x70, 0x79, 0x4D, 0x70, 0x63, 0x65, 0x0A, 0x6D, 0x69, 0x6B, 0x2B, 0x72, 0x59, 0x33, + 0x6D, 0x6F, 0x78, 0x74, 0x74, 0x39, 0x58, 0x55, 0x61, 0x35, 0x72, 0x42, 0x6F, 0x75, 0x56, 0x75, 0x69, 0x37, 0x6D, 0x6C, 0x48, 0x4A, 0x7A, 0x57, 0x68, 0x68, 0x70, 0x6D, 0x41, 0x38, 0x7A, 0x4E, 0x4C, 0x34, 0x57, 0x75, 0x6B, 0x4A, 0x73, 0x50, + 0x76, 0x64, 0x46, 0x6C, 0x73, 0x65, 0x71, 0x4A, 0x6B, 0x74, 0x68, 0x35, 0x45, 0x77, 0x31, 0x44, 0x67, 0x44, 0x7A, 0x6B, 0x39, 0x71, 0x54, 0x50, 0x78, 0x70, 0x66, 0x50, 0x53, 0x0A, 0x76, 0x57, 0x4B, 0x45, 0x72, 0x49, 0x34, 0x63, 0x71, 0x63, + 0x31, 0x61, 0x76, 0x54, 0x63, 0x37, 0x62, 0x67, 0x6F, 0x69, 0x74, 0x50, 0x51, 0x56, 0x35, 0x35, 0x46, 0x59, 0x78, 0x54, 0x70, 0x45, 0x30, 0x35, 0x55, 0x6F, 0x32, 0x63, 0x42, 0x6C, 0x36, 0x58, 0x4C, 0x4B, 0x30, 0x41, 0x2B, 0x39, 0x48, 0x37, + 0x4D, 0x56, 0x32, 0x61, 0x6E, 0x6A, 0x70, 0x45, 0x63, 0x4A, 0x6E, 0x75, 0x44, 0x4C, 0x4E, 0x2F, 0x76, 0x39, 0x76, 0x5A, 0x66, 0x56, 0x76, 0x68, 0x67, 0x61, 0x0A, 0x61, 0x61, 0x49, 0x35, 0x67, 0x64, 0x6B, 0x61, 0x39, 0x61, 0x74, 0x2F, 0x79, + 0x4F, 0x50, 0x69, 0x5A, 0x77, 0x75, 0x64, 0x39, 0x41, 0x7A, 0x71, 0x56, 0x4E, 0x2F, 0x53, 0x73, 0x71, 0x2B, 0x78, 0x49, 0x76, 0x45, 0x67, 0x33, 0x37, 0x78, 0x45, 0x48, 0x41, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x31, 0x34, 0x0A, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x63, 0x6A, 0x43, 0x43, 0x41, 0x31, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x5A, 0x4E, 0x74, 0x61, 0x44, 0x43, 0x42, 0x4F, 0x36, 0x4E, 0x63, + 0x70, 0x64, 0x38, 0x68, 0x51, 0x4A, 0x36, 0x4A, 0x61, 0x4A, 0x39, 0x30, 0x74, 0x38, 0x73, 0x73, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x77, 0x55, 0x54, 0x45, + 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x6C, 0x41, 0x78, 0x49, 0x7A, 0x41, 0x68, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x6B, 0x4E, 0x35, 0x59, 0x6D, 0x56, 0x79, 0x64, 0x48, + 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x53, 0x6D, 0x46, 0x77, 0x59, 0x57, 0x34, 0x67, 0x51, 0x32, 0x38, 0x75, 0x4C, 0x43, 0x42, 0x4D, 0x64, 0x47, 0x51, 0x75, 0x4D, 0x52, 0x30, 0x77, 0x47, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, + 0x52, 0x54, 0x0A, 0x5A, 0x57, 0x4E, 0x31, 0x63, 0x6D, 0x56, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x78, 0x4E, 0x44, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x44, 0x41, 0x30, 0x4D, + 0x44, 0x67, 0x77, 0x4E, 0x7A, 0x41, 0x32, 0x4D, 0x54, 0x6C, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4E, 0x54, 0x41, 0x30, 0x4D, 0x44, 0x67, 0x77, 0x4E, 0x7A, 0x41, 0x32, 0x4D, 0x54, 0x6C, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x0A, + 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x70, 0x51, 0x4D, 0x53, 0x4D, 0x77, 0x49, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x70, 0x44, 0x65, 0x57, 0x4A, 0x6C, 0x63, 0x6E, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, + 0x49, 0x45, 0x70, 0x68, 0x63, 0x47, 0x46, 0x75, 0x49, 0x45, 0x4E, 0x76, 0x4C, 0x69, 0x77, 0x67, 0x54, 0x48, 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x64, 0x4D, 0x42, 0x73, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x55, 0x0A, 0x55, 0x32, 0x56, + 0x6A, 0x64, 0x58, 0x4A, 0x6C, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x4D, 0x54, 0x51, 0x77, 0x67, 0x67, 0x49, 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, + 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, 0x46, 0x30, 0x6E, 0x71, 0x68, 0x0A, 0x31, 0x6F, 0x71, 0x2F, 0x46, 0x6A, + 0x48, 0x51, 0x6D, 0x4E, 0x45, 0x36, 0x6C, 0x50, 0x78, 0x61, 0x75, 0x47, 0x34, 0x69, 0x77, 0x57, 0x4C, 0x33, 0x70, 0x77, 0x6F, 0x6E, 0x37, 0x31, 0x44, 0x32, 0x4C, 0x72, 0x47, 0x65, 0x61, 0x42, 0x4C, 0x77, 0x62, 0x43, 0x52, 0x6A, 0x4F, 0x66, + 0x48, 0x77, 0x33, 0x78, 0x44, 0x47, 0x33, 0x72, 0x64, 0x53, 0x49, 0x4E, 0x56, 0x53, 0x57, 0x30, 0x4B, 0x5A, 0x6E, 0x76, 0x4F, 0x67, 0x76, 0x6C, 0x49, 0x66, 0x58, 0x38, 0x78, 0x6E, 0x0A, 0x62, 0x61, 0x63, 0x75, 0x55, 0x4B, 0x4C, 0x42, 0x6C, + 0x34, 0x32, 0x32, 0x2B, 0x4A, 0x58, 0x31, 0x73, 0x4C, 0x72, 0x63, 0x6E, 0x65, 0x43, 0x2B, 0x79, 0x39, 0x2F, 0x33, 0x4F, 0x50, 0x4A, 0x48, 0x39, 0x61, 0x61, 0x61, 0x6B, 0x70, 0x55, 0x71, 0x59, 0x6C, 0x6C, 0x51, 0x43, 0x36, 0x4B, 0x78, 0x4E, + 0x65, 0x64, 0x6C, 0x73, 0x6D, 0x47, 0x79, 0x36, 0x70, 0x4A, 0x78, 0x61, 0x65, 0x51, 0x70, 0x38, 0x45, 0x2B, 0x42, 0x67, 0x51, 0x51, 0x38, 0x73, 0x71, 0x56, 0x62, 0x0A, 0x31, 0x4D, 0x57, 0x6F, 0x57, 0x57, 0x64, 0x37, 0x56, 0x52, 0x78, 0x4A, + 0x71, 0x33, 0x71, 0x64, 0x77, 0x75, 0x64, 0x7A, 0x54, 0x65, 0x2F, 0x4E, 0x43, 0x63, 0x4C, 0x45, 0x56, 0x78, 0x4C, 0x62, 0x41, 0x51, 0x34, 0x6A, 0x65, 0x51, 0x6B, 0x48, 0x4F, 0x36, 0x4C, 0x6F, 0x2F, 0x49, 0x72, 0x50, 0x6A, 0x38, 0x42, 0x47, + 0x4A, 0x4A, 0x77, 0x34, 0x4A, 0x2B, 0x43, 0x44, 0x6E, 0x52, 0x75, 0x67, 0x76, 0x33, 0x67, 0x56, 0x45, 0x4F, 0x75, 0x47, 0x54, 0x67, 0x70, 0x61, 0x0A, 0x2F, 0x64, 0x2F, 0x61, 0x4C, 0x49, 0x4A, 0x2B, 0x37, 0x73, 0x72, 0x32, 0x4B, 0x65, 0x48, + 0x36, 0x63, 0x61, 0x48, 0x33, 0x69, 0x47, 0x69, 0x63, 0x6E, 0x50, 0x43, 0x4E, 0x76, 0x67, 0x39, 0x4A, 0x6B, 0x64, 0x6A, 0x71, 0x4F, 0x76, 0x6E, 0x39, 0x30, 0x47, 0x68, 0x78, 0x32, 0x2B, 0x6D, 0x31, 0x4B, 0x30, 0x36, 0x43, 0x6B, 0x6D, 0x39, + 0x6D, 0x48, 0x2B, 0x44, 0x77, 0x33, 0x45, 0x7A, 0x73, 0x79, 0x74, 0x48, 0x71, 0x75, 0x6E, 0x51, 0x47, 0x2B, 0x62, 0x4F, 0x45, 0x0A, 0x6B, 0x4A, 0x54, 0x52, 0x58, 0x34, 0x35, 0x7A, 0x47, 0x52, 0x42, 0x64, 0x41, 0x75, 0x56, 0x77, 0x70, 0x63, + 0x41, 0x51, 0x30, 0x42, 0x42, 0x38, 0x62, 0x38, 0x56, 0x59, 0x53, 0x62, 0x53, 0x77, 0x62, 0x70, 0x72, 0x61, 0x66, 0x5A, 0x58, 0x31, 0x7A, 0x4E, 0x6F, 0x43, 0x72, 0x37, 0x67, 0x73, 0x66, 0x58, 0x6D, 0x50, 0x76, 0x6B, 0x50, 0x78, 0x2B, 0x53, + 0x67, 0x6F, 0x6A, 0x51, 0x6C, 0x44, 0x2B, 0x41, 0x6A, 0x64, 0x61, 0x38, 0x69, 0x4C, 0x4C, 0x43, 0x53, 0x78, 0x0A, 0x6A, 0x56, 0x49, 0x48, 0x76, 0x58, 0x69, 0x62, 0x79, 0x38, 0x70, 0x6F, 0x73, 0x71, 0x54, 0x64, 0x44, 0x45, 0x78, 0x35, 0x59, + 0x4D, 0x61, 0x5A, 0x30, 0x5A, 0x50, 0x78, 0x4D, 0x42, 0x6F, 0x48, 0x30, 0x36, 0x34, 0x69, 0x77, 0x75, 0x72, 0x4F, 0x38, 0x59, 0x51, 0x4A, 0x7A, 0x4F, 0x41, 0x55, 0x62, 0x6E, 0x38, 0x2F, 0x66, 0x74, 0x4B, 0x43, 0x68, 0x61, 0x7A, 0x63, 0x71, + 0x52, 0x5A, 0x4F, 0x68, 0x61, 0x42, 0x67, 0x79, 0x2F, 0x61, 0x63, 0x31, 0x38, 0x69, 0x7A, 0x0A, 0x6A, 0x75, 0x33, 0x47, 0x6D, 0x35, 0x68, 0x31, 0x44, 0x56, 0x58, 0x6F, 0x58, 0x2B, 0x57, 0x56, 0x69, 0x77, 0x4B, 0x6B, 0x72, 0x6B, 0x4D, 0x70, + 0x4B, 0x42, 0x47, 0x6B, 0x35, 0x68, 0x49, 0x77, 0x41, 0x55, 0x74, 0x31, 0x61, 0x78, 0x35, 0x6D, 0x6E, 0x58, 0x6B, 0x76, 0x70, 0x58, 0x59, 0x76, 0x48, 0x55, 0x43, 0x30, 0x62, 0x63, 0x6C, 0x39, 0x65, 0x51, 0x6A, 0x73, 0x30, 0x57, 0x71, 0x32, + 0x58, 0x53, 0x71, 0x79, 0x70, 0x57, 0x61, 0x39, 0x61, 0x34, 0x58, 0x30, 0x0A, 0x64, 0x46, 0x62, 0x44, 0x39, 0x65, 0x64, 0x31, 0x55, 0x69, 0x67, 0x73, 0x70, 0x66, 0x39, 0x6D, 0x52, 0x36, 0x58, 0x55, 0x2F, 0x76, 0x36, 0x65, 0x56, 0x4C, 0x39, + 0x6C, 0x66, 0x67, 0x48, 0x57, 0x4D, 0x49, 0x2B, 0x6C, 0x4E, 0x70, 0x79, 0x69, 0x55, 0x42, 0x7A, 0x75, 0x4F, 0x49, 0x41, 0x42, 0x53, 0x4D, 0x62, 0x48, 0x64, 0x50, 0x54, 0x47, 0x72, 0x4D, 0x4E, 0x41, 0x53, 0x52, 0x5A, 0x68, 0x64, 0x43, 0x79, + 0x76, 0x6A, 0x47, 0x38, 0x31, 0x37, 0x58, 0x73, 0x59, 0x0A, 0x41, 0x46, 0x73, 0x32, 0x50, 0x4A, 0x78, 0x51, 0x44, 0x63, 0x71, 0x53, 0x4D, 0x78, 0x44, 0x78, 0x4A, 0x6B, 0x6C, 0x74, 0x33, 0x33, 0x55, 0x6B, 0x4E, 0x34, 0x49, 0x69, 0x31, 0x2B, + 0x69, 0x57, 0x2F, 0x52, 0x56, 0x4C, 0x41, 0x70, 0x59, 0x2B, 0x42, 0x33, 0x4B, 0x56, 0x66, 0x71, 0x73, 0x39, 0x54, 0x43, 0x37, 0x58, 0x79, 0x76, 0x44, 0x66, 0x34, 0x46, 0x67, 0x2F, 0x4C, 0x53, 0x38, 0x45, 0x6D, 0x6A, 0x69, 0x6A, 0x41, 0x51, + 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x0A, 0x6F, 0x30, 0x49, 0x77, 0x51, 0x44, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, + 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x42, 0x70, 0x4F, 0x6A, 0x43, 0x6C, 0x34, 0x6F, 0x61, + 0x54, 0x65, 0x71, 0x0A, 0x59, 0x52, 0x33, 0x72, 0x36, 0x2F, 0x77, 0x74, 0x62, 0x79, 0x50, 0x6B, 0x38, 0x36, 0x41, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x44, + 0x67, 0x67, 0x49, 0x42, 0x41, 0x4A, 0x61, 0x41, 0x63, 0x67, 0x6B, 0x47, 0x66, 0x70, 0x7A, 0x4D, 0x6B, 0x77, 0x51, 0x57, 0x75, 0x36, 0x41, 0x36, 0x6A, 0x5A, 0x4A, 0x4F, 0x74, 0x78, 0x45, 0x61, 0x43, 0x6E, 0x46, 0x78, 0x45, 0x4D, 0x30, 0x45, + 0x0A, 0x72, 0x58, 0x2B, 0x6C, 0x52, 0x56, 0x41, 0x51, 0x5A, 0x6B, 0x35, 0x4B, 0x51, 0x61, 0x49, 0x44, 0x32, 0x52, 0x46, 0x50, 0x65, 0x6A, 0x65, 0x35, 0x53, 0x2B, 0x4C, 0x47, 0x6A, 0x7A, 0x4A, 0x6D, 0x64, 0x53, 0x58, 0x37, 0x36, 0x38, 0x34, + 0x2F, 0x41, 0x79, 0x6B, 0x6D, 0x6A, 0x62, 0x67, 0x57, 0x48, 0x66, 0x59, 0x66, 0x4D, 0x32, 0x35, 0x49, 0x35, 0x75, 0x6A, 0x34, 0x56, 0x37, 0x49, 0x62, 0x65, 0x64, 0x38, 0x37, 0x68, 0x77, 0x72, 0x69, 0x5A, 0x4C, 0x6F, 0x41, 0x0A, 0x79, 0x6D, + 0x7A, 0x76, 0x66, 0x74, 0x41, 0x6A, 0x36, 0x33, 0x69, 0x50, 0x2F, 0x32, 0x53, 0x62, 0x4E, 0x44, 0x65, 0x66, 0x4E, 0x57, 0x57, 0x69, 0x70, 0x41, 0x41, 0x39, 0x45, 0x69, 0x4F, 0x57, 0x57, 0x46, 0x33, 0x4B, 0x59, 0x34, 0x66, 0x47, 0x6F, 0x77, + 0x65, 0x49, 0x54, 0x65, 0x64, 0x70, 0x64, 0x6F, 0x70, 0x54, 0x7A, 0x66, 0x46, 0x50, 0x37, 0x45, 0x4C, 0x79, 0x6B, 0x2B, 0x4F, 0x5A, 0x70, 0x44, 0x63, 0x38, 0x68, 0x37, 0x68, 0x69, 0x32, 0x2F, 0x44, 0x73, 0x0A, 0x48, 0x7A, 0x63, 0x2F, 0x4E, + 0x31, 0x39, 0x44, 0x7A, 0x46, 0x47, 0x64, 0x74, 0x66, 0x43, 0x58, 0x77, 0x72, 0x65, 0x46, 0x61, 0x6D, 0x67, 0x4C, 0x52, 0x42, 0x37, 0x6C, 0x55, 0x65, 0x36, 0x54, 0x7A, 0x6B, 0x74, 0x75, 0x68, 0x73, 0x48, 0x53, 0x44, 0x43, 0x52, 0x5A, 0x4E, + 0x68, 0x71, 0x66, 0x4C, 0x4A, 0x47, 0x50, 0x34, 0x78, 0x6A, 0x62, 0x6C, 0x4A, 0x55, 0x4B, 0x37, 0x5A, 0x47, 0x71, 0x44, 0x70, 0x6E, 0x63, 0x6C, 0x6C, 0x50, 0x6A, 0x59, 0x59, 0x50, 0x47, 0x0A, 0x46, 0x72, 0x6F, 0x6A, 0x75, 0x74, 0x7A, 0x64, + 0x66, 0x68, 0x72, 0x47, 0x65, 0x30, 0x4B, 0x32, 0x32, 0x56, 0x6F, 0x46, 0x33, 0x4A, 0x70, 0x66, 0x31, 0x64, 0x2B, 0x34, 0x32, 0x6B, 0x64, 0x39, 0x32, 0x6A, 0x6A, 0x62, 0x72, 0x44, 0x6E, 0x56, 0x48, 0x6D, 0x74, 0x73, 0x4B, 0x68, 0x65, 0x4D, + 0x59, 0x63, 0x32, 0x78, 0x62, 0x58, 0x49, 0x42, 0x77, 0x38, 0x4D, 0x67, 0x41, 0x47, 0x4A, 0x6F, 0x46, 0x6A, 0x48, 0x56, 0x64, 0x71, 0x71, 0x47, 0x75, 0x77, 0x36, 0x71, 0x0A, 0x6E, 0x73, 0x62, 0x35, 0x38, 0x4E, 0x6E, 0x34, 0x44, 0x53, 0x45, + 0x43, 0x35, 0x4D, 0x55, 0x6F, 0x46, 0x6C, 0x6B, 0x52, 0x75, 0x64, 0x6C, 0x70, 0x63, 0x79, 0x71, 0x53, 0x65, 0x4C, 0x69, 0x53, 0x56, 0x35, 0x73, 0x49, 0x38, 0x6A, 0x72, 0x6C, 0x4C, 0x35, 0x57, 0x77, 0x57, 0x4C, 0x64, 0x72, 0x49, 0x42, 0x52, + 0x74, 0x46, 0x4F, 0x38, 0x4B, 0x76, 0x48, 0x37, 0x59, 0x56, 0x64, 0x69, 0x49, 0x32, 0x69, 0x2F, 0x36, 0x47, 0x61, 0x58, 0x37, 0x69, 0x2B, 0x42, 0x2F, 0x0A, 0x4F, 0x66, 0x56, 0x79, 0x4B, 0x34, 0x58, 0x45, 0x4C, 0x4B, 0x7A, 0x76, 0x47, 0x55, + 0x57, 0x53, 0x54, 0x4C, 0x4E, 0x68, 0x42, 0x39, 0x78, 0x4E, 0x48, 0x32, 0x37, 0x53, 0x67, 0x52, 0x4E, 0x63, 0x6D, 0x76, 0x4D, 0x53, 0x5A, 0x34, 0x50, 0x50, 0x6D, 0x7A, 0x2B, 0x4C, 0x6E, 0x35, 0x32, 0x6B, 0x75, 0x61, 0x69, 0x57, 0x41, 0x33, + 0x72, 0x46, 0x37, 0x69, 0x44, 0x65, 0x4D, 0x39, 0x6F, 0x76, 0x6E, 0x68, 0x70, 0x36, 0x64, 0x42, 0x37, 0x68, 0x37, 0x73, 0x78, 0x61, 0x0A, 0x4F, 0x67, 0x54, 0x64, 0x73, 0x78, 0x6F, 0x45, 0x71, 0x42, 0x52, 0x6A, 0x72, 0x4C, 0x64, 0x48, 0x45, + 0x6F, 0x4F, 0x61, 0x62, 0x50, 0x58, 0x6D, 0x36, 0x52, 0x55, 0x56, 0x6B, 0x52, 0x71, 0x45, 0x47, 0x51, 0x36, 0x55, 0x52, 0x4F, 0x63, 0x53, 0x6A, 0x69, 0x56, 0x62, 0x67, 0x47, 0x63, 0x5A, 0x33, 0x47, 0x4F, 0x54, 0x45, 0x41, 0x74, 0x6C, 0x4C, + 0x6F, 0x72, 0x36, 0x43, 0x5A, 0x70, 0x4F, 0x32, 0x6F, 0x59, 0x6F, 0x66, 0x61, 0x70, 0x68, 0x4E, 0x64, 0x67, 0x4F, 0x0A, 0x70, 0x79, 0x67, 0x61, 0x75, 0x31, 0x4C, 0x67, 0x65, 0x50, 0x68, 0x73, 0x75, 0x6D, 0x79, 0x77, 0x62, 0x72, 0x6D, 0x48, + 0x58, 0x75, 0x6D, 0x5A, 0x4E, 0x54, 0x66, 0x78, 0x50, 0x57, 0x51, 0x72, 0x71, 0x61, 0x41, 0x30, 0x6B, 0x38, 0x39, 0x6A, 0x4C, 0x39, 0x57, 0x42, 0x33, 0x36, 0x35, 0x6A, 0x4A, 0x36, 0x55, 0x65, 0x54, 0x6F, 0x33, 0x63, 0x4B, 0x58, 0x68, 0x5A, + 0x2B, 0x50, 0x6D, 0x68, 0x49, 0x49, 0x79, 0x6E, 0x4A, 0x6B, 0x42, 0x75, 0x67, 0x6E, 0x4C, 0x4E, 0x0A, 0x65, 0x4C, 0x4C, 0x49, 0x6A, 0x7A, 0x77, 0x65, 0x63, 0x2B, 0x66, 0x42, 0x48, 0x37, 0x2F, 0x50, 0x7A, 0x71, 0x55, 0x71, 0x6D, 0x39, 0x74, + 0x45, 0x5A, 0x44, 0x4B, 0x67, 0x75, 0x33, 0x39, 0x63, 0x4A, 0x52, 0x4E, 0x49, 0x74, 0x58, 0x2B, 0x53, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, + 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x31, 0x35, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x49, + 0x7A, 0x43, 0x43, 0x41, 0x61, 0x6D, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x46, 0x68, 0x58, 0x48, 0x77, 0x39, 0x68, 0x4A, 0x70, 0x37, 0x35, 0x70, 0x44, 0x49, 0x71, 0x49, 0x37, 0x66, 0x42, 0x77, 0x2B, 0x64, 0x32, 0x33, 0x50, + 0x6F, 0x63, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x77, 0x55, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, 0x4D, 0x43, 0x53, 0x6C, 0x41, 0x78, + 0x49, 0x7A, 0x41, 0x68, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x47, 0x6B, 0x4E, 0x35, 0x59, 0x6D, 0x56, 0x79, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x53, 0x6D, 0x46, 0x77, 0x59, 0x57, 0x34, 0x67, 0x51, 0x32, 0x38, 0x75, + 0x4C, 0x43, 0x42, 0x4D, 0x64, 0x47, 0x51, 0x75, 0x4D, 0x52, 0x30, 0x77, 0x47, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x52, 0x54, 0x5A, 0x57, 0x4E, 0x31, 0x0A, 0x63, 0x6D, 0x56, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, + 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x78, 0x4E, 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x44, 0x41, 0x30, 0x4D, 0x44, 0x67, 0x77, 0x4F, 0x44, 0x4D, 0x79, 0x4E, 0x54, 0x5A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4E, 0x54, 0x41, + 0x30, 0x4D, 0x44, 0x67, 0x77, 0x4F, 0x44, 0x4D, 0x79, 0x4E, 0x54, 0x5A, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x42, 0x67, 0x4E, 0x56, 0x0A, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x70, 0x51, 0x4D, 0x53, 0x4D, 0x77, 0x49, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x78, 0x70, 0x44, 0x65, 0x57, 0x4A, 0x6C, 0x63, 0x6E, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x49, 0x45, 0x70, 0x68, 0x63, 0x47, 0x46, 0x75, 0x49, 0x45, 0x4E, 0x76, 0x4C, 0x69, 0x77, 0x67, 0x54, 0x48, + 0x52, 0x6B, 0x4C, 0x6A, 0x45, 0x64, 0x4D, 0x42, 0x73, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x55, 0x55, 0x32, 0x56, 0x6A, 0x0A, 0x64, 0x58, 0x4A, 0x6C, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, + 0x45, 0x4E, 0x42, 0x4D, 0x54, 0x55, 0x77, 0x64, 0x6A, 0x41, 0x51, 0x42, 0x67, 0x63, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x49, 0x42, 0x42, 0x67, 0x55, 0x72, 0x67, 0x51, 0x51, 0x41, 0x49, 0x67, 0x4E, 0x69, 0x41, 0x41, 0x51, 0x4C, 0x55, + 0x48, 0x53, 0x4E, 0x5A, 0x44, 0x4B, 0x5A, 0x6D, 0x62, 0x50, 0x53, 0x59, 0x41, 0x69, 0x34, 0x49, 0x6F, 0x35, 0x47, 0x0A, 0x64, 0x43, 0x78, 0x34, 0x77, 0x43, 0x74, 0x45, 0x4C, 0x57, 0x31, 0x66, 0x48, 0x63, 0x6D, 0x75, 0x53, 0x31, 0x49, 0x67, + 0x67, 0x7A, 0x32, 0x34, 0x46, 0x47, 0x31, 0x54, 0x68, 0x32, 0x43, 0x65, 0x58, 0x32, 0x79, 0x46, 0x32, 0x77, 0x59, 0x55, 0x6C, 0x65, 0x44, 0x48, 0x4B, 0x50, 0x2B, 0x64, 0x58, 0x2B, 0x53, 0x71, 0x38, 0x62, 0x4F, 0x4C, 0x62, 0x65, 0x31, 0x50, + 0x4C, 0x30, 0x76, 0x4A, 0x53, 0x70, 0x53, 0x52, 0x5A, 0x48, 0x58, 0x2B, 0x41, 0x65, 0x7A, 0x42, 0x0A, 0x32, 0x4F, 0x74, 0x36, 0x6C, 0x48, 0x68, 0x57, 0x47, 0x45, 0x4E, 0x66, 0x61, 0x34, 0x48, 0x4C, 0x39, 0x72, 0x7A, 0x61, 0x74, 0x41, 0x79, + 0x32, 0x4B, 0x5A, 0x4D, 0x49, 0x61, 0x59, 0x2B, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x44, 0x67, 0x59, + 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x0A, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x42, 0x30, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x67, 0x51, 0x57, 0x42, 0x42, 0x54, 0x72, 0x51, 0x63, 0x69, 0x75, 0x2F, 0x4E, + 0x57, 0x65, 0x55, 0x55, 0x6A, 0x31, 0x76, 0x59, 0x76, 0x30, 0x68, 0x79, 0x43, 0x54, 0x51, 0x53, 0x76, 0x54, 0x39, 0x44, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x77, 0x4E, 0x6F, 0x41, 0x44, + 0x42, 0x6C, 0x41, 0x6A, 0x45, 0x41, 0x32, 0x53, 0x36, 0x4A, 0x0A, 0x66, 0x6C, 0x35, 0x4F, 0x70, 0x42, 0x45, 0x48, 0x76, 0x56, 0x6E, 0x43, 0x42, 0x39, 0x36, 0x72, 0x4D, 0x6A, 0x68, 0x54, 0x4B, 0x6B, 0x5A, 0x45, 0x42, 0x68, 0x64, 0x36, 0x7A, + 0x6C, 0x48, 0x70, 0x34, 0x50, 0x39, 0x6D, 0x4C, 0x51, 0x6C, 0x4F, 0x34, 0x45, 0x2F, 0x30, 0x42, 0x64, 0x47, 0x46, 0x39, 0x6A, 0x56, 0x67, 0x33, 0x50, 0x56, 0x79, 0x73, 0x30, 0x5A, 0x39, 0x41, 0x6A, 0x42, 0x45, 0x6D, 0x45, 0x59, 0x61, 0x67, + 0x6F, 0x55, 0x65, 0x59, 0x57, 0x6D, 0x4A, 0x0A, 0x53, 0x77, 0x64, 0x4C, 0x5A, 0x72, 0x57, 0x65, 0x71, 0x72, 0x71, 0x67, 0x48, 0x6B, 0x48, 0x5A, 0x41, 0x58, 0x51, 0x36, 0x62, 0x6B, 0x55, 0x36, 0x69, 0x59, 0x41, 0x5A, 0x65, 0x7A, 0x4B, 0x59, + 0x56, 0x57, 0x4F, 0x72, 0x36, 0x32, 0x4E, 0x75, 0x6B, 0x32, 0x32, 0x72, 0x47, 0x77, 0x6C, 0x67, 0x4D, 0x55, 0x34, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, + 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x2D, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x42, 0x52, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x20, 0x32, 0x30, 0x32, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x71, 0x54, 0x43, 0x43, 0x41, 0x35, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x63, 0x7A, 0x73, 0x77, 0x42, 0x45, 0x68, 0x62, 0x32, 0x55, 0x31, 0x34, + 0x4C, 0x6E, 0x4E, 0x4C, 0x79, 0x61, 0x48, 0x63, 0x5A, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x30, 0x46, 0x41, 0x44, 0x42, 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x45, 0x52, 0x54, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x43, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x48, 0x62, 0x57, 0x4A, + 0x49, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6C, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x4A, 0x53, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, + 0x67, 0x0A, 0x4D, 0x69, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x7A, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x7A, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x54, 0x41, 0x34, 0x4E, 0x54, 0x59, 0x7A, 0x4D, 0x56, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x34, 0x4D, 0x44, + 0x55, 0x77, 0x4F, 0x54, 0x41, 0x34, 0x4E, 0x54, 0x59, 0x7A, 0x4D, 0x46, 0x6F, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x46, 0x54, 0x41, 0x54, 0x0A, 0x42, + 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x45, 0x51, 0x74, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x52, 0x32, 0x31, 0x69, 0x53, 0x44, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x5A, 0x52, + 0x43, 0x31, 0x55, 0x55, 0x6C, 0x56, 0x54, 0x56, 0x43, 0x42, 0x43, 0x55, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x7A, 0x43, 0x43, 0x0A, 0x41, 0x69, 0x49, 0x77, + 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4B, 0x37, 0x2F, + 0x43, 0x56, 0x6D, 0x52, 0x67, 0x41, 0x70, 0x4B, 0x61, 0x4F, 0x59, 0x6B, 0x50, 0x37, 0x69, 0x6E, 0x35, 0x4D, 0x67, 0x36, 0x43, 0x6A, 0x6F, 0x57, 0x7A, 0x63, 0x6B, 0x6A, 0x59, 0x61, 0x43, 0x54, 0x0A, 0x63, 0x66, 0x4B, 0x72, 0x69, 0x33, 0x4F, + 0x50, 0x6F, 0x47, 0x64, 0x6C, 0x59, 0x4E, 0x4A, 0x55, 0x61, 0x32, 0x4E, 0x52, 0x62, 0x30, 0x6B, 0x7A, 0x34, 0x48, 0x49, 0x48, 0x45, 0x33, 0x30, 0x34, 0x7A, 0x51, 0x61, 0x53, 0x42, 0x79, 0x6C, 0x53, 0x61, 0x30, 0x35, 0x33, 0x62, 0x41, 0x54, + 0x54, 0x6C, 0x66, 0x72, 0x64, 0x54, 0x49, 0x7A, 0x5A, 0x58, 0x63, 0x46, 0x68, 0x66, 0x55, 0x76, 0x6E, 0x4B, 0x4C, 0x4E, 0x45, 0x67, 0x58, 0x74, 0x52, 0x72, 0x39, 0x30, 0x7A, 0x0A, 0x73, 0x57, 0x68, 0x38, 0x31, 0x6B, 0x35, 0x4D, 0x2F, 0x69, + 0x74, 0x6F, 0x75, 0x63, 0x70, 0x6D, 0x61, 0x63, 0x54, 0x73, 0x58, 0x6C, 0x64, 0x2F, 0x39, 0x77, 0x33, 0x48, 0x6E, 0x44, 0x59, 0x32, 0x35, 0x51, 0x64, 0x67, 0x72, 0x4D, 0x42, 0x4D, 0x36, 0x67, 0x68, 0x73, 0x37, 0x77, 0x5A, 0x38, 0x54, 0x31, + 0x73, 0x6F, 0x65, 0x67, 0x6A, 0x38, 0x6B, 0x31, 0x32, 0x62, 0x39, 0x70, 0x79, 0x30, 0x69, 0x34, 0x61, 0x36, 0x49, 0x62, 0x6E, 0x30, 0x38, 0x4F, 0x68, 0x5A, 0x0A, 0x57, 0x69, 0x69, 0x68, 0x4E, 0x49, 0x51, 0x61, 0x4A, 0x5A, 0x47, 0x32, 0x74, + 0x59, 0x2F, 0x76, 0x73, 0x76, 0x6D, 0x41, 0x2B, 0x76, 0x6B, 0x39, 0x50, 0x42, 0x46, 0x79, 0x32, 0x4F, 0x4D, 0x76, 0x68, 0x6E, 0x62, 0x46, 0x65, 0x53, 0x7A, 0x42, 0x71, 0x5A, 0x43, 0x54, 0x52, 0x70, 0x68, 0x6E, 0x79, 0x34, 0x4E, 0x71, 0x6F, + 0x46, 0x41, 0x6A, 0x70, 0x7A, 0x76, 0x32, 0x67, 0x54, 0x6E, 0x67, 0x37, 0x66, 0x43, 0x35, 0x76, 0x32, 0x58, 0x78, 0x32, 0x4D, 0x74, 0x36, 0x0A, 0x2B, 0x2B, 0x39, 0x7A, 0x41, 0x38, 0x34, 0x41, 0x39, 0x48, 0x33, 0x58, 0x34, 0x46, 0x30, 0x37, + 0x5A, 0x72, 0x6A, 0x63, 0x6A, 0x72, 0x71, 0x44, 0x79, 0x34, 0x64, 0x32, 0x41, 0x2F, 0x77, 0x6C, 0x32, 0x65, 0x63, 0x6A, 0x62, 0x77, 0x62, 0x39, 0x5A, 0x2F, 0x50, 0x67, 0x2F, 0x34, 0x53, 0x38, 0x52, 0x37, 0x2B, 0x31, 0x46, 0x68, 0x68, 0x47, + 0x61, 0x52, 0x54, 0x4D, 0x42, 0x66, 0x66, 0x62, 0x30, 0x30, 0x6D, 0x73, 0x61, 0x38, 0x79, 0x72, 0x35, 0x4C, 0x55, 0x4C, 0x0A, 0x51, 0x79, 0x52, 0x65, 0x53, 0x32, 0x74, 0x4E, 0x5A, 0x39, 0x2F, 0x57, 0x74, 0x54, 0x35, 0x50, 0x65, 0x42, 0x2B, + 0x55, 0x63, 0x53, 0x54, 0x71, 0x33, 0x6E, 0x44, 0x38, 0x38, 0x5A, 0x50, 0x2B, 0x6E, 0x70, 0x4E, 0x61, 0x35, 0x4A, 0x52, 0x61, 0x6C, 0x31, 0x51, 0x4D, 0x4E, 0x58, 0x74, 0x66, 0x62, 0x4F, 0x34, 0x41, 0x48, 0x79, 0x54, 0x73, 0x41, 0x37, 0x6F, + 0x43, 0x39, 0x58, 0x62, 0x30, 0x6E, 0x39, 0x53, 0x61, 0x37, 0x59, 0x55, 0x73, 0x4F, 0x43, 0x49, 0x76, 0x0A, 0x78, 0x39, 0x67, 0x76, 0x64, 0x68, 0x46, 0x50, 0x2F, 0x57, 0x78, 0x63, 0x36, 0x50, 0x57, 0x4F, 0x4A, 0x34, 0x64, 0x2F, 0x47, 0x55, + 0x6F, 0x68, 0x52, 0x35, 0x41, 0x64, 0x65, 0x59, 0x30, 0x63, 0x57, 0x2F, 0x6A, 0x50, 0x53, 0x6F, 0x58, 0x6B, 0x37, 0x62, 0x4E, 0x62, 0x6A, 0x62, 0x37, 0x45, 0x5A, 0x43, 0x68, 0x64, 0x51, 0x63, 0x52, 0x75, 0x72, 0x44, 0x68, 0x61, 0x54, 0x79, + 0x4E, 0x30, 0x64, 0x4B, 0x6B, 0x53, 0x77, 0x2F, 0x62, 0x53, 0x75, 0x52, 0x45, 0x56, 0x0A, 0x4D, 0x77, 0x65, 0x52, 0x32, 0x44, 0x73, 0x33, 0x4F, 0x6D, 0x4D, 0x77, 0x42, 0x74, 0x48, 0x46, 0x49, 0x6A, 0x59, 0x6F, 0x59, 0x69, 0x4D, 0x51, 0x34, + 0x45, 0x62, 0x4D, 0x6C, 0x36, 0x7A, 0x57, 0x4B, 0x31, 0x31, 0x6B, 0x4A, 0x4E, 0x58, 0x75, 0x48, 0x41, 0x37, 0x65, 0x2B, 0x77, 0x68, 0x61, 0x64, 0x53, 0x72, 0x32, 0x59, 0x32, 0x33, 0x4F, 0x43, 0x30, 0x4B, 0x2B, 0x30, 0x62, 0x70, 0x77, 0x48, + 0x4A, 0x77, 0x68, 0x35, 0x51, 0x38, 0x78, 0x61, 0x52, 0x66, 0x58, 0x0A, 0x2F, 0x41, 0x71, 0x30, 0x33, 0x75, 0x32, 0x41, 0x6E, 0x4D, 0x75, 0x53, 0x74, 0x49, 0x76, 0x31, 0x33, 0x6C, 0x6D, 0x69, 0x57, 0x41, 0x6D, 0x6C, 0x59, 0x30, 0x63, 0x4C, + 0x34, 0x55, 0x45, 0x79, 0x4E, 0x45, 0x48, 0x5A, 0x6D, 0x72, 0x48, 0x5A, 0x71, 0x4C, 0x41, 0x62, 0x57, 0x74, 0x34, 0x4E, 0x44, 0x66, 0x54, 0x69, 0x73, 0x6C, 0x30, 0x31, 0x67, 0x4C, 0x6D, 0x42, 0x31, 0x49, 0x52, 0x70, 0x6B, 0x51, 0x4C, 0x4C, + 0x64, 0x64, 0x43, 0x4E, 0x78, 0x62, 0x55, 0x39, 0x0A, 0x43, 0x5A, 0x45, 0x4A, 0x6A, 0x78, 0x53, 0x68, 0x46, 0x48, 0x52, 0x35, 0x50, 0x74, 0x62, 0x4A, 0x46, 0x52, 0x32, 0x6B, 0x57, 0x56, 0x6B, 0x69, 0x33, 0x50, 0x61, 0x4B, 0x52, 0x54, 0x30, + 0x38, 0x45, 0x74, 0x59, 0x2B, 0x58, 0x54, 0x49, 0x76, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x67, 0x59, 0x34, 0x77, 0x67, 0x59, 0x73, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, + 0x77, 0x41, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x5A, 0x35, 0x44, 0x77, 0x31, 0x74, 0x36, 0x31, 0x47, 0x4E, 0x56, 0x47, 0x4B, 0x58, 0x35, 0x63, 0x71, 0x2F, + 0x69, 0x65, 0x43, 0x4C, 0x78, 0x6B, 0x6C, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x45, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x64, 0x48, 0x77, + 0x52, 0x43, 0x0A, 0x4D, 0x45, 0x41, 0x77, 0x50, 0x71, 0x41, 0x38, 0x6F, 0x44, 0x71, 0x47, 0x4F, 0x47, 0x68, 0x30, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x6A, 0x63, 0x6D, 0x77, 0x75, 0x5A, 0x43, 0x31, 0x30, 0x63, 0x6E, 0x56, 0x7A, 0x64, + 0x43, 0x35, 0x75, 0x5A, 0x58, 0x51, 0x76, 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x32, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x66, 0x59, 0x6E, 0x4A, 0x66, 0x63, 0x6D, 0x39, 0x76, 0x64, 0x46, 0x39, 0x6A, 0x59, 0x56, 0x38, 0x79, 0x0A, + 0x58, 0x7A, 0x49, 0x77, 0x4D, 0x6A, 0x4D, 0x75, 0x59, 0x33, 0x4A, 0x73, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x44, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, 0x41, 0x30, + 0x39, 0x37, 0x4E, 0x33, 0x55, 0x39, 0x73, 0x77, 0x46, 0x72, 0x6B, 0x74, 0x70, 0x53, 0x48, 0x78, 0x51, 0x43, 0x46, 0x31, 0x36, 0x2B, 0x74, 0x49, 0x46, 0x6F, 0x45, 0x39, 0x63, 0x2B, 0x43, 0x65, 0x4A, 0x79, 0x72, 0x72, 0x0A, 0x64, 0x36, 0x6B, + 0x54, 0x70, 0x47, 0x6F, 0x4B, 0x57, 0x6C, 0x6F, 0x55, 0x4D, 0x7A, 0x31, 0x6F, 0x48, 0x34, 0x47, 0x75, 0x61, 0x66, 0x32, 0x4D, 0x6E, 0x32, 0x56, 0x73, 0x4E, 0x45, 0x4C, 0x5A, 0x4C, 0x64, 0x42, 0x2F, 0x65, 0x42, 0x61, 0x78, 0x4F, 0x71, 0x77, + 0x6A, 0x4D, 0x61, 0x31, 0x65, 0x66, 0x36, 0x37, 0x6E, 0x72, 0x69, 0x76, 0x36, 0x75, 0x76, 0x77, 0x38, 0x6C, 0x35, 0x56, 0x41, 0x6B, 0x31, 0x2F, 0x44, 0x4C, 0x51, 0x4F, 0x6A, 0x37, 0x61, 0x52, 0x76, 0x0A, 0x55, 0x39, 0x66, 0x36, 0x51, 0x41, + 0x34, 0x77, 0x39, 0x51, 0x41, 0x67, 0x4C, 0x41, 0x42, 0x4D, 0x6A, 0x44, 0x75, 0x30, 0x6F, 0x78, 0x2B, 0x32, 0x76, 0x35, 0x45, 0x79, 0x71, 0x36, 0x2B, 0x53, 0x6D, 0x4E, 0x4D, 0x57, 0x35, 0x74, 0x54, 0x52, 0x56, 0x46, 0x78, 0x44, 0x57, 0x79, + 0x36, 0x75, 0x37, 0x31, 0x63, 0x71, 0x71, 0x4C, 0x52, 0x76, 0x70, 0x4F, 0x38, 0x4E, 0x56, 0x68, 0x54, 0x61, 0x49, 0x61, 0x73, 0x67, 0x64, 0x70, 0x34, 0x44, 0x2F, 0x43, 0x61, 0x34, 0x0A, 0x6E, 0x6A, 0x38, 0x2B, 0x41, 0x79, 0x62, 0x6D, 0x54, + 0x4E, 0x75, 0x64, 0x58, 0x30, 0x4B, 0x45, 0x50, 0x55, 0x55, 0x44, 0x41, 0x78, 0x78, 0x5A, 0x69, 0x4D, 0x72, 0x63, 0x4C, 0x6D, 0x45, 0x6B, 0x57, 0x71, 0x54, 0x71, 0x4A, 0x77, 0x74, 0x7A, 0x45, 0x72, 0x35, 0x53, 0x73, 0x77, 0x72, 0x50, 0x4D, + 0x68, 0x66, 0x69, 0x48, 0x6F, 0x63, 0x61, 0x46, 0x70, 0x56, 0x49, 0x62, 0x56, 0x72, 0x67, 0x30, 0x4D, 0x38, 0x4A, 0x6B, 0x69, 0x5A, 0x6D, 0x6B, 0x64, 0x69, 0x6A, 0x0A, 0x59, 0x51, 0x36, 0x71, 0x67, 0x59, 0x46, 0x2F, 0x36, 0x46, 0x4B, 0x43, + 0x30, 0x55, 0x4C, 0x6E, 0x34, 0x42, 0x30, 0x59, 0x2B, 0x71, 0x53, 0x46, 0x4E, 0x75, 0x65, 0x47, 0x34, 0x41, 0x33, 0x72, 0x76, 0x4E, 0x54, 0x4A, 0x31, 0x6A, 0x78, 0x44, 0x38, 0x56, 0x31, 0x4A, 0x62, 0x6E, 0x36, 0x42, 0x6D, 0x32, 0x6D, 0x31, + 0x69, 0x57, 0x4B, 0x50, 0x69, 0x46, 0x4C, 0x59, 0x31, 0x2F, 0x34, 0x6E, 0x77, 0x53, 0x50, 0x46, 0x79, 0x79, 0x73, 0x43, 0x75, 0x37, 0x46, 0x66, 0x0A, 0x2F, 0x76, 0x74, 0x44, 0x68, 0x51, 0x4E, 0x47, 0x76, 0x6C, 0x33, 0x47, 0x79, 0x69, 0x45, + 0x6D, 0x2F, 0x39, 0x63, 0x43, 0x6E, 0x6E, 0x52, 0x4B, 0x33, 0x50, 0x67, 0x54, 0x46, 0x62, 0x47, 0x42, 0x56, 0x7A, 0x62, 0x4C, 0x5A, 0x56, 0x7A, 0x52, 0x48, 0x54, 0x46, 0x33, 0x36, 0x53, 0x58, 0x44, 0x77, 0x37, 0x49, 0x79, 0x4E, 0x39, 0x58, + 0x78, 0x6D, 0x41, 0x6E, 0x6B, 0x62, 0x57, 0x4F, 0x41, 0x43, 0x4B, 0x73, 0x47, 0x6B, 0x6F, 0x48, 0x55, 0x36, 0x58, 0x43, 0x50, 0x0A, 0x70, 0x7A, 0x2B, 0x79, 0x37, 0x59, 0x61, 0x4D, 0x67, 0x6D, 0x6F, 0x31, 0x79, 0x45, 0x4A, 0x61, 0x67, 0x74, + 0x46, 0x53, 0x47, 0x6B, 0x55, 0x50, 0x46, 0x61, 0x55, 0x41, 0x38, 0x4A, 0x52, 0x37, 0x5A, 0x53, 0x64, 0x58, 0x4F, 0x55, 0x50, 0x50, 0x66, 0x48, 0x2F, 0x6D, 0x76, 0x54, 0x57, 0x7A, 0x65, 0x2F, 0x45, 0x5A, 0x54, 0x4E, 0x34, 0x36, 0x6C, 0x73, + 0x2F, 0x70, 0x64, 0x75, 0x34, 0x44, 0x35, 0x38, 0x4A, 0x44, 0x55, 0x6A, 0x78, 0x71, 0x67, 0x65, 0x6A, 0x42, 0x0A, 0x57, 0x6F, 0x43, 0x39, 0x45, 0x56, 0x32, 0x54, 0x61, 0x2F, 0x76, 0x48, 0x35, 0x6D, 0x51, 0x2F, 0x75, 0x32, 0x6B, 0x63, 0x36, + 0x64, 0x30, 0x6C, 0x69, 0x36, 0x39, 0x30, 0x79, 0x56, 0x52, 0x41, 0x79, 0x73, 0x75, 0x54, 0x45, 0x77, 0x72, 0x74, 0x2B, 0x32, 0x61, 0x53, 0x45, 0x63, 0x72, 0x31, 0x77, 0x50, 0x72, 0x59, 0x67, 0x31, 0x55, 0x44, 0x66, 0x4E, 0x50, 0x46, 0x49, + 0x6B, 0x5A, 0x31, 0x63, 0x47, 0x74, 0x35, 0x53, 0x41, 0x59, 0x71, 0x67, 0x70, 0x71, 0x2F, 0x0A, 0x35, 0x75, 0x73, 0x57, 0x44, 0x69, 0x4A, 0x46, 0x41, 0x62, 0x7A, 0x64, 0x4E, 0x70, 0x51, 0x30, 0x71, 0x54, 0x55, 0x6D, 0x69, 0x74, 0x65, 0x58, + 0x75, 0x65, 0x34, 0x49, 0x63, 0x72, 0x38, 0x30, 0x6B, 0x6E, 0x43, 0x44, 0x67, 0x4B, 0x73, 0x34, 0x71, 0x6C, 0x6C, 0x6F, 0x33, 0x55, 0x43, 0x6B, 0x47, 0x4A, 0x43, 0x79, 0x38, 0x39, 0x55, 0x44, 0x79, 0x69, 0x62, 0x4B, 0x37, 0x39, 0x58, 0x48, + 0x34, 0x49, 0x39, 0x54, 0x6A, 0x76, 0x41, 0x41, 0x34, 0x36, 0x6A, 0x74, 0x0A, 0x6E, 0x2F, 0x6D, 0x74, 0x64, 0x2B, 0x41, 0x72, 0x59, 0x30, 0x2B, 0x65, 0x77, 0x2B, 0x34, 0x33, 0x75, 0x33, 0x67, 0x4A, 0x68, 0x4A, 0x36, 0x35, 0x62, 0x76, 0x73, + 0x70, 0x6D, 0x5A, 0x44, 0x6F, 0x67, 0x4E, 0x4F, 0x66, 0x4A, 0x41, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, + 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x41, 0x73, 0x69, 0x61, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x45, 0x43, 0x43, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4D, 0x54, 0x43, 0x43, 0x41, 0x62, 0x65, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x4E, 0x6E, 0x54, 0x68, 0x54, 0x58, 0x78, 0x6C, 0x45, 0x38, 0x6D, 0x73, 0x67, 0x31, 0x55, 0x6C, 0x6F, 0x44, 0x35, + 0x53, 0x66, 0x69, 0x39, 0x51, 0x61, 0x4D, 0x63, 0x77, 0x43, 0x67, 0x59, 0x49, 0x4B, 0x6F, 0x5A, 0x49, 0x7A, 0x6A, 0x30, 0x45, 0x41, 0x77, 0x4D, 0x77, 0x57, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x0A, 0x42, 0x68, + 0x4D, 0x43, 0x51, 0x30, 0x34, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, + 0x39, 0x73, 0x62, 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x49, 0x6A, 0x41, 0x67, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x47, 0x56, 0x52, 0x79, 0x0A, 0x64, 0x58, 0x4E, 0x30, 0x51, + 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x55, 0x54, 0x46, 0x4D, 0x67, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x51, 0x77, 0x4E, 0x54, 0x45, 0x31, 0x4D, + 0x44, 0x55, 0x30, 0x4D, 0x54, 0x55, 0x32, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x51, 0x77, 0x4E, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x55, 0x30, 0x4D, 0x54, 0x55, 0x31, 0x57, 0x6A, 0x42, 0x59, 0x0A, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x63, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x42, 0x63, 0x32, 0x6C, 0x68, 0x49, 0x46, 0x52, 0x6C, + 0x59, 0x32, 0x68, 0x75, 0x62, 0x32, 0x78, 0x76, 0x5A, 0x32, 0x6C, 0x6C, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x5A, 0x56, 0x48, 0x4A, + 0x31, 0x63, 0x33, 0x52, 0x42, 0x63, 0x32, 0x6C, 0x68, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x46, 0x51, 0x30, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, + 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, 0x42, 0x4C, 0x68, 0x2F, 0x0A, 0x70, 0x56, 0x73, 0x2F, 0x41, 0x54, 0x35, 0x39, 0x38, 0x49, 0x68, 0x74, 0x72, 0x69, + 0x6D, 0x59, 0x34, 0x5A, 0x74, 0x63, 0x55, 0x35, 0x6E, 0x62, 0x39, 0x77, 0x6A, 0x2F, 0x31, 0x57, 0x72, 0x67, 0x6A, 0x73, 0x74, 0x45, 0x70, 0x76, 0x44, 0x42, 0x6A, 0x4C, 0x31, 0x50, 0x31, 0x4D, 0x37, 0x55, 0x69, 0x46, 0x50, 0x6F, 0x58, 0x6C, + 0x66, 0x58, 0x54, 0x72, 0x34, 0x73, 0x50, 0x2F, 0x4D, 0x53, 0x70, 0x77, 0x44, 0x70, 0x67, 0x75, 0x4D, 0x71, 0x57, 0x7A, 0x4A, 0x38, 0x0A, 0x53, 0x35, 0x73, 0x55, 0x4B, 0x5A, 0x37, 0x34, 0x4C, 0x59, 0x4F, 0x31, 0x36, 0x34, 0x34, 0x78, 0x53, + 0x54, 0x30, 0x6D, 0x59, 0x65, 0x6B, 0x64, 0x63, 0x6F, 0x75, 0x4A, 0x74, 0x67, 0x71, 0x37, 0x6E, 0x44, 0x4D, 0x31, 0x44, 0x39, 0x72, 0x73, 0x33, 0x71, 0x6C, 0x4B, 0x48, 0x38, 0x6B, 0x7A, 0x73, 0x61, 0x4E, 0x43, 0x4D, 0x45, 0x41, 0x77, 0x44, + 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x4C, 0x49, 0x56, 0x54, + 0x75, 0x37, 0x46, 0x44, 0x7A, 0x54, 0x4C, 0x71, 0x6E, 0x71, 0x4F, 0x48, 0x2F, 0x71, 0x4B, 0x59, 0x71, 0x4B, 0x61, 0x54, 0x36, 0x52, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, + 0x41, 0x67, 0x45, 0x47, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x0A, 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x67, 0x41, 0x4D, 0x47, 0x55, 0x43, 0x4D, 0x46, 0x52, 0x48, 0x31, 0x38, 0x4D, 0x74, 0x59, 0x59, 0x5A, + 0x49, 0x39, 0x48, 0x6C, 0x61, 0x56, 0x51, 0x30, 0x31, 0x4C, 0x31, 0x38, 0x4E, 0x39, 0x6D, 0x64, 0x73, 0x64, 0x30, 0x41, 0x61, 0x52, 0x75, 0x66, 0x34, 0x61, 0x46, 0x74, 0x4F, 0x4A, 0x78, 0x32, 0x34, 0x6D, 0x48, 0x31, 0x2F, 0x6B, 0x37, 0x38, + 0x49, 0x54, 0x63, 0x54, 0x61, 0x52, 0x54, 0x43, 0x68, 0x44, 0x31, 0x35, 0x4B, 0x0A, 0x65, 0x41, 0x49, 0x78, 0x41, 0x4B, 0x4F, 0x52, 0x68, 0x2F, 0x49, 0x52, 0x4D, 0x34, 0x50, 0x44, 0x77, 0x59, 0x71, 0x52, 0x4F, 0x6B, 0x77, 0x72, 0x55, 0x4C, + 0x47, 0x39, 0x49, 0x70, 0x52, 0x64, 0x4E, 0x59, 0x6C, 0x7A, 0x67, 0x38, 0x57, 0x62, 0x47, 0x66, 0x36, 0x30, 0x6F, 0x65, 0x6E, 0x55, 0x6F, 0x57, 0x61, 0x32, 0x41, 0x61, 0x55, 0x32, 0x2B, 0x64, 0x68, 0x6F, 0x59, 0x53, 0x69, 0x33, 0x64, 0x4F, + 0x47, 0x69, 0x4D, 0x51, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x54, 0x72, 0x75, 0x73, 0x74, 0x41, + 0x73, 0x69, 0x61, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x52, 0x53, 0x41, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x67, 0x44, + 0x43, 0x43, 0x41, 0x32, 0x69, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x55, 0x48, 0x42, 0x6A, 0x59, 0x7A, 0x2B, 0x56, 0x54, 0x50, 0x79, 0x49, 0x31, 0x52, 0x6C, 0x4E, 0x55, 0x4A, 0x44, 0x78, 0x73, 0x52, 0x39, 0x46, 0x63, 0x53, 0x70, + 0x77, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4D, 0x42, 0x51, 0x41, 0x77, 0x57, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, + 0x30, 0x34, 0x78, 0x4A, 0x54, 0x41, 0x6A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x48, 0x46, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x58, 0x4E, 0x70, 0x59, 0x53, 0x42, 0x55, 0x5A, 0x57, 0x4E, 0x6F, 0x62, 0x6D, 0x39, 0x73, 0x62, + 0x32, 0x64, 0x70, 0x5A, 0x58, 0x4D, 0x73, 0x49, 0x45, 0x6C, 0x75, 0x59, 0x79, 0x34, 0x78, 0x49, 0x6A, 0x41, 0x67, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x0A, 0x47, 0x56, 0x52, 0x79, 0x64, 0x58, 0x4E, 0x30, 0x51, 0x58, 0x4E, 0x70, + 0x59, 0x53, 0x42, 0x55, 0x54, 0x46, 0x4D, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x77, 0x48, 0x68, 0x63, 0x4E, 0x4D, 0x6A, 0x51, 0x77, 0x4E, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x55, 0x30, + 0x4D, 0x54, 0x55, 0x33, 0x57, 0x68, 0x63, 0x4E, 0x4E, 0x44, 0x51, 0x77, 0x4E, 0x54, 0x45, 0x31, 0x4D, 0x44, 0x55, 0x30, 0x4D, 0x54, 0x55, 0x32, 0x0A, 0x57, 0x6A, 0x42, 0x59, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x47, 0x45, 0x77, 0x4A, 0x44, 0x54, 0x6A, 0x45, 0x6C, 0x4D, 0x43, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x63, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x42, 0x63, 0x32, 0x6C, 0x68, 0x49, 0x46, 0x52, 0x6C, 0x59, 0x32, 0x68, + 0x75, 0x62, 0x32, 0x78, 0x76, 0x5A, 0x32, 0x6C, 0x6C, 0x63, 0x79, 0x77, 0x67, 0x53, 0x57, 0x35, 0x6A, 0x4C, 0x6A, 0x45, 0x69, 0x0A, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x5A, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, + 0x52, 0x42, 0x63, 0x32, 0x6C, 0x68, 0x49, 0x46, 0x52, 0x4D, 0x55, 0x79, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, + 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x0A, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4D, 0x4D, 0x57, 0x75, 0x42, 0x74, 0x71, 0x70, + 0x45, 0x52, 0x7A, 0x35, 0x64, 0x5A, 0x4F, 0x39, 0x4C, 0x6E, 0x50, 0x57, 0x77, 0x76, 0x42, 0x30, 0x5A, 0x71, 0x42, 0x39, 0x57, 0x4F, 0x77, 0x6A, 0x30, 0x50, 0x42, 0x75, 0x77, 0x68, 0x61, 0x47, 0x6E, 0x72, 0x68, 0x42, 0x33, 0x59, 0x6D, 0x48, + 0x34, 0x39, 0x70, 0x56, 0x72, 0x37, 0x2B, 0x4E, 0x6D, 0x44, 0x51, 0x44, 0x49, 0x50, 0x4E, 0x0A, 0x6C, 0x4F, 0x72, 0x6E, 0x78, 0x53, 0x31, 0x63, 0x4C, 0x77, 0x55, 0x57, 0x41, 0x70, 0x34, 0x4B, 0x71, 0x43, 0x2F, 0x6C, 0x59, 0x43, 0x5A, 0x55, + 0x6C, 0x76, 0x69, 0x59, 0x51, 0x42, 0x32, 0x73, 0x72, 0x70, 0x31, 0x30, 0x5A, 0x79, 0x39, 0x55, 0x2B, 0x35, 0x52, 0x6A, 0x6D, 0x4F, 0x4D, 0x6D, 0x53, 0x6F, 0x50, 0x47, 0x6C, 0x62, 0x59, 0x4A, 0x51, 0x31, 0x44, 0x4E, 0x44, 0x58, 0x33, 0x65, + 0x52, 0x41, 0x35, 0x67, 0x45, 0x6B, 0x39, 0x62, 0x4E, 0x62, 0x32, 0x2F, 0x0A, 0x6D, 0x54, 0x68, 0x74, 0x66, 0x57, 0x7A, 0x61, 0x34, 0x6D, 0x68, 0x7A, 0x48, 0x2F, 0x6B, 0x78, 0x70, 0x52, 0x6B, 0x51, 0x63, 0x77, 0x55, 0x71, 0x77, 0x7A, 0x49, + 0x5A, 0x68, 0x65, 0x6F, 0x30, 0x71, 0x74, 0x31, 0x43, 0x48, 0x6A, 0x43, 0x4E, 0x50, 0x35, 0x36, 0x31, 0x48, 0x6D, 0x48, 0x56, 0x62, 0x37, 0x30, 0x41, 0x63, 0x6E, 0x4B, 0x74, 0x45, 0x6A, 0x2B, 0x71, 0x70, 0x6B, 0x6C, 0x7A, 0x38, 0x6F, 0x59, + 0x56, 0x6C, 0x51, 0x77, 0x51, 0x58, 0x31, 0x46, 0x6B, 0x0A, 0x7A, 0x76, 0x39, 0x33, 0x75, 0x4D, 0x6C, 0x74, 0x72, 0x4F, 0x58, 0x56, 0x6D, 0x50, 0x47, 0x5A, 0x4C, 0x6D, 0x7A, 0x6A, 0x79, 0x55, 0x54, 0x35, 0x74, 0x55, 0x4D, 0x6E, 0x43, 0x45, + 0x33, 0x32, 0x66, 0x74, 0x35, 0x45, 0x65, 0x62, 0x75, 0x79, 0x6A, 0x42, 0x7A, 0x61, 0x30, 0x30, 0x74, 0x73, 0x4C, 0x74, 0x62, 0x44, 0x65, 0x4C, 0x64, 0x4D, 0x31, 0x61, 0x54, 0x6B, 0x32, 0x74, 0x79, 0x4B, 0x6A, 0x67, 0x37, 0x2F, 0x44, 0x38, + 0x4F, 0x6D, 0x59, 0x43, 0x59, 0x6F, 0x0A, 0x7A, 0x7A, 0x61, 0x2F, 0x2B, 0x6C, 0x63, 0x4B, 0x37, 0x46, 0x73, 0x2F, 0x36, 0x54, 0x41, 0x57, 0x65, 0x38, 0x54, 0x62, 0x78, 0x4E, 0x52, 0x6B, 0x6F, 0x44, 0x44, 0x37, 0x35, 0x66, 0x30, 0x64, 0x63, + 0x5A, 0x4C, 0x64, 0x4B, 0x59, 0x39, 0x42, 0x57, 0x4E, 0x34, 0x41, 0x72, 0x54, 0x72, 0x39, 0x50, 0x58, 0x77, 0x61, 0x71, 0x4C, 0x45, 0x58, 0x38, 0x45, 0x34, 0x30, 0x65, 0x46, 0x67, 0x6C, 0x31, 0x6F, 0x55, 0x68, 0x36, 0x33, 0x6B, 0x64, 0x30, + 0x4E, 0x79, 0x72, 0x0A, 0x7A, 0x32, 0x49, 0x38, 0x73, 0x4D, 0x65, 0x58, 0x69, 0x39, 0x62, 0x51, 0x6E, 0x39, 0x50, 0x2B, 0x50, 0x4E, 0x37, 0x46, 0x34, 0x2F, 0x77, 0x36, 0x67, 0x33, 0x43, 0x45, 0x49, 0x52, 0x30, 0x4A, 0x77, 0x71, 0x48, 0x38, + 0x75, 0x79, 0x67, 0x68, 0x5A, 0x56, 0x4E, 0x67, 0x65, 0x70, 0x42, 0x74, 0x6C, 0x6A, 0x68, 0x62, 0x2F, 0x2F, 0x48, 0x58, 0x65, 0x6C, 0x74, 0x74, 0x30, 0x38, 0x6C, 0x77, 0x53, 0x55, 0x71, 0x36, 0x48, 0x54, 0x72, 0x51, 0x55, 0x4E, 0x6F, 0x79, + 0x0A, 0x49, 0x42, 0x6E, 0x6B, 0x69, 0x7A, 0x2F, 0x72, 0x31, 0x52, 0x59, 0x6D, 0x4E, 0x7A, 0x7A, 0x37, 0x64, 0x5A, 0x36, 0x77, 0x42, 0x33, 0x43, 0x34, 0x46, 0x47, 0x42, 0x33, 0x33, 0x50, 0x59, 0x50, 0x58, 0x46, 0x49, 0x4B, 0x76, 0x46, 0x31, + 0x74, 0x6A, 0x56, 0x45, 0x4B, 0x32, 0x73, 0x55, 0x59, 0x79, 0x4A, 0x74, 0x74, 0x33, 0x4C, 0x43, 0x44, 0x73, 0x33, 0x2B, 0x6A, 0x54, 0x6E, 0x68, 0x4D, 0x6D, 0x43, 0x57, 0x72, 0x38, 0x6E, 0x34, 0x75, 0x49, 0x46, 0x36, 0x43, 0x0A, 0x46, 0x61, + 0x62, 0x57, 0x32, 0x49, 0x2B, 0x73, 0x35, 0x63, 0x30, 0x79, 0x68, 0x73, 0x6A, 0x35, 0x35, 0x4E, 0x71, 0x4A, 0x34, 0x6A, 0x73, 0x2B, 0x6B, 0x38, 0x55, 0x54, 0x61, 0x76, 0x2F, 0x48, 0x39, 0x78, 0x6A, 0x38, 0x5A, 0x37, 0x58, 0x76, 0x47, 0x43, + 0x78, 0x55, 0x71, 0x30, 0x44, 0x54, 0x62, 0x45, 0x33, 0x74, 0x78, 0x63, 0x69, 0x33, 0x4F, 0x45, 0x39, 0x6B, 0x78, 0x4A, 0x52, 0x4D, 0x54, 0x36, 0x44, 0x4E, 0x72, 0x71, 0x58, 0x47, 0x4A, 0x79, 0x56, 0x31, 0x0A, 0x4A, 0x32, 0x33, 0x47, 0x32, + 0x70, 0x79, 0x4F, 0x73, 0x41, 0x57, 0x5A, 0x31, 0x53, 0x67, 0x52, 0x78, 0x53, 0x48, 0x55, 0x75, 0x50, 0x7A, 0x48, 0x6C, 0x71, 0x74, 0x4B, 0x5A, 0x46, 0x6C, 0x68, 0x61, 0x78, 0x50, 0x38, 0x53, 0x38, 0x79, 0x53, 0x70, 0x67, 0x2B, 0x6B, 0x55, + 0x62, 0x38, 0x4F, 0x57, 0x4A, 0x44, 0x5A, 0x67, 0x6F, 0x4D, 0x35, 0x70, 0x6C, 0x2B, 0x7A, 0x2B, 0x6D, 0x36, 0x53, 0x73, 0x38, 0x30, 0x7A, 0x44, 0x6F, 0x57, 0x6F, 0x38, 0x53, 0x6E, 0x54, 0x0A, 0x71, 0x31, 0x6D, 0x74, 0x31, 0x74, 0x76, 0x65, + 0x31, 0x43, 0x75, 0x42, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x51, 0x6A, 0x42, 0x41, 0x4D, 0x41, 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, + 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, 0x4C, 0x67, 0x48, 0x6B, 0x58, 0x6C, 0x63, 0x42, 0x76, 0x52, 0x47, 0x2F, 0x58, 0x74, 0x5A, 0x0A, 0x79, 0x6C, 0x6F, 0x6D, 0x6B, 0x61, 0x64, 0x46, 0x4B, 0x2F, 0x68, + 0x54, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, + 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x49, 0x5A, 0x74, 0x71, 0x42, 0x53, 0x42, 0x64, 0x47, 0x42, 0x61, 0x6E, 0x45, 0x71, 0x54, 0x33, 0x0A, 0x52, 0x7A, 0x2F, 0x4E, 0x79, 0x6A, 0x75, 0x75, 0x6A, 0x73, 0x43, 0x43, 0x7A, 0x74, + 0x78, 0x49, 0x4A, 0x58, 0x67, 0x58, 0x62, 0x4F, 0x44, 0x67, 0x63, 0x4D, 0x54, 0x57, 0x6C, 0x74, 0x6E, 0x5A, 0x39, 0x72, 0x39, 0x36, 0x6E, 0x42, 0x4F, 0x37, 0x55, 0x35, 0x57, 0x53, 0x2F, 0x38, 0x2B, 0x53, 0x34, 0x50, 0x50, 0x46, 0x4A, 0x7A, + 0x56, 0x58, 0x71, 0x44, 0x75, 0x69, 0x47, 0x65, 0x76, 0x34, 0x69, 0x71, 0x4D, 0x45, 0x33, 0x6D, 0x6D, 0x4C, 0x35, 0x44, 0x77, 0x38, 0x0A, 0x76, 0x65, 0x57, 0x76, 0x30, 0x42, 0x49, 0x62, 0x35, 0x59, 0x6C, 0x72, 0x63, 0x35, 0x74, 0x76, 0x4A, + 0x51, 0x4A, 0x4C, 0x6B, 0x49, 0x4B, 0x76, 0x51, 0x4D, 0x4B, 0x74, 0x75, 0x70, 0x70, 0x67, 0x4A, 0x46, 0x71, 0x42, 0x54, 0x51, 0x55, 0x59, 0x6F, 0x2B, 0x49, 0x7A, 0x65, 0x58, 0x6F, 0x4C, 0x48, 0x35, 0x50, 0x74, 0x37, 0x44, 0x6C, 0x4B, 0x39, + 0x52, 0x4D, 0x45, 0x37, 0x49, 0x31, 0x30, 0x6E, 0x59, 0x45, 0x4B, 0x71, 0x47, 0x2F, 0x6F, 0x64, 0x76, 0x36, 0x4C, 0x0A, 0x54, 0x79, 0x74, 0x70, 0x45, 0x6F, 0x59, 0x4B, 0x4E, 0x44, 0x62, 0x64, 0x67, 0x70, 0x74, 0x76, 0x54, 0x2B, 0x42, 0x7A, + 0x33, 0x55, 0x6C, 0x2F, 0x4B, 0x44, 0x37, 0x4A, 0x4F, 0x36, 0x4E, 0x58, 0x42, 0x4E, 0x69, 0x54, 0x32, 0x54, 0x77, 0x70, 0x32, 0x78, 0x49, 0x51, 0x61, 0x4F, 0x48, 0x45, 0x69, 0x62, 0x67, 0x47, 0x49, 0x4F, 0x63, 0x62, 0x65, 0x72, 0x79, 0x78, + 0x6B, 0x32, 0x47, 0x61, 0x47, 0x55, 0x41, 0x52, 0x74, 0x57, 0x71, 0x46, 0x56, 0x77, 0x48, 0x78, 0x0A, 0x74, 0x6C, 0x6F, 0x74, 0x4A, 0x6E, 0x4D, 0x6E, 0x6C, 0x76, 0x6D, 0x35, 0x50, 0x31, 0x76, 0x51, 0x69, 0x4A, 0x33, 0x6B, 0x6F, 0x50, 0x32, + 0x36, 0x54, 0x70, 0x55, 0x4A, 0x67, 0x33, 0x39, 0x33, 0x33, 0x46, 0x45, 0x46, 0x6C, 0x4A, 0x30, 0x67, 0x63, 0x58, 0x61, 0x78, 0x37, 0x50, 0x71, 0x4A, 0x74, 0x5A, 0x77, 0x75, 0x68, 0x66, 0x47, 0x35, 0x57, 0x79, 0x52, 0x61, 0x73, 0x51, 0x6D, + 0x72, 0x32, 0x73, 0x6F, 0x61, 0x42, 0x38, 0x32, 0x47, 0x33, 0x39, 0x74, 0x70, 0x0A, 0x32, 0x37, 0x52, 0x49, 0x47, 0x41, 0x41, 0x74, 0x76, 0x4B, 0x4C, 0x45, 0x69, 0x55, 0x55, 0x6A, 0x70, 0x51, 0x37, 0x68, 0x52, 0x47, 0x55, 0x2B, 0x69, 0x73, + 0x46, 0x71, 0x4D, 0x42, 0x33, 0x69, 0x59, 0x50, 0x67, 0x36, 0x71, 0x6F, 0x63, 0x4A, 0x51, 0x72, 0x6D, 0x42, 0x6B, 0x74, 0x77, 0x6C, 0x69, 0x4A, 0x69, 0x4A, 0x38, 0x58, 0x77, 0x31, 0x38, 0x57, 0x4C, 0x4B, 0x37, 0x6E, 0x6E, 0x34, 0x47, 0x53, + 0x2F, 0x2B, 0x58, 0x2F, 0x6A, 0x62, 0x68, 0x38, 0x37, 0x71, 0x0A, 0x71, 0x41, 0x38, 0x4D, 0x70, 0x75, 0x67, 0x4C, 0x6F, 0x44, 0x7A, 0x67, 0x61, 0x35, 0x53, 0x59, 0x6E, 0x48, 0x2B, 0x74, 0x42, 0x75, 0x59, 0x63, 0x36, 0x6B, 0x49, 0x51, 0x58, + 0x2B, 0x49, 0x6D, 0x46, 0x54, 0x77, 0x33, 0x4F, 0x66, 0x66, 0x58, 0x76, 0x4F, 0x36, 0x34, 0x35, 0x65, 0x38, 0x44, 0x37, 0x72, 0x30, 0x69, 0x2B, 0x79, 0x69, 0x47, 0x4E, 0x46, 0x6A, 0x45, 0x57, 0x6E, 0x39, 0x68, 0x6F, 0x6E, 0x67, 0x50, 0x58, + 0x76, 0x50, 0x4B, 0x6E, 0x62, 0x77, 0x62, 0x0A, 0x50, 0x4B, 0x66, 0x49, 0x4C, 0x66, 0x61, 0x6E, 0x49, 0x68, 0x48, 0x4B, 0x41, 0x39, 0x6A, 0x6E, 0x5A, 0x77, 0x71, 0x4B, 0x44, 0x73, 0x73, 0x31, 0x6A, 0x6A, 0x51, 0x35, 0x32, 0x4D, 0x6A, 0x71, + 0x6A, 0x5A, 0x39, 0x6B, 0x34, 0x44, 0x65, 0x77, 0x62, 0x4E, 0x66, 0x46, 0x6A, 0x38, 0x47, 0x51, 0x59, 0x53, 0x62, 0x62, 0x4A, 0x49, 0x77, 0x65, 0x53, 0x73, 0x43, 0x49, 0x33, 0x7A, 0x57, 0x51, 0x7A, 0x6A, 0x38, 0x43, 0x39, 0x47, 0x52, 0x68, + 0x33, 0x73, 0x66, 0x49, 0x0A, 0x42, 0x35, 0x58, 0x65, 0x4D, 0x68, 0x67, 0x36, 0x6A, 0x36, 0x4A, 0x43, 0x51, 0x43, 0x54, 0x6C, 0x31, 0x6A, 0x4E, 0x64, 0x66, 0x4B, 0x37, 0x76, 0x73, 0x55, 0x31, 0x50, 0x31, 0x46, 0x65, 0x51, 0x4E, 0x57, 0x72, + 0x63, 0x72, 0x67, 0x53, 0x58, 0x53, 0x59, 0x6B, 0x30, 0x6C, 0x79, 0x34, 0x77, 0x42, 0x4F, 0x65, 0x59, 0x39, 0x39, 0x73, 0x4C, 0x41, 0x5A, 0x44, 0x42, 0x48, 0x77, 0x6F, 0x2F, 0x2B, 0x4D, 0x4C, 0x2B, 0x54, 0x76, 0x72, 0x62, 0x6D, 0x6E, 0x4E, + 0x7A, 0x0A, 0x46, 0x72, 0x77, 0x46, 0x75, 0x48, 0x6E, 0x59, 0x57, 0x61, 0x38, 0x47, 0x35, 0x7A, 0x39, 0x6E, 0x4F, 0x44, 0x6D, 0x78, 0x66, 0x4B, 0x75, 0x55, 0x34, 0x43, 0x6B, 0x55, 0x70, 0x69, 0x6A, 0x79, 0x33, 0x32, 0x33, 0x69, 0x6D, 0x74, + 0x74, 0x55, 0x51, 0x2F, 0x68, 0x48, 0x57, 0x4B, 0x4E, 0x64, 0x64, 0x42, 0x57, 0x63, 0x77, 0x61, 0x75, 0x77, 0x78, 0x7A, 0x51, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, + 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x44, 0x2D, 0x54, 0x52, 0x55, 0x53, 0x54, 0x20, 0x45, 0x56, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x20, 0x32, 0x30, 0x32, 0x33, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x71, 0x54, 0x43, 0x43, 0x41, 0x35, 0x47, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x61, 0x53, 0x59, 0x4A, 0x66, 0x6F, 0x42, 0x4C, 0x54, 0x4B, + 0x43, 0x6E, 0x6A, 0x48, 0x68, 0x69, 0x55, 0x31, 0x39, 0x61, 0x62, 0x7A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x30, 0x46, 0x41, 0x44, 0x42, 0x49, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, + 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x45, 0x52, 0x54, 0x45, 0x56, 0x4D, 0x42, 0x4D, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4D, 0x4D, 0x52, 0x43, 0x31, 0x55, 0x63, 0x6E, 0x56, 0x7A, 0x64, 0x43, 0x42, 0x48, 0x62, + 0x57, 0x4A, 0x49, 0x4D, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6C, 0x45, 0x4C, 0x56, 0x52, 0x53, 0x56, 0x56, 0x4E, 0x55, 0x49, 0x45, 0x56, 0x57, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, + 0x30, 0x45, 0x67, 0x0A, 0x4D, 0x69, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x7A, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x7A, 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x54, 0x41, 0x35, 0x4D, 0x54, 0x41, 0x7A, 0x4D, 0x31, 0x6F, 0x58, 0x44, 0x54, 0x4D, 0x34, + 0x4D, 0x44, 0x55, 0x77, 0x4F, 0x54, 0x41, 0x35, 0x4D, 0x54, 0x41, 0x7A, 0x4D, 0x6C, 0x6F, 0x77, 0x53, 0x44, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x52, 0x45, 0x55, 0x78, 0x46, 0x54, 0x41, 0x54, + 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x54, 0x44, 0x45, 0x51, 0x74, 0x56, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x51, 0x67, 0x52, 0x32, 0x31, 0x69, 0x53, 0x44, 0x45, 0x69, 0x4D, 0x43, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, + 0x5A, 0x52, 0x43, 0x31, 0x55, 0x55, 0x6C, 0x56, 0x54, 0x56, 0x43, 0x42, 0x46, 0x56, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, 0x45, 0x4E, 0x42, 0x49, 0x44, 0x49, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x7A, 0x43, 0x43, 0x0A, 0x41, 0x69, + 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4E, + 0x69, 0x4F, 0x6F, 0x34, 0x6D, 0x41, 0x43, 0x37, 0x4A, 0x58, 0x55, 0x74, 0x79, 0x70, 0x55, 0x30, 0x77, 0x33, 0x75, 0x58, 0x39, 0x6A, 0x46, 0x78, 0x50, 0x76, 0x70, 0x31, 0x73, 0x6A, 0x57, 0x32, 0x6C, 0x31, 0x0A, 0x73, 0x4A, 0x6B, 0x4B, 0x46, + 0x38, 0x47, 0x4C, 0x78, 0x4E, 0x75, 0x6F, 0x34, 0x4D, 0x77, 0x78, 0x75, 0x73, 0x4C, 0x79, 0x7A, 0x56, 0x33, 0x70, 0x74, 0x2F, 0x67, 0x64, 0x72, 0x32, 0x72, 0x45, 0x6C, 0x59, 0x66, 0x58, 0x52, 0x38, 0x6D, 0x56, 0x32, 0x49, 0x49, 0x45, 0x55, + 0x44, 0x32, 0x42, 0x43, 0x50, 0x2F, 0x6B, 0x50, 0x62, 0x4F, 0x78, 0x31, 0x73, 0x57, 0x79, 0x2F, 0x59, 0x67, 0x4A, 0x32, 0x35, 0x79, 0x45, 0x37, 0x43, 0x55, 0x58, 0x46, 0x49, 0x64, 0x2F, 0x0A, 0x4D, 0x48, 0x69, 0x62, 0x61, 0x6C, 0x6A, 0x4A, + 0x74, 0x6E, 0x4D, 0x6F, 0x50, 0x44, 0x54, 0x33, 0x6D, 0x66, 0x64, 0x2F, 0x30, 0x36, 0x62, 0x34, 0x48, 0x45, 0x56, 0x38, 0x72, 0x53, 0x79, 0x4D, 0x6C, 0x44, 0x2F, 0x59, 0x5A, 0x78, 0x42, 0x54, 0x66, 0x69, 0x4C, 0x4E, 0x54, 0x69, 0x56, 0x52, + 0x38, 0x43, 0x55, 0x6B, 0x4E, 0x52, 0x46, 0x65, 0x45, 0x4D, 0x62, 0x73, 0x68, 0x32, 0x61, 0x4A, 0x67, 0x57, 0x69, 0x36, 0x7A, 0x43, 0x75, 0x64, 0x52, 0x33, 0x4D, 0x66, 0x0A, 0x76, 0x63, 0x32, 0x52, 0x70, 0x48, 0x4A, 0x71, 0x6E, 0x4B, 0x49, + 0x62, 0x47, 0x4B, 0x42, 0x76, 0x37, 0x46, 0x44, 0x30, 0x66, 0x55, 0x44, 0x43, 0x71, 0x44, 0x44, 0x50, 0x76, 0x58, 0x50, 0x49, 0x45, 0x79, 0x73, 0x51, 0x45, 0x78, 0x36, 0x4C, 0x6D, 0x71, 0x67, 0x36, 0x6C, 0x48, 0x50, 0x54, 0x47, 0x47, 0x6B, + 0x4B, 0x53, 0x76, 0x2F, 0x42, 0x41, 0x51, 0x50, 0x2F, 0x65, 0x58, 0x2B, 0x31, 0x53, 0x48, 0x39, 0x37, 0x37, 0x75, 0x67, 0x70, 0x62, 0x7A, 0x5A, 0x4D, 0x0A, 0x6C, 0x57, 0x47, 0x47, 0x32, 0x50, 0x6D, 0x69, 0x63, 0x34, 0x72, 0x75, 0x72, 0x69, + 0x2B, 0x57, 0x37, 0x6D, 0x6A, 0x4E, 0x50, 0x55, 0x30, 0x6F, 0x51, 0x76, 0x6C, 0x46, 0x4B, 0x7A, 0x49, 0x62, 0x52, 0x6C, 0x55, 0x57, 0x61, 0x71, 0x5A, 0x4C, 0x4B, 0x66, 0x6D, 0x37, 0x6C, 0x56, 0x61, 0x2F, 0x52, 0x68, 0x33, 0x73, 0x48, 0x5A, + 0x4D, 0x64, 0x77, 0x47, 0x57, 0x79, 0x48, 0x36, 0x46, 0x44, 0x72, 0x6C, 0x61, 0x65, 0x6F, 0x4C, 0x47, 0x50, 0x61, 0x78, 0x4B, 0x33, 0x0A, 0x59, 0x47, 0x31, 0x34, 0x43, 0x38, 0x71, 0x4B, 0x58, 0x4F, 0x30, 0x65, 0x6C, 0x67, 0x36, 0x44, 0x70, + 0x6B, 0x69, 0x56, 0x6A, 0x54, 0x75, 0x6A, 0x49, 0x63, 0x53, 0x75, 0x57, 0x4D, 0x59, 0x41, 0x73, 0x6F, 0x53, 0x30, 0x49, 0x36, 0x53, 0x57, 0x68, 0x6A, 0x57, 0x34, 0x32, 0x4A, 0x37, 0x59, 0x72, 0x44, 0x52, 0x4A, 0x6D, 0x47, 0x4F, 0x56, 0x78, + 0x63, 0x74, 0x74, 0x53, 0x45, 0x66, 0x69, 0x38, 0x69, 0x34, 0x59, 0x48, 0x74, 0x41, 0x78, 0x71, 0x39, 0x31, 0x30, 0x0A, 0x37, 0x50, 0x6E, 0x63, 0x6A, 0x4C, 0x67, 0x63, 0x6A, 0x6D, 0x67, 0x6A, 0x75, 0x74, 0x44, 0x7A, 0x55, 0x4E, 0x7A, 0x50, + 0x5A, 0x59, 0x39, 0x7A, 0x4F, 0x6A, 0x4C, 0x48, 0x66, 0x50, 0x37, 0x4B, 0x67, 0x69, 0x4A, 0x50, 0x76, 0x6F, 0x35, 0x69, 0x52, 0x32, 0x62, 0x6C, 0x7A, 0x59, 0x66, 0x69, 0x36, 0x4E, 0x55, 0x50, 0x47, 0x4A, 0x2F, 0x6C, 0x42, 0x48, 0x4A, 0x4C, + 0x52, 0x6A, 0x77, 0x51, 0x38, 0x6B, 0x54, 0x43, 0x5A, 0x46, 0x5A, 0x78, 0x54, 0x6E, 0x58, 0x6F, 0x0A, 0x6E, 0x4D, 0x6B, 0x6D, 0x64, 0x4D, 0x56, 0x39, 0x57, 0x64, 0x45, 0x4B, 0x57, 0x77, 0x39, 0x74, 0x2F, 0x70, 0x35, 0x31, 0x48, 0x42, 0x6A, + 0x47, 0x47, 0x6A, 0x70, 0x38, 0x32, 0x41, 0x30, 0x45, 0x7A, 0x4D, 0x32, 0x33, 0x52, 0x57, 0x56, 0x36, 0x73, 0x59, 0x2B, 0x34, 0x72, 0x6F, 0x52, 0x49, 0x50, 0x72, 0x4E, 0x36, 0x54, 0x61, 0x67, 0x44, 0x34, 0x75, 0x4A, 0x2B, 0x41, 0x52, 0x5A, + 0x5A, 0x61, 0x42, 0x68, 0x44, 0x4D, 0x37, 0x44, 0x53, 0x33, 0x4C, 0x41, 0x61, 0x0A, 0x51, 0x7A, 0x58, 0x75, 0x70, 0x64, 0x71, 0x70, 0x52, 0x6C, 0x79, 0x75, 0x68, 0x6F, 0x46, 0x42, 0x41, 0x55, 0x70, 0x30, 0x4A, 0x75, 0x79, 0x66, 0x42, 0x72, + 0x2F, 0x43, 0x42, 0x54, 0x64, 0x6B, 0x64, 0x58, 0x67, 0x70, 0x61, 0x50, 0x33, 0x46, 0x39, 0x65, 0x76, 0x2B, 0x52, 0x2F, 0x6E, 0x6B, 0x68, 0x62, 0x44, 0x68, 0x65, 0x7A, 0x47, 0x64, 0x70, 0x6E, 0x39, 0x79, 0x6F, 0x37, 0x6E, 0x45, 0x4C, 0x43, + 0x37, 0x4D, 0x6D, 0x56, 0x63, 0x4F, 0x49, 0x51, 0x78, 0x46, 0x0A, 0x41, 0x5A, 0x52, 0x6C, 0x36, 0x32, 0x55, 0x4A, 0x78, 0x6D, 0x4D, 0x69, 0x43, 0x7A, 0x4E, 0x4A, 0x6B, 0x6B, 0x67, 0x38, 0x2F, 0x4D, 0x33, 0x4F, 0x73, 0x44, 0x36, 0x4F, 0x6E, + 0x6F, 0x76, 0x34, 0x2F, 0x6B, 0x6E, 0x46, 0x4E, 0x58, 0x4A, 0x48, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x67, 0x59, 0x34, 0x77, 0x67, 0x59, 0x73, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, + 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x0A, 0x2F, 0x7A, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x71, 0x76, 0x79, 0x52, 0x45, 0x42, 0x75, 0x48, 0x6B, 0x56, 0x38, 0x57, 0x75, 0x62, 0x39, 0x50, + 0x53, 0x35, 0x46, 0x65, 0x41, 0x42, 0x79, 0x78, 0x4D, 0x6F, 0x41, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4D, 0x45, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x64, + 0x48, 0x77, 0x52, 0x43, 0x0A, 0x4D, 0x45, 0x41, 0x77, 0x50, 0x71, 0x41, 0x38, 0x6F, 0x44, 0x71, 0x47, 0x4F, 0x47, 0x68, 0x30, 0x64, 0x48, 0x41, 0x36, 0x4C, 0x79, 0x39, 0x6A, 0x63, 0x6D, 0x77, 0x75, 0x5A, 0x43, 0x31, 0x30, 0x63, 0x6E, 0x56, + 0x7A, 0x64, 0x43, 0x35, 0x75, 0x5A, 0x58, 0x51, 0x76, 0x59, 0x33, 0x4A, 0x73, 0x4C, 0x32, 0x51, 0x74, 0x64, 0x48, 0x4A, 0x31, 0x63, 0x33, 0x52, 0x66, 0x5A, 0x58, 0x5A, 0x66, 0x63, 0x6D, 0x39, 0x76, 0x64, 0x46, 0x39, 0x6A, 0x59, 0x56, 0x38, + 0x79, 0x0A, 0x58, 0x7A, 0x49, 0x77, 0x4D, 0x6A, 0x4D, 0x75, 0x59, 0x33, 0x4A, 0x73, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x44, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x41, 0x51, + 0x43, 0x54, 0x79, 0x36, 0x55, 0x66, 0x6D, 0x52, 0x48, 0x73, 0x6D, 0x67, 0x31, 0x66, 0x4C, 0x42, 0x57, 0x54, 0x78, 0x6A, 0x2B, 0x2B, 0x45, 0x49, 0x31, 0x34, 0x51, 0x76, 0x42, 0x75, 0x6B, 0x45, 0x64, 0x48, 0x6A, 0x71, 0x4F, 0x53, 0x0A, 0x4D, + 0x6F, 0x31, 0x77, 0x6A, 0x2F, 0x5A, 0x62, 0x6A, 0x62, 0x36, 0x4A, 0x7A, 0x6B, 0x63, 0x42, 0x61, 0x68, 0x73, 0x67, 0x49, 0x49, 0x6C, 0x62, 0x79, 0x49, 0x49, 0x51, 0x62, 0x4F, 0x44, 0x6E, 0x6D, 0x61, 0x70, 0x72, 0x78, 0x69, 0x71, 0x67, 0x59, + 0x7A, 0x57, 0x52, 0x61, 0x6F, 0x55, 0x6C, 0x72, 0x52, 0x63, 0x34, 0x70, 0x5A, 0x74, 0x2B, 0x55, 0x50, 0x4A, 0x32, 0x36, 0x6F, 0x55, 0x46, 0x4B, 0x69, 0x64, 0x42, 0x4B, 0x37, 0x47, 0x42, 0x30, 0x61, 0x4C, 0x32, 0x0A, 0x51, 0x48, 0x57, 0x70, + 0x44, 0x73, 0x76, 0x78, 0x56, 0x55, 0x6A, 0x59, 0x37, 0x4E, 0x48, 0x73, 0x73, 0x2B, 0x6A, 0x4F, 0x46, 0x4B, 0x45, 0x31, 0x37, 0x4D, 0x4A, 0x65, 0x4E, 0x52, 0x71, 0x72, 0x70, 0x68, 0x59, 0x42, 0x42, 0x6F, 0x37, 0x71, 0x33, 0x43, 0x2B, 0x6A, + 0x69, 0x73, 0x6F, 0x73, 0x6B, 0x65, 0x74, 0x53, 0x6A, 0x6C, 0x38, 0x4D, 0x6D, 0x78, 0x66, 0x50, 0x79, 0x33, 0x4D, 0x48, 0x47, 0x63, 0x52, 0x71, 0x77, 0x6E, 0x4E, 0x55, 0x37, 0x33, 0x78, 0x44, 0x0A, 0x55, 0x6D, 0x50, 0x42, 0x45, 0x63, 0x72, + 0x43, 0x52, 0x62, 0x48, 0x30, 0x4F, 0x31, 0x50, 0x31, 0x61, 0x61, 0x34, 0x38, 0x34, 0x36, 0x58, 0x65, 0x72, 0x4F, 0x68, 0x55, 0x74, 0x37, 0x4B, 0x52, 0x2F, 0x61, 0x79, 0x70, 0x48, 0x2F, 0x4B, 0x48, 0x35, 0x42, 0x66, 0x47, 0x53, 0x61, 0x68, + 0x38, 0x32, 0x41, 0x70, 0x42, 0x39, 0x50, 0x49, 0x2B, 0x35, 0x33, 0x63, 0x30, 0x42, 0x46, 0x4C, 0x64, 0x36, 0x49, 0x48, 0x79, 0x54, 0x53, 0x39, 0x55, 0x52, 0x5A, 0x30, 0x56, 0x0A, 0x34, 0x55, 0x2F, 0x4D, 0x35, 0x64, 0x34, 0x30, 0x56, 0x78, + 0x44, 0x4A, 0x49, 0x33, 0x49, 0x58, 0x63, 0x49, 0x31, 0x51, 0x63, 0x42, 0x39, 0x57, 0x62, 0x4D, 0x79, 0x35, 0x2F, 0x7A, 0x70, 0x61, 0x54, 0x32, 0x4E, 0x36, 0x77, 0x32, 0x35, 0x6C, 0x42, 0x78, 0x32, 0x45, 0x6F, 0x66, 0x2B, 0x70, 0x44, 0x47, + 0x4F, 0x4A, 0x62, 0x62, 0x4A, 0x41, 0x69, 0x44, 0x6E, 0x58, 0x48, 0x33, 0x64, 0x6F, 0x74, 0x66, 0x79, 0x63, 0x31, 0x64, 0x5A, 0x6E, 0x61, 0x56, 0x75, 0x6F, 0x0A, 0x64, 0x4E, 0x76, 0x38, 0x69, 0x66, 0x59, 0x62, 0x4D, 0x76, 0x65, 0x6B, 0x4A, + 0x4B, 0x5A, 0x32, 0x74, 0x30, 0x64, 0x54, 0x37, 0x34, 0x31, 0x4A, 0x6A, 0x36, 0x6D, 0x32, 0x67, 0x31, 0x71, 0x6C, 0x6C, 0x70, 0x42, 0x46, 0x59, 0x66, 0x58, 0x65, 0x41, 0x30, 0x38, 0x6D, 0x44, 0x36, 0x69, 0x4C, 0x38, 0x41, 0x4F, 0x57, 0x73, + 0x4B, 0x77, 0x56, 0x30, 0x48, 0x46, 0x61, 0x61, 0x6E, 0x75, 0x55, 0x35, 0x6E, 0x43, 0x54, 0x32, 0x76, 0x46, 0x70, 0x34, 0x4C, 0x4A, 0x69, 0x0A, 0x54, 0x5A, 0x36, 0x50, 0x2F, 0x34, 0x6D, 0x64, 0x6D, 0x31, 0x33, 0x4E, 0x52, 0x65, 0x6D, 0x55, + 0x41, 0x69, 0x4B, 0x4E, 0x34, 0x44, 0x56, 0x2F, 0x36, 0x50, 0x45, 0x45, 0x65, 0x58, 0x46, 0x73, 0x56, 0x49, 0x50, 0x34, 0x4D, 0x37, 0x6B, 0x46, 0x4D, 0x68, 0x74, 0x59, 0x56, 0x52, 0x46, 0x50, 0x30, 0x4F, 0x55, 0x6E, 0x52, 0x33, 0x48, 0x73, + 0x37, 0x64, 0x70, 0x6E, 0x31, 0x6D, 0x4B, 0x6D, 0x53, 0x30, 0x30, 0x50, 0x61, 0x61, 0x4C, 0x4A, 0x76, 0x4F, 0x77, 0x69, 0x0A, 0x53, 0x35, 0x54, 0x48, 0x61, 0x4A, 0x51, 0x58, 0x66, 0x75, 0x4B, 0x4F, 0x4B, 0x44, 0x36, 0x32, 0x78, 0x75, 0x72, + 0x31, 0x4E, 0x47, 0x79, 0x66, 0x4E, 0x34, 0x67, 0x48, 0x4F, 0x4E, 0x75, 0x47, 0x63, 0x66, 0x72, 0x4E, 0x6C, 0x55, 0x68, 0x44, 0x62, 0x71, 0x4E, 0x50, 0x67, 0x6F, 0x66, 0x58, 0x4E, 0x4A, 0x68, 0x75, 0x53, 0x35, 0x4E, 0x35, 0x59, 0x48, 0x56, + 0x70, 0x44, 0x2F, 0x41, 0x61, 0x31, 0x56, 0x50, 0x36, 0x49, 0x51, 0x7A, 0x43, 0x50, 0x2B, 0x6B, 0x2F, 0x0A, 0x48, 0x78, 0x69, 0x4D, 0x6B, 0x6C, 0x31, 0x34, 0x70, 0x33, 0x5A, 0x6E, 0x47, 0x62, 0x75, 0x79, 0x36, 0x6E, 0x2F, 0x70, 0x63, 0x41, + 0x6C, 0x57, 0x56, 0x71, 0x4F, 0x77, 0x44, 0x41, 0x73, 0x74, 0x4E, 0x6C, 0x37, 0x46, 0x36, 0x63, 0x54, 0x56, 0x67, 0x38, 0x75, 0x47, 0x46, 0x35, 0x63, 0x73, 0x62, 0x42, 0x4E, 0x76, 0x68, 0x31, 0x71, 0x76, 0x53, 0x61, 0x59, 0x64, 0x32, 0x38, + 0x30, 0x34, 0x42, 0x43, 0x35, 0x66, 0x34, 0x6B, 0x6F, 0x31, 0x44, 0x69, 0x31, 0x4C, 0x0A, 0x2B, 0x4B, 0x49, 0x6B, 0x42, 0x49, 0x33, 0x59, 0x34, 0x57, 0x4E, 0x65, 0x41, 0x70, 0x49, 0x30, 0x32, 0x70, 0x68, 0x68, 0x58, 0x42, 0x78, 0x76, 0x57, + 0x48, 0x5A, 0x6B, 0x73, 0x2F, 0x77, 0x43, 0x75, 0x50, 0x57, 0x64, 0x43, 0x67, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, + 0x2D, 0x0A, 0x0A, 0x53, 0x77, 0x69, 0x73, 0x73, 0x53, 0x69, 0x67, 0x6E, 0x20, 0x52, 0x53, 0x41, 0x20, 0x54, 0x4C, 0x53, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32, 0x30, 0x32, 0x32, 0x20, 0x2D, 0x20, 0x31, 0x0A, 0x3D, 0x3D, + 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, + 0x47, 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x6B, 0x7A, 0x43, 0x43, 0x41, 0x33, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, + 0x55, 0x51, 0x2F, 0x6F, 0x4D, 0x58, 0x30, 0x34, 0x62, 0x67, 0x42, 0x68, 0x45, 0x37, 0x39, 0x47, 0x30, 0x54, 0x7A, 0x55, 0x66, 0x52, 0x50, 0x53, 0x41, 0x37, 0x63, 0x73, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, + 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x42, 0x51, 0x41, 0x77, 0x55, 0x54, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x0A, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, 0x4D, 0x43, 0x51, 0x30, 0x67, 0x78, 0x46, 0x54, 0x41, 0x54, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, + 0x6F, 0x54, 0x44, 0x46, 0x4E, 0x33, 0x61, 0x58, 0x4E, 0x7A, 0x55, 0x32, 0x6C, 0x6E, 0x62, 0x69, 0x42, 0x42, 0x52, 0x7A, 0x45, 0x72, 0x4D, 0x43, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4D, 0x69, 0x55, 0x33, 0x64, 0x70, 0x63, 0x33, + 0x4E, 0x54, 0x61, 0x57, 0x64, 0x75, 0x49, 0x46, 0x4A, 0x54, 0x51, 0x53, 0x42, 0x55, 0x0A, 0x54, 0x46, 0x4D, 0x67, 0x55, 0x6D, 0x39, 0x76, 0x64, 0x43, 0x42, 0x44, 0x51, 0x53, 0x41, 0x79, 0x4D, 0x44, 0x49, 0x79, 0x49, 0x43, 0x30, 0x67, 0x4D, + 0x54, 0x41, 0x65, 0x46, 0x77, 0x30, 0x79, 0x4D, 0x6A, 0x41, 0x32, 0x4D, 0x44, 0x67, 0x78, 0x4D, 0x54, 0x41, 0x34, 0x4D, 0x6A, 0x4A, 0x61, 0x46, 0x77, 0x30, 0x30, 0x4E, 0x7A, 0x41, 0x32, 0x4D, 0x44, 0x67, 0x78, 0x4D, 0x54, 0x41, 0x34, 0x4D, + 0x6A, 0x4A, 0x61, 0x4D, 0x46, 0x45, 0x78, 0x43, 0x7A, 0x41, 0x4A, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6B, 0x4E, 0x49, 0x4D, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4B, 0x45, 0x77, 0x78, 0x54, + 0x64, 0x32, 0x6C, 0x7A, 0x63, 0x31, 0x4E, 0x70, 0x5A, 0x32, 0x34, 0x67, 0x51, 0x55, 0x63, 0x78, 0x4B, 0x7A, 0x41, 0x70, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x54, 0x49, 0x6C, 0x4E, 0x33, 0x61, 0x58, 0x4E, 0x7A, 0x55, 0x32, 0x6C, 0x6E, + 0x62, 0x69, 0x42, 0x53, 0x55, 0x30, 0x45, 0x67, 0x0A, 0x56, 0x45, 0x78, 0x54, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x51, 0x30, 0x45, 0x67, 0x4D, 0x6A, 0x41, 0x79, 0x4D, 0x69, 0x41, 0x74, 0x49, 0x44, 0x45, 0x77, 0x67, 0x67, 0x49, + 0x69, 0x4D, 0x41, 0x30, 0x47, 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x44, 0x51, 0x45, 0x42, 0x41, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x43, 0x44, 0x77, 0x41, 0x77, 0x67, 0x67, 0x49, 0x4B, 0x41, 0x6F, 0x49, 0x43, 0x41, 0x51, 0x44, + 0x4C, 0x4B, 0x6D, 0x6A, 0x69, 0x0A, 0x43, 0x38, 0x4E, 0x58, 0x76, 0x44, 0x56, 0x6A, 0x76, 0x48, 0x43, 0x6C, 0x4F, 0x2F, 0x4F, 0x4D, 0x50, 0x45, 0x35, 0x58, 0x6C, 0x6D, 0x37, 0x44, 0x54, 0x6A, 0x61, 0x6B, 0x39, 0x67, 0x4C, 0x4B, 0x48, 0x71, + 0x71, 0x75, 0x75, 0x4E, 0x36, 0x6F, 0x72, 0x78, 0x31, 0x32, 0x32, 0x72, 0x6F, 0x31, 0x30, 0x4A, 0x46, 0x77, 0x42, 0x39, 0x2B, 0x7A, 0x42, 0x76, 0x4B, 0x4B, 0x38, 0x69, 0x35, 0x56, 0x55, 0x58, 0x75, 0x37, 0x4C, 0x43, 0x54, 0x4C, 0x66, 0x35, + 0x49, 0x6D, 0x0A, 0x67, 0x4B, 0x4F, 0x30, 0x6C, 0x50, 0x61, 0x43, 0x6F, 0x61, 0x54, 0x6F, 0x2B, 0x6E, 0x55, 0x64, 0x57, 0x66, 0x4D, 0x48, 0x61, 0x6D, 0x46, 0x6B, 0x34, 0x73, 0x61, 0x4D, 0x6C, 0x61, 0x2B, 0x6A, 0x75, 0x34, 0x35, 0x76, 0x56, + 0x73, 0x39, 0x78, 0x7A, 0x46, 0x36, 0x42, 0x59, 0x51, 0x31, 0x74, 0x38, 0x71, 0x73, 0x43, 0x4C, 0x71, 0x53, 0x58, 0x35, 0x58, 0x48, 0x38, 0x69, 0x72, 0x43, 0x52, 0x49, 0x46, 0x75, 0x63, 0x64, 0x46, 0x4A, 0x74, 0x72, 0x68, 0x55, 0x6E, 0x0A, + 0x57, 0x58, 0x6A, 0x79, 0x43, 0x63, 0x70, 0x6C, 0x44, 0x6E, 0x2F, 0x4C, 0x39, 0x4F, 0x76, 0x6E, 0x33, 0x4B, 0x6C, 0x4D, 0x64, 0x2F, 0x59, 0x72, 0x46, 0x67, 0x53, 0x56, 0x72, 0x70, 0x78, 0x78, 0x70, 0x54, 0x38, 0x71, 0x32, 0x6B, 0x46, 0x43, + 0x35, 0x7A, 0x79, 0x45, 0x45, 0x50, 0x54, 0x68, 0x50, 0x59, 0x78, 0x72, 0x34, 0x69, 0x75, 0x52, 0x52, 0x31, 0x56, 0x50, 0x75, 0x46, 0x61, 0x2B, 0x52, 0x64, 0x34, 0x69, 0x55, 0x55, 0x31, 0x4F, 0x4B, 0x4E, 0x6C, 0x66, 0x0A, 0x47, 0x55, 0x45, + 0x47, 0x6A, 0x77, 0x35, 0x4E, 0x42, 0x75, 0x42, 0x77, 0x51, 0x43, 0x4D, 0x42, 0x61, 0x75, 0x54, 0x4C, 0x45, 0x35, 0x74, 0x7A, 0x72, 0x45, 0x30, 0x55, 0x53, 0x4A, 0x49, 0x74, 0x2F, 0x6D, 0x32, 0x6E, 0x2B, 0x49, 0x64, 0x72, 0x65, 0x58, 0x58, + 0x68, 0x76, 0x68, 0x43, 0x78, 0x71, 0x6F, 0x68, 0x41, 0x57, 0x56, 0x54, 0x58, 0x7A, 0x38, 0x54, 0x51, 0x6D, 0x30, 0x53, 0x7A, 0x4F, 0x47, 0x6C, 0x6B, 0x6A, 0x49, 0x48, 0x52, 0x49, 0x33, 0x36, 0x71, 0x0A, 0x4F, 0x54, 0x77, 0x37, 0x44, 0x35, + 0x39, 0x4B, 0x65, 0x34, 0x4C, 0x4B, 0x61, 0x32, 0x2F, 0x4B, 0x49, 0x6A, 0x34, 0x78, 0x30, 0x4C, 0x44, 0x51, 0x4B, 0x68, 0x79, 0x53, 0x69, 0x6F, 0x2F, 0x59, 0x47, 0x5A, 0x78, 0x48, 0x35, 0x44, 0x34, 0x4D, 0x75, 0x63, 0x4C, 0x4E, 0x76, 0x6B, + 0x45, 0x4D, 0x2B, 0x4B, 0x52, 0x48, 0x42, 0x64, 0x76, 0x42, 0x46, 0x7A, 0x41, 0x34, 0x4F, 0x6D, 0x6E, 0x63, 0x7A, 0x63, 0x4E, 0x70, 0x49, 0x2F, 0x32, 0x61, 0x44, 0x77, 0x4C, 0x4F, 0x0A, 0x45, 0x47, 0x72, 0x4F, 0x79, 0x76, 0x69, 0x35, 0x4B, + 0x61, 0x4D, 0x32, 0x69, 0x59, 0x61, 0x75, 0x43, 0x38, 0x42, 0x50, 0x59, 0x37, 0x6B, 0x47, 0x57, 0x55, 0x6C, 0x65, 0x44, 0x73, 0x46, 0x70, 0x73, 0x77, 0x72, 0x7A, 0x64, 0x33, 0x34, 0x75, 0x6E, 0x59, 0x79, 0x7A, 0x4A, 0x35, 0x6A, 0x53, 0x6D, + 0x59, 0x30, 0x6C, 0x70, 0x78, 0x2B, 0x47, 0x73, 0x36, 0x5A, 0x55, 0x63, 0x44, 0x6A, 0x38, 0x66, 0x56, 0x33, 0x6F, 0x54, 0x34, 0x4D, 0x4D, 0x30, 0x5A, 0x50, 0x6C, 0x0A, 0x45, 0x75, 0x52, 0x55, 0x32, 0x6A, 0x37, 0x79, 0x72, 0x54, 0x72, 0x65, + 0x50, 0x6A, 0x78, 0x46, 0x38, 0x43, 0x67, 0x50, 0x42, 0x72, 0x6E, 0x68, 0x32, 0x35, 0x64, 0x37, 0x6D, 0x55, 0x57, 0x65, 0x33, 0x66, 0x36, 0x56, 0x57, 0x51, 0x51, 0x76, 0x64, 0x54, 0x2F, 0x54, 0x72, 0x6F, 0x6D, 0x5A, 0x68, 0x71, 0x77, 0x55, + 0x74, 0x4B, 0x69, 0x45, 0x2B, 0x73, 0x68, 0x64, 0x4F, 0x78, 0x74, 0x59, 0x6B, 0x38, 0x45, 0x58, 0x6C, 0x46, 0x58, 0x49, 0x43, 0x2B, 0x4F, 0x43, 0x0A, 0x65, 0x59, 0x53, 0x66, 0x38, 0x77, 0x43, 0x45, 0x4E, 0x4F, 0x37, 0x63, 0x4D, 0x64, 0x57, + 0x50, 0x38, 0x76, 0x70, 0x50, 0x6C, 0x6B, 0x77, 0x47, 0x71, 0x6E, 0x6A, 0x37, 0x33, 0x6D, 0x53, 0x69, 0x49, 0x38, 0x30, 0x66, 0x50, 0x73, 0x57, 0x4D, 0x76, 0x44, 0x64, 0x55, 0x44, 0x72, 0x74, 0x61, 0x63, 0x6C, 0x58, 0x76, 0x79, 0x46, 0x75, + 0x31, 0x63, 0x76, 0x68, 0x34, 0x33, 0x7A, 0x63, 0x67, 0x54, 0x46, 0x65, 0x52, 0x63, 0x35, 0x4A, 0x7A, 0x72, 0x42, 0x68, 0x33, 0x0A, 0x51, 0x34, 0x49, 0x67, 0x61, 0x65, 0x7A, 0x70, 0x72, 0x43, 0x6C, 0x47, 0x35, 0x51, 0x74, 0x4F, 0x2B, 0x44, + 0x64, 0x7A, 0x69, 0x5A, 0x61, 0x4B, 0x48, 0x47, 0x32, 0x39, 0x37, 0x37, 0x37, 0x59, 0x74, 0x76, 0x54, 0x4B, 0x77, 0x50, 0x31, 0x48, 0x38, 0x4B, 0x34, 0x4C, 0x57, 0x43, 0x44, 0x46, 0x79, 0x42, 0x30, 0x32, 0x72, 0x70, 0x65, 0x4E, 0x55, 0x49, + 0x4D, 0x6D, 0x4A, 0x43, 0x6E, 0x33, 0x6E, 0x54, 0x73, 0x50, 0x42, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x0A, 0x6F, 0x32, 0x4D, 0x77, 0x59, 0x54, 0x41, 0x50, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x52, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, + 0x54, 0x41, 0x44, 0x41, 0x51, 0x48, 0x2F, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x42, 0x6A, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, + 0x44, 0x41, 0x57, 0x67, 0x42, 0x52, 0x76, 0x6A, 0x6D, 0x4B, 0x4C, 0x6B, 0x30, 0x4F, 0x77, 0x0A, 0x34, 0x55, 0x44, 0x32, 0x70, 0x38, 0x50, 0x39, 0x38, 0x51, 0x2B, 0x34, 0x44, 0x78, 0x55, 0x34, 0x70, 0x54, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, + 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x62, 0x34, 0x35, 0x69, 0x69, 0x35, 0x4E, 0x44, 0x73, 0x4F, 0x46, 0x41, 0x39, 0x71, 0x66, 0x44, 0x2F, 0x66, 0x45, 0x50, 0x75, 0x41, 0x38, 0x56, 0x4F, 0x4B, 0x55, 0x77, 0x44, 0x51, 0x59, 0x4A, + 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x4C, 0x0A, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4B, 0x77, 0x73, 0x4B, 0x55, 0x46, 0x39, 0x2B, 0x6C, 0x7A, 0x31, 0x47, 0x70, 0x55, 0x59, 0x76, 0x79, 0x79, + 0x70, 0x69, 0x71, 0x6B, 0x6B, 0x56, 0x48, 0x58, 0x31, 0x75, 0x45, 0x43, 0x72, 0x79, 0x36, 0x67, 0x6B, 0x55, 0x53, 0x73, 0x59, 0x50, 0x32, 0x4F, 0x70, 0x72, 0x70, 0x68, 0x57, 0x4B, 0x77, 0x56, 0x44, 0x49, 0x71, 0x4F, 0x33, 0x31, 0x30, 0x61, + 0x65, 0x77, 0x43, 0x6F, 0x53, 0x50, 0x59, 0x36, 0x57, 0x0A, 0x6C, 0x6B, 0x44, 0x66, 0x44, 0x44, 0x4F, 0x4C, 0x61, 0x7A, 0x65, 0x52, 0x4F, 0x70, 0x57, 0x37, 0x4F, 0x53, 0x6C, 0x74, 0x77, 0x41, 0x4A, 0x73, 0x69, 0x70, 0x51, 0x4C, 0x42, 0x77, + 0x4A, 0x4E, 0x47, 0x44, 0x37, 0x37, 0x2B, 0x33, 0x76, 0x31, 0x64, 0x6A, 0x32, 0x62, 0x39, 0x6C, 0x34, 0x77, 0x42, 0x6C, 0x67, 0x7A, 0x48, 0x71, 0x70, 0x34, 0x31, 0x65, 0x5A, 0x55, 0x42, 0x44, 0x71, 0x79, 0x67, 0x67, 0x6D, 0x4E, 0x7A, 0x68, + 0x59, 0x7A, 0x57, 0x55, 0x55, 0x6F, 0x0A, 0x38, 0x61, 0x57, 0x6A, 0x6C, 0x77, 0x35, 0x44, 0x49, 0x2F, 0x30, 0x4C, 0x49, 0x49, 0x43, 0x51, 0x2F, 0x2B, 0x4D, 0x6D, 0x7A, 0x37, 0x68, 0x6B, 0x6B, 0x65, 0x55, 0x46, 0x6A, 0x78, 0x4F, 0x67, 0x64, + 0x67, 0x33, 0x58, 0x4E, 0x77, 0x77, 0x51, 0x69, 0x4A, 0x62, 0x30, 0x50, 0x72, 0x36, 0x56, 0x76, 0x66, 0x48, 0x44, 0x66, 0x66, 0x43, 0x6A, 0x77, 0x33, 0x6C, 0x48, 0x43, 0x31, 0x79, 0x53, 0x46, 0x57, 0x50, 0x74, 0x55, 0x6E, 0x57, 0x4B, 0x35, + 0x30, 0x5A, 0x70, 0x0A, 0x79, 0x31, 0x46, 0x56, 0x43, 0x79, 0x70, 0x4D, 0x39, 0x66, 0x4A, 0x6B, 0x54, 0x36, 0x6C, 0x63, 0x2F, 0x32, 0x63, 0x79, 0x6A, 0x6C, 0x55, 0x74, 0x4D, 0x6F, 0x49, 0x63, 0x67, 0x43, 0x39, 0x71, 0x6B, 0x66, 0x6A, 0x4C, + 0x76, 0x48, 0x34, 0x59, 0x6F, 0x69, 0x61, 0x6F, 0x4C, 0x71, 0x4E, 0x54, 0x4B, 0x49, 0x66, 0x74, 0x56, 0x2B, 0x56, 0x6C, 0x65, 0x6B, 0x34, 0x41, 0x53, 0x6C, 0x74, 0x4F, 0x55, 0x38, 0x6C, 0x69, 0x4E, 0x72, 0x33, 0x43, 0x6A, 0x6C, 0x76, 0x72, + 0x0A, 0x7A, 0x47, 0x34, 0x6E, 0x67, 0x52, 0x68, 0x5A, 0x69, 0x30, 0x52, 0x6A, 0x6E, 0x39, 0x55, 0x4D, 0x5A, 0x66, 0x51, 0x70, 0x5A, 0x58, 0x2B, 0x52, 0x4C, 0x4F, 0x56, 0x2F, 0x66, 0x75, 0x69, 0x4A, 0x7A, 0x34, 0x38, 0x67, 0x79, 0x32, 0x30, + 0x48, 0x51, 0x68, 0x46, 0x52, 0x4A, 0x6A, 0x4B, 0x4B, 0x4C, 0x6A, 0x70, 0x48, 0x45, 0x37, 0x69, 0x4E, 0x76, 0x55, 0x63, 0x4E, 0x43, 0x66, 0x41, 0x57, 0x70, 0x4F, 0x32, 0x57, 0x68, 0x69, 0x34, 0x5A, 0x32, 0x4C, 0x36, 0x4D, 0x0A, 0x4F, 0x75, + 0x68, 0x46, 0x4C, 0x68, 0x47, 0x36, 0x72, 0x6C, 0x72, 0x6E, 0x75, 0x62, 0x2B, 0x78, 0x7A, 0x49, 0x2F, 0x67, 0x6F, 0x50, 0x2B, 0x34, 0x73, 0x39, 0x47, 0x46, 0x65, 0x33, 0x6C, 0x6D, 0x6F, 0x7A, 0x6D, 0x31, 0x4F, 0x32, 0x62, 0x59, 0x51, 0x4C, + 0x37, 0x50, 0x74, 0x32, 0x65, 0x4C, 0x53, 0x4D, 0x6B, 0x5A, 0x4A, 0x56, 0x58, 0x38, 0x76, 0x59, 0x33, 0x50, 0x58, 0x74, 0x70, 0x4F, 0x70, 0x76, 0x4A, 0x70, 0x7A, 0x76, 0x31, 0x2F, 0x54, 0x48, 0x66, 0x51, 0x0A, 0x77, 0x55, 0x59, 0x31, 0x6D, + 0x46, 0x77, 0x6A, 0x6D, 0x77, 0x4A, 0x46, 0x51, 0x35, 0x52, 0x61, 0x33, 0x62, 0x78, 0x48, 0x72, 0x53, 0x4C, 0x2B, 0x75, 0x6C, 0x34, 0x76, 0x6B, 0x53, 0x6B, 0x70, 0x68, 0x6E, 0x73, 0x68, 0x33, 0x6D, 0x35, 0x6B, 0x74, 0x38, 0x73, 0x4E, 0x6A, + 0x7A, 0x64, 0x62, 0x6F, 0x77, 0x68, 0x71, 0x36, 0x2F, 0x54, 0x64, 0x41, 0x6F, 0x39, 0x51, 0x41, 0x77, 0x4B, 0x78, 0x75, 0x44, 0x64, 0x6F, 0x6C, 0x6C, 0x44, 0x72, 0x75, 0x46, 0x2F, 0x55, 0x0A, 0x4B, 0x49, 0x71, 0x6C, 0x49, 0x67, 0x79, 0x4B, + 0x68, 0x50, 0x42, 0x5A, 0x4C, 0x74, 0x55, 0x33, 0x30, 0x57, 0x48, 0x6C, 0x51, 0x6E, 0x4E, 0x59, 0x4B, 0x6F, 0x48, 0x33, 0x64, 0x74, 0x76, 0x69, 0x34, 0x6B, 0x30, 0x4E, 0x58, 0x2F, 0x61, 0x33, 0x76, 0x67, 0x57, 0x30, 0x72, 0x6B, 0x34, 0x4E, + 0x33, 0x68, 0x59, 0x39, 0x41, 0x34, 0x47, 0x7A, 0x4A, 0x6C, 0x35, 0x4C, 0x75, 0x45, 0x73, 0x41, 0x7A, 0x2F, 0x2B, 0x4D, 0x46, 0x37, 0x70, 0x73, 0x59, 0x43, 0x30, 0x6E, 0x0A, 0x68, 0x7A, 0x63, 0x6B, 0x35, 0x6E, 0x70, 0x67, 0x4C, 0x37, 0x58, + 0x54, 0x67, 0x77, 0x53, 0x71, 0x54, 0x30, 0x4E, 0x31, 0x6F, 0x73, 0x47, 0x44, 0x73, 0x69, 0x65, 0x59, 0x4B, 0x37, 0x45, 0x4F, 0x67, 0x4C, 0x72, 0x41, 0x68, 0x56, 0x35, 0x43, 0x75, 0x64, 0x2B, 0x78, 0x59, 0x4A, 0x48, 0x54, 0x36, 0x78, 0x68, + 0x2B, 0x63, 0x48, 0x69, 0x75, 0x64, 0x6F, 0x4F, 0x2B, 0x63, 0x56, 0x72, 0x51, 0x6B, 0x4F, 0x50, 0x4B, 0x77, 0x52, 0x59, 0x6C, 0x5A, 0x30, 0x72, 0x77, 0x0A, 0x74, 0x6E, 0x75, 0x36, 0x34, 0x5A, 0x7A, 0x5A, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, + 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x4F, 0x49, 0x53, 0x54, 0x45, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, + 0x45, 0x43, 0x43, 0x20, 0x47, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, + 0x49, 0x4E, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x43, 0x4E, 0x54, 0x43, 0x43, 0x41, 0x62, 0x71, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, + 0x49, 0x2F, 0x6E, 0x44, 0x31, 0x6A, 0x57, 0x76, 0x6A, 0x79, 0x68, 0x4C, 0x48, 0x2F, 0x42, 0x55, 0x36, 0x6E, 0x36, 0x58, 0x6E, 0x54, 0x41, 0x4B, 0x42, 0x67, 0x67, 0x71, 0x68, 0x6B, 0x6A, 0x4F, 0x50, 0x51, 0x51, 0x44, 0x41, 0x7A, 0x42, 0x4C, + 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4A, 0x44, 0x0A, 0x53, 0x44, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x51, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, 0x55, + 0x67, 0x52, 0x6D, 0x39, 0x31, 0x62, 0x6D, 0x52, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6A, 0x45, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x59, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, 0x55, 0x67, 0x55, 0x32, 0x56, + 0x79, 0x64, 0x6D, 0x56, 0x79, 0x49, 0x46, 0x4A, 0x76, 0x62, 0x33, 0x51, 0x67, 0x0A, 0x52, 0x55, 0x4E, 0x44, 0x49, 0x45, 0x63, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x7A, 0x4D, 0x44, 0x55, 0x7A, 0x4D, 0x54, 0x45, 0x30, 0x4E, 0x44, + 0x49, 0x79, 0x4F, 0x46, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x34, 0x4D, 0x44, 0x55, 0x79, 0x4E, 0x44, 0x45, 0x30, 0x4E, 0x44, 0x49, 0x79, 0x4E, 0x31, 0x6F, 0x77, 0x53, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, 0x68, + 0x4D, 0x43, 0x51, 0x30, 0x67, 0x78, 0x47, 0x54, 0x41, 0x58, 0x0A, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x45, 0x45, 0x39, 0x4A, 0x55, 0x31, 0x52, 0x46, 0x49, 0x45, 0x5A, 0x76, 0x64, 0x57, 0x35, 0x6B, 0x59, 0x58, 0x52, 0x70, 0x62, + 0x32, 0x34, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x47, 0x45, 0x39, 0x4A, 0x55, 0x31, 0x52, 0x46, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x6C, 0x63, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x49, + 0x45, 0x56, 0x44, 0x51, 0x79, 0x42, 0x48, 0x0A, 0x4D, 0x54, 0x42, 0x32, 0x4D, 0x42, 0x41, 0x47, 0x42, 0x79, 0x71, 0x47, 0x53, 0x4D, 0x34, 0x39, 0x41, 0x67, 0x45, 0x47, 0x42, 0x53, 0x75, 0x42, 0x42, 0x41, 0x41, 0x69, 0x41, 0x32, 0x49, 0x41, + 0x42, 0x42, 0x63, 0x76, 0x2B, 0x68, 0x4B, 0x38, 0x72, 0x42, 0x6A, 0x7A, 0x43, 0x76, 0x52, 0x45, 0x31, 0x6E, 0x5A, 0x43, 0x6E, 0x72, 0x50, 0x6F, 0x48, 0x37, 0x64, 0x35, 0x71, 0x56, 0x69, 0x32, 0x2B, 0x47, 0x58, 0x52, 0x4F, 0x69, 0x46, 0x50, + 0x71, 0x4F, 0x75, 0x6A, 0x0A, 0x76, 0x71, 0x51, 0x79, 0x63, 0x76, 0x4F, 0x32, 0x41, 0x63, 0x6B, 0x72, 0x2F, 0x58, 0x65, 0x46, 0x62, 0x6C, 0x50, 0x64, 0x72, 0x65, 0x71, 0x71, 0x4C, 0x69, 0x57, 0x53, 0x74, 0x75, 0x6B, 0x68, 0x45, 0x61, 0x69, + 0x76, 0x74, 0x55, 0x77, 0x4C, 0x38, 0x35, 0x5A, 0x67, 0x6D, 0x6A, 0x76, 0x6E, 0x36, 0x68, 0x70, 0x34, 0x4C, 0x72, 0x51, 0x39, 0x35, 0x53, 0x6A, 0x65, 0x48, 0x49, 0x43, 0x36, 0x58, 0x47, 0x34, 0x4E, 0x32, 0x78, 0x6D, 0x6C, 0x34, 0x7A, 0x2B, + 0x63, 0x0A, 0x4B, 0x72, 0x68, 0x41, 0x53, 0x39, 0x33, 0x6D, 0x54, 0x36, 0x4E, 0x6A, 0x4D, 0x47, 0x45, 0x77, 0x44, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x54, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x55, 0x77, 0x41, 0x77, 0x45, 0x42, 0x2F, 0x7A, + 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x53, 0x4D, 0x45, 0x47, 0x44, 0x41, 0x57, 0x67, 0x42, 0x51, 0x33, 0x54, 0x59, 0x68, 0x6C, 0x7A, 0x2F, 0x77, 0x39, 0x69, 0x74, 0x57, 0x6A, 0x38, 0x55, 0x6E, 0x41, 0x54, 0x67, 0x77, 0x51, 0x0A, 0x62, + 0x30, 0x4B, 0x30, 0x6E, 0x44, 0x41, 0x64, 0x42, 0x67, 0x4E, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, 0x51, 0x55, 0x4E, 0x30, 0x32, 0x49, 0x5A, 0x63, 0x2F, 0x38, 0x50, 0x59, 0x72, 0x56, 0x6F, 0x2F, 0x46, 0x4A, 0x77, 0x45, 0x34, 0x4D, 0x45, + 0x47, 0x39, 0x43, 0x74, 0x4A, 0x77, 0x77, 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2F, 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x47, 0x47, 0x4D, 0x41, 0x6F, 0x47, 0x43, 0x43, 0x71, 0x47, 0x0A, 0x53, 0x4D, 0x34, 0x39, + 0x42, 0x41, 0x4D, 0x44, 0x41, 0x32, 0x6B, 0x41, 0x4D, 0x47, 0x59, 0x43, 0x4D, 0x51, 0x43, 0x70, 0x4B, 0x6A, 0x41, 0x64, 0x30, 0x4D, 0x4B, 0x66, 0x6B, 0x46, 0x46, 0x52, 0x51, 0x44, 0x36, 0x56, 0x56, 0x43, 0x48, 0x4E, 0x46, 0x6D, 0x62, 0x33, + 0x55, 0x32, 0x77, 0x49, 0x46, 0x6A, 0x6E, 0x51, 0x45, 0x6E, 0x78, 0x2F, 0x59, 0x78, 0x76, 0x66, 0x34, 0x7A, 0x67, 0x41, 0x4F, 0x64, 0x6B, 0x74, 0x55, 0x79, 0x42, 0x46, 0x43, 0x78, 0x78, 0x67, 0x0A, 0x5A, 0x7A, 0x46, 0x44, 0x4A, 0x65, 0x30, + 0x43, 0x4D, 0x51, 0x43, 0x53, 0x69, 0x61, 0x37, 0x70, 0x58, 0x47, 0x4B, 0x44, 0x59, 0x6D, 0x48, 0x35, 0x4C, 0x56, 0x65, 0x72, 0x56, 0x72, 0x6B, 0x52, 0x33, 0x53, 0x57, 0x2B, 0x61, 0x6B, 0x35, 0x4B, 0x47, 0x6F, 0x4A, 0x72, 0x33, 0x4D, 0x2F, + 0x54, 0x76, 0x45, 0x71, 0x7A, 0x50, 0x4E, 0x63, 0x75, 0x6D, 0x39, 0x76, 0x34, 0x4B, 0x47, 0x6D, 0x38, 0x61, 0x79, 0x33, 0x73, 0x4D, 0x61, 0x45, 0x36, 0x34, 0x31, 0x63, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x0A, 0x20, 0x4F, 0x49, 0x53, 0x54, 0x45, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x52, 0x53, 0x41, 0x20, + 0x47, 0x31, 0x0A, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, + 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x4D, 0x49, 0x49, 0x46, 0x67, 0x7A, 0x43, 0x43, 0x41, 0x32, 0x75, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x51, 0x56, 0x61, 0x58, + 0x5A, 0x5A, 0x35, 0x51, 0x6F, 0x78, 0x75, 0x30, 0x4D, 0x2B, 0x69, 0x66, 0x64, 0x57, 0x77, 0x46, 0x4E, 0x47, 0x44, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x44, 0x42, + 0x4C, 0x4D, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x0A, 0x45, 0x77, 0x4A, 0x44, 0x53, 0x44, 0x45, 0x5A, 0x4D, 0x42, 0x63, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x67, 0x77, 0x51, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, + 0x55, 0x67, 0x52, 0x6D, 0x39, 0x31, 0x62, 0x6D, 0x52, 0x68, 0x64, 0x47, 0x6C, 0x76, 0x62, 0x6A, 0x45, 0x68, 0x4D, 0x42, 0x38, 0x47, 0x41, 0x31, 0x55, 0x45, 0x41, 0x77, 0x77, 0x59, 0x54, 0x30, 0x6C, 0x54, 0x56, 0x45, 0x55, 0x67, 0x55, 0x32, + 0x56, 0x79, 0x64, 0x6D, 0x56, 0x79, 0x49, 0x46, 0x4A, 0x76, 0x0A, 0x62, 0x33, 0x51, 0x67, 0x55, 0x6C, 0x4E, 0x42, 0x49, 0x45, 0x63, 0x78, 0x4D, 0x42, 0x34, 0x58, 0x44, 0x54, 0x49, 0x7A, 0x4D, 0x44, 0x55, 0x7A, 0x4D, 0x54, 0x45, 0x30, 0x4D, + 0x7A, 0x63, 0x78, 0x4E, 0x6C, 0x6F, 0x58, 0x44, 0x54, 0x51, 0x34, 0x4D, 0x44, 0x55, 0x79, 0x4E, 0x44, 0x45, 0x30, 0x4D, 0x7A, 0x63, 0x78, 0x4E, 0x56, 0x6F, 0x77, 0x53, 0x7A, 0x45, 0x4C, 0x4D, 0x41, 0x6B, 0x47, 0x41, 0x31, 0x55, 0x45, 0x42, + 0x68, 0x4D, 0x43, 0x51, 0x30, 0x67, 0x78, 0x0A, 0x47, 0x54, 0x41, 0x58, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x6F, 0x4D, 0x45, 0x45, 0x39, 0x4A, 0x55, 0x31, 0x52, 0x46, 0x49, 0x45, 0x5A, 0x76, 0x64, 0x57, 0x35, 0x6B, 0x59, 0x58, 0x52, 0x70, + 0x62, 0x32, 0x34, 0x78, 0x49, 0x54, 0x41, 0x66, 0x42, 0x67, 0x4E, 0x56, 0x42, 0x41, 0x4D, 0x4D, 0x47, 0x45, 0x39, 0x4A, 0x55, 0x31, 0x52, 0x46, 0x49, 0x46, 0x4E, 0x6C, 0x63, 0x6E, 0x5A, 0x6C, 0x63, 0x69, 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, + 0x49, 0x46, 0x4A, 0x54, 0x0A, 0x51, 0x53, 0x42, 0x48, 0x4D, 0x54, 0x43, 0x43, 0x41, 0x69, 0x49, 0x77, 0x44, 0x51, 0x59, 0x4A, 0x4B, 0x6F, 0x5A, 0x49, 0x68, 0x76, 0x63, 0x4E, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x49, + 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x67, 0x6F, 0x43, 0x67, 0x67, 0x49, 0x42, 0x41, 0x4B, 0x71, 0x75, 0x39, 0x4B, 0x75, 0x43, 0x7A, 0x2F, 0x76, 0x6C, 0x4E, 0x77, 0x76, 0x6E, 0x31, 0x5A, 0x61, 0x74, 0x6B, 0x4F, 0x68, 0x4C, 0x4B, 0x64, 0x78, + 0x56, 0x0A, 0x59, 0x4F, 0x50, 0x4D, 0x76, 0x4C, 0x4F, 0x38, 0x4C, 0x5A, 0x4B, 0x35, 0x35, 0x4B, 0x4E, 0x36, 0x38, 0x59, 0x47, 0x30, 0x6E, 0x6E, 0x4A, 0x79, 0x51, 0x39, 0x38, 0x2F, 0x71, 0x77, 0x73, 0x6D, 0x74, 0x4F, 0x35, 0x37, 0x47, 0x6D, + 0x6E, 0x37, 0x4B, 0x4E, 0x42, 0x79, 0x58, 0x45, 0x70, 0x74, 0x61, 0x5A, 0x6E, 0x77, 0x59, 0x78, 0x34, 0x4D, 0x30, 0x72, 0x48, 0x2F, 0x31, 0x6F, 0x77, 0x30, 0x30, 0x4F, 0x37, 0x62, 0x72, 0x45, 0x69, 0x35, 0x36, 0x72, 0x41, 0x55, 0x0A, 0x6A, + 0x74, 0x67, 0x48, 0x71, 0x53, 0x53, 0x59, 0x33, 0x65, 0x6B, 0x4A, 0x76, 0x71, 0x67, 0x69, 0x47, 0x31, 0x6B, 0x35, 0x30, 0x53, 0x65, 0x48, 0x33, 0x42, 0x7A, 0x4E, 0x2B, 0x50, 0x75, 0x7A, 0x36, 0x2B, 0x6D, 0x54, 0x65, 0x4F, 0x30, 0x50, 0x7A, + 0x6A, 0x64, 0x38, 0x4A, 0x6E, 0x64, 0x75, 0x6F, 0x64, 0x67, 0x73, 0x49, 0x55, 0x7A, 0x6B, 0x69, 0x6B, 0x2F, 0x48, 0x45, 0x7A, 0x78, 0x75, 0x78, 0x39, 0x55, 0x54, 0x6C, 0x37, 0x4B, 0x6F, 0x32, 0x79, 0x52, 0x70, 0x0A, 0x67, 0x31, 0x62, 0x54, + 0x61, 0x63, 0x75, 0x43, 0x45, 0x72, 0x75, 0x64, 0x47, 0x2F, 0x4C, 0x34, 0x4E, 0x50, 0x4B, 0x59, 0x4B, 0x79, 0x71, 0x4F, 0x42, 0x47, 0x66, 0x32, 0x34, 0x34, 0x65, 0x68, 0x48, 0x61, 0x31, 0x75, 0x7A, 0x6A, 0x5A, 0x30, 0x44, 0x6C, 0x34, 0x7A, + 0x4F, 0x38, 0x76, 0x62, 0x55, 0x5A, 0x65, 0x55, 0x61, 0x70, 0x55, 0x38, 0x7A, 0x68, 0x68, 0x61, 0x62, 0x6B, 0x76, 0x47, 0x2F, 0x41, 0x65, 0x50, 0x4C, 0x68, 0x71, 0x35, 0x53, 0x76, 0x64, 0x6B, 0x0A, 0x4E, 0x43, 0x6E, 0x63, 0x70, 0x6F, 0x31, + 0x51, 0x34, 0x59, 0x32, 0x4C, 0x53, 0x2B, 0x56, 0x49, 0x47, 0x32, 0x34, 0x75, 0x67, 0x42, 0x41, 0x2F, 0x35, 0x4A, 0x38, 0x62, 0x5A, 0x54, 0x38, 0x52, 0x74, 0x4F, 0x70, 0x58, 0x61, 0x5A, 0x2B, 0x30, 0x41, 0x4F, 0x75, 0x46, 0x4A, 0x4A, 0x6B, + 0x6B, 0x39, 0x53, 0x47, 0x64, 0x6C, 0x36, 0x72, 0x37, 0x4E, 0x48, 0x38, 0x43, 0x61, 0x78, 0x57, 0x51, 0x72, 0x62, 0x75, 0x65, 0x57, 0x68, 0x6C, 0x2F, 0x70, 0x49, 0x7A, 0x59, 0x0A, 0x2B, 0x6D, 0x30, 0x6F, 0x2F, 0x44, 0x6A, 0x48, 0x34, 0x30, + 0x79, 0x74, 0x61, 0x73, 0x37, 0x5A, 0x54, 0x70, 0x4F, 0x53, 0x6A, 0x73, 0x77, 0x4D, 0x5A, 0x37, 0x38, 0x4C, 0x53, 0x35, 0x62, 0x4F, 0x5A, 0x6D, 0x64, 0x54, 0x61, 0x4D, 0x73, 0x58, 0x45, 0x59, 0x35, 0x5A, 0x39, 0x36, 0x79, 0x63, 0x47, 0x37, + 0x6D, 0x4F, 0x61, 0x45, 0x53, 0x33, 0x47, 0x4B, 0x2F, 0x6D, 0x35, 0x51, 0x39, 0x6C, 0x33, 0x4A, 0x55, 0x4A, 0x73, 0x4A, 0x4D, 0x53, 0x74, 0x52, 0x38, 0x2B, 0x0A, 0x6C, 0x4B, 0x58, 0x48, 0x69, 0x48, 0x55, 0x68, 0x73, 0x64, 0x34, 0x4A, 0x4A, + 0x43, 0x70, 0x4D, 0x34, 0x72, 0x7A, 0x73, 0x54, 0x47, 0x64, 0x48, 0x77, 0x69, 0x6D, 0x49, 0x75, 0x51, 0x71, 0x36, 0x2B, 0x63, 0x46, 0x30, 0x7A, 0x6F, 0x77, 0x59, 0x4A, 0x6D, 0x58, 0x61, 0x39, 0x32, 0x2F, 0x47, 0x6A, 0x48, 0x74, 0x6F, 0x58, + 0x41, 0x76, 0x75, 0x59, 0x38, 0x42, 0x65, 0x53, 0x2F, 0x46, 0x4F, 0x7A, 0x4A, 0x38, 0x76, 0x44, 0x2B, 0x48, 0x6F, 0x6D, 0x6E, 0x71, 0x54, 0x0A, 0x38, 0x65, 0x44, 0x49, 0x32, 0x37, 0x38, 0x6E, 0x35, 0x6D, 0x55, 0x70, 0x65, 0x7A, 0x62, 0x67, + 0x4D, 0x78, 0x56, 0x7A, 0x38, 0x70, 0x31, 0x72, 0x68, 0x41, 0x68, 0x6F, 0x4B, 0x7A, 0x59, 0x48, 0x4B, 0x79, 0x66, 0x4D, 0x65, 0x4E, 0x68, 0x71, 0x68, 0x77, 0x35, 0x48, 0x64, 0x50, 0x53, 0x71, 0x6F, 0x42, 0x4E, 0x64, 0x5A, 0x48, 0x37, 0x30, + 0x32, 0x78, 0x53, 0x75, 0x2B, 0x7A, 0x72, 0x6B, 0x4C, 0x38, 0x46, 0x6C, 0x34, 0x37, 0x6C, 0x36, 0x51, 0x47, 0x7A, 0x77, 0x0A, 0x42, 0x72, 0x64, 0x37, 0x4B, 0x4A, 0x76, 0x58, 0x34, 0x56, 0x38, 0x34, 0x63, 0x35, 0x53, 0x73, 0x32, 0x58, 0x43, + 0x54, 0x4C, 0x64, 0x79, 0x45, 0x72, 0x30, 0x59, 0x63, 0x6F, 0x6E, 0x6F, 0x73, 0x50, 0x34, 0x45, 0x6D, 0x51, 0x75, 0x66, 0x55, 0x32, 0x4D, 0x56, 0x73, 0x68, 0x47, 0x59, 0x52, 0x69, 0x33, 0x64, 0x72, 0x56, 0x42, 0x79, 0x6A, 0x74, 0x64, 0x67, + 0x51, 0x38, 0x4B, 0x34, 0x70, 0x39, 0x32, 0x63, 0x49, 0x69, 0x42, 0x64, 0x63, 0x75, 0x4A, 0x64, 0x35, 0x0A, 0x7A, 0x2B, 0x6F, 0x72, 0x4B, 0x75, 0x35, 0x59, 0x4D, 0x2B, 0x56, 0x74, 0x36, 0x53, 0x6D, 0x71, 0x5A, 0x51, 0x45, 0x4E, 0x67, 0x68, + 0x50, 0x73, 0x4A, 0x51, 0x74, 0x64, 0x4C, 0x45, 0x42, 0x79, 0x46, 0x53, 0x6E, 0x54, 0x6B, 0x43, 0x7A, 0x33, 0x47, 0x6B, 0x50, 0x56, 0x61, 0x76, 0x42, 0x70, 0x41, 0x67, 0x4D, 0x42, 0x41, 0x41, 0x47, 0x6A, 0x59, 0x7A, 0x42, 0x68, 0x4D, 0x41, + 0x38, 0x47, 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2F, 0x77, 0x51, 0x46, 0x0A, 0x4D, 0x41, 0x4D, 0x42, 0x41, 0x66, 0x38, 0x77, 0x48, 0x77, 0x59, 0x44, 0x56, 0x52, 0x30, 0x6A, 0x42, 0x42, 0x67, 0x77, 0x46, 0x6F, 0x41, 0x55, 0x38, + 0x73, 0x6E, 0x42, 0x44, 0x77, 0x31, 0x6A, 0x41, 0x4C, 0x76, 0x73, 0x52, 0x51, 0x35, 0x4B, 0x48, 0x37, 0x57, 0x78, 0x73, 0x7A, 0x62, 0x4E, 0x44, 0x6F, 0x30, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4F, 0x42, 0x42, 0x59, 0x45, 0x46, + 0x50, 0x4C, 0x4A, 0x77, 0x51, 0x38, 0x4E, 0x59, 0x77, 0x43, 0x37, 0x0A, 0x37, 0x45, 0x55, 0x4F, 0x53, 0x68, 0x2B, 0x31, 0x73, 0x62, 0x4D, 0x32, 0x7A, 0x51, 0x36, 0x4E, 0x4D, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, 0x45, 0x42, + 0x2F, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6A, 0x41, 0x4E, 0x42, 0x67, 0x6B, 0x71, 0x68, 0x6B, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x77, 0x46, 0x41, 0x41, 0x4F, 0x43, 0x41, 0x67, 0x45, 0x41, 0x4E, 0x47, 0x64, 0x35, + 0x73, 0x6A, 0x72, 0x47, 0x35, 0x54, 0x33, 0x33, 0x0A, 0x49, 0x33, 0x4B, 0x35, 0x43, 0x65, 0x2B, 0x53, 0x72, 0x53, 0x63, 0x66, 0x6F, 0x45, 0x34, 0x4B, 0x73, 0x76, 0x58, 0x61, 0x46, 0x77, 0x79, 0x69, 0x68, 0x64, 0x4A, 0x2B, 0x6B, 0x6C, 0x48, + 0x39, 0x46, 0x57, 0x58, 0x58, 0x58, 0x47, 0x74, 0x6B, 0x46, 0x75, 0x36, 0x4B, 0x52, 0x63, 0x6F, 0x4D, 0x51, 0x7A, 0x5A, 0x45, 0x4E, 0x64, 0x6C, 0x2F, 0x2F, 0x6E, 0x6B, 0x36, 0x48, 0x4F, 0x6A, 0x47, 0x35, 0x44, 0x31, 0x72, 0x64, 0x39, 0x51, + 0x68, 0x45, 0x4F, 0x50, 0x32, 0x0A, 0x38, 0x79, 0x42, 0x4F, 0x71, 0x62, 0x36, 0x4A, 0x38, 0x78, 0x79, 0x63, 0x71, 0x64, 0x2B, 0x38, 0x4D, 0x44, 0x6F, 0x58, 0x30, 0x54, 0x4A, 0x44, 0x30, 0x4B, 0x71, 0x4B, 0x63, 0x68, 0x78, 0x52, 0x4B, 0x45, + 0x7A, 0x64, 0x4E, 0x73, 0x6A, 0x6B, 0x4C, 0x57, 0x64, 0x39, 0x6B, 0x59, 0x63, 0x63, 0x6E, 0x62, 0x7A, 0x38, 0x71, 0x79, 0x69, 0x57, 0x58, 0x6D, 0x46, 0x63, 0x75, 0x43, 0x49, 0x7A, 0x47, 0x45, 0x67, 0x57, 0x55, 0x4F, 0x72, 0x4B, 0x4C, 0x2B, + 0x6D, 0x6C, 0x0A, 0x53, 0x64, 0x78, 0x2F, 0x50, 0x4B, 0x51, 0x5A, 0x76, 0x44, 0x61, 0x74, 0x6B, 0x75, 0x4B, 0x35, 0x39, 0x45, 0x76, 0x56, 0x36, 0x77, 0x69, 0x74, 0x35, 0x33, 0x6A, 0x2B, 0x46, 0x38, 0x42, 0x64, 0x68, 0x33, 0x66, 0x6F, 0x5A, + 0x33, 0x64, 0x50, 0x41, 0x47, 0x61, 0x76, 0x39, 0x4C, 0x45, 0x44, 0x4F, 0x72, 0x34, 0x53, 0x66, 0x45, 0x45, 0x31, 0x35, 0x66, 0x53, 0x6D, 0x47, 0x30, 0x65, 0x4C, 0x79, 0x33, 0x6E, 0x33, 0x31, 0x72, 0x38, 0x58, 0x62, 0x6B, 0x35, 0x6C, 0x0A, + 0x38, 0x50, 0x6A, 0x61, 0x56, 0x38, 0x47, 0x55, 0x67, 0x65, 0x56, 0x36, 0x56, 0x67, 0x32, 0x37, 0x52, 0x6E, 0x39, 0x76, 0x6B, 0x66, 0x31, 0x39, 0x35, 0x68, 0x66, 0x6B, 0x67, 0x53, 0x65, 0x37, 0x42, 0x59, 0x68, 0x57, 0x33, 0x53, 0x43, 0x6C, + 0x39, 0x35, 0x67, 0x74, 0x6B, 0x52, 0x6C, 0x70, 0x4D, 0x56, 0x2B, 0x62, 0x4D, 0x50, 0x4B, 0x5A, 0x72, 0x58, 0x4A, 0x41, 0x6C, 0x73, 0x7A, 0x59, 0x64, 0x32, 0x61, 0x62, 0x74, 0x4E, 0x55, 0x4F, 0x73, 0x68, 0x44, 0x2B, 0x0A, 0x46, 0x4B, 0x72, + 0x44, 0x67, 0x48, 0x47, 0x64, 0x50, 0x59, 0x33, 0x6F, 0x66, 0x52, 0x52, 0x73, 0x59, 0x57, 0x53, 0x47, 0x52, 0x71, 0x62, 0x58, 0x56, 0x4D, 0x57, 0x32, 0x31, 0x35, 0x41, 0x57, 0x52, 0x71, 0x57, 0x46, 0x79, 0x70, 0x34, 0x36, 0x34, 0x2B, 0x59, + 0x54, 0x46, 0x72, 0x59, 0x56, 0x49, 0x38, 0x79, 0x70, 0x4B, 0x56, 0x4C, 0x39, 0x41, 0x4D, 0x62, 0x32, 0x6B, 0x49, 0x35, 0x57, 0x6A, 0x34, 0x6B, 0x49, 0x33, 0x5A, 0x61, 0x71, 0x35, 0x74, 0x4E, 0x71, 0x0A, 0x71, 0x59, 0x59, 0x31, 0x39, 0x74, + 0x56, 0x46, 0x65, 0x45, 0x4A, 0x4B, 0x52, 0x76, 0x77, 0x44, 0x79, 0x46, 0x37, 0x59, 0x5A, 0x76, 0x5A, 0x46, 0x5A, 0x53, 0x53, 0x30, 0x76, 0x6F, 0x64, 0x37, 0x56, 0x53, 0x43, 0x64, 0x39, 0x35, 0x32, 0x31, 0x4B, 0x76, 0x79, 0x35, 0x59, 0x68, + 0x6E, 0x4C, 0x62, 0x44, 0x75, 0x76, 0x30, 0x32, 0x30, 0x34, 0x62, 0x4B, 0x74, 0x37, 0x70, 0x68, 0x36, 0x4E, 0x2F, 0x4F, 0x6D, 0x65, 0x2F, 0x6D, 0x73, 0x56, 0x75, 0x64, 0x75, 0x43, 0x0A, 0x6D, 0x73, 0x75, 0x59, 0x33, 0x33, 0x4F, 0x68, 0x6B, + 0x4B, 0x43, 0x67, 0x78, 0x65, 0x44, 0x6F, 0x41, 0x61, 0x69, 0x6A, 0x46, 0x4A, 0x7A, 0x49, 0x77, 0x5A, 0x71, 0x73, 0x46, 0x56, 0x41, 0x7A, 0x6A, 0x65, 0x31, 0x38, 0x4B, 0x6F, 0x74, 0x7A, 0x6C, 0x55, 0x42, 0x44, 0x4A, 0x76, 0x79, 0x42, 0x70, + 0x43, 0x70, 0x66, 0x4F, 0x5A, 0x43, 0x33, 0x4A, 0x38, 0x74, 0x52, 0x64, 0x2F, 0x69, 0x57, 0x6B, 0x78, 0x37, 0x50, 0x38, 0x6E, 0x64, 0x39, 0x48, 0x30, 0x61, 0x54, 0x0A, 0x6F, 0x6C, 0x6B, 0x65, 0x6C, 0x55, 0x54, 0x46, 0x4C, 0x58, 0x56, 0x6B, + 0x73, 0x4E, 0x62, 0x35, 0x34, 0x44, 0x78, 0x70, 0x36, 0x67, 0x53, 0x31, 0x48, 0x41, 0x76, 0x69, 0x52, 0x6B, 0x52, 0x4E, 0x51, 0x7A, 0x75, 0x58, 0x53, 0x58, 0x45, 0x52, 0x76, 0x53, 0x53, 0x32, 0x77, 0x71, 0x31, 0x79, 0x56, 0x41, 0x62, 0x2B, + 0x61, 0x78, 0x6A, 0x35, 0x64, 0x39, 0x73, 0x70, 0x4C, 0x46, 0x4B, 0x65, 0x62, 0x58, 0x64, 0x37, 0x59, 0x76, 0x30, 0x50, 0x54, 0x59, 0x36, 0x59, 0x0A, 0x4D, 0x6A, 0x41, 0x77, 0x63, 0x52, 0x4C, 0x57, 0x4A, 0x54, 0x58, 0x6A, 0x6E, 0x2F, 0x68, + 0x76, 0x6E, 0x4C, 0x58, 0x72, 0x61, 0x68, 0x75, 0x74, 0x36, 0x68, 0x44, 0x54, 0x6C, 0x68, 0x5A, 0x79, 0x42, 0x69, 0x45, 0x6C, 0x78, 0x6B, 0x79, 0x38, 0x6A, 0x33, 0x43, 0x37, 0x44, 0x4F, 0x52, 0x65, 0x49, 0x6F, 0x4D, 0x74, 0x30, 0x72, 0x37, + 0x2B, 0x68, 0x56, 0x75, 0x30, 0x35, 0x4C, 0x30, 0x3D, 0x0A, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x45, 0x4E, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x0A, 0x00 +}; +const size_t _accacert_len = sizeof(_accacert); +/*************************** End of file ****************************/ From 1dbec57019a9c8f94dc3a475c925105b2437ec77 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:12:38 +0100 Subject: [PATCH 27/41] RA frontend files --- .../resources/icons/placeholder.png | Bin 0 -> 2762 bytes .../resources/icons/ra-generic-user.png | Bin 0 -> 7916 bytes .../resources/icons/ra-icon.png | Bin 0 -> 81347 bytes .../resources/icons/ra-icon.webp | Bin 0 -> 12338 bytes .../resources/icons/trophy-icon-gray.svg | 10 ++ .../resources/icons/trophy-icon-star.svg | 13 ++ .../resources/icons/trophy-icon.svg | 10 ++ .../qt_sdl/retroachievements/resources/ra.qrc | 16 +++ .../resources/sounds/lbsubmit.wav | Bin 0 -> 131284 bytes .../resources/sounds/message.wav | Bin 0 -> 195772 bytes .../resources/sounds/unlock.wav | Bin 0 -> 195772 bytes .../qt_sdl/toast/AchievementToast.cpp | 55 ++++++++ src/frontend/qt_sdl/toast/AchievementToast.h | 19 +++ src/frontend/qt_sdl/toast/BadgeCache.cpp | 49 +++++++ src/frontend/qt_sdl/toast/BadgeCache.h | 23 ++++ src/frontend/qt_sdl/toast/ToastManager.cpp | 43 ++++++ src/frontend/qt_sdl/toast/ToastManager.h | 19 +++ src/frontend/qt_sdl/toast/ToastOverlay.cpp | 129 ++++++++++++++++++ src/frontend/qt_sdl/toast/ToastOverlay.h | 39 ++++++ 19 files changed, 425 insertions(+) create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/placeholder.png create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/ra-generic-user.png create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.png create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.webp create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-gray.svg create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg create mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg create mode 100644 src/frontend/qt_sdl/retroachievements/resources/ra.qrc create mode 100644 src/frontend/qt_sdl/retroachievements/resources/sounds/lbsubmit.wav create mode 100644 src/frontend/qt_sdl/retroachievements/resources/sounds/message.wav create mode 100644 src/frontend/qt_sdl/retroachievements/resources/sounds/unlock.wav create mode 100644 src/frontend/qt_sdl/toast/AchievementToast.cpp create mode 100644 src/frontend/qt_sdl/toast/AchievementToast.h create mode 100644 src/frontend/qt_sdl/toast/BadgeCache.cpp create mode 100644 src/frontend/qt_sdl/toast/BadgeCache.h create mode 100644 src/frontend/qt_sdl/toast/ToastManager.cpp create mode 100644 src/frontend/qt_sdl/toast/ToastManager.h create mode 100644 src/frontend/qt_sdl/toast/ToastOverlay.cpp create mode 100644 src/frontend/qt_sdl/toast/ToastOverlay.h diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/placeholder.png b/src/frontend/qt_sdl/retroachievements/resources/icons/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..064082d2c8704ef09d71ee17a25367f0ce3743ba GIT binary patch literal 2762 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 literal 0 HcmV?d00001 diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/ra-generic-user.png b/src/frontend/qt_sdl/retroachievements/resources/icons/ra-generic-user.png new file mode 100644 index 0000000000000000000000000000000000000000..3d4a9608be89b25fc98e1c708e6c25b03d3349eb GIT binary patch literal 7916 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 UuIvH#zu17Pl9pnbJSyb>0BYL4F#rGn literal 0 HcmV?d00001 diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.png b/src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..911ffcd891fa9366930b8dad726bc3b2a17a74a2 GIT binary patch literal 81347 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%0D*kpZF& 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 z"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 + + trophy + + + + + + + \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg b/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg new file mode 100644 index 0000000000..db374ea946 --- /dev/null +++ b/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg @@ -0,0 +1,13 @@ + + + trophy + + + + + + + + + + \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg b/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg new file mode 100644 index 0000000000..02b24390a1 --- /dev/null +++ b/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg @@ -0,0 +1,10 @@ + + + trophy + + + + + + + \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/ra.qrc b/src/frontend/qt_sdl/retroachievements/resources/ra.qrc new file mode 100644 index 0000000000..6413e69fbf --- /dev/null +++ b/src/frontend/qt_sdl/retroachievements/resources/ra.qrc @@ -0,0 +1,16 @@ + + + + icons/placeholder.png + icons/ra-generic-user.png + icons/ra-icon.png + icons/trophy-icon.svg + icons/trophy-icon-gray.svg + icons/trophy-icon-star.svg + + + sounds/unlock.wav + sounds/message.wav + sounds/lbsubmit.wav + + \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/sounds/lbsubmit.wav b/src/frontend/qt_sdl/retroachievements/resources/sounds/lbsubmit.wav new file mode 100644 index 0000000000000000000000000000000000000000..d1b15e789a3c738a839feab653621df98521e5b6 GIT binary patch literal 131284 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 literal 0 HcmV?d00001 diff --git a/src/frontend/qt_sdl/retroachievements/resources/sounds/message.wav b/src/frontend/qt_sdl/retroachievements/resources/sounds/message.wav new file mode 100644 index 0000000000000000000000000000000000000000..a71c02709e46eda6b24c4fd81d0003f81df6a3e4 GIT binary patch literal 195772 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

Os-gQ)MDy-Av2&?xt;g@7=cOtiBt)alZ_u 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%0A~AK8Quq$4dW_&9Rq}Y{N(q)d;*a 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 literal 0 HcmV?d00001 diff --git a/src/frontend/qt_sdl/toast/AchievementToast.cpp b/src/frontend/qt_sdl/toast/AchievementToast.cpp new file mode 100644 index 0000000000..c4df7c9a7e --- /dev/null +++ b/src/frontend/qt_sdl/toast/AchievementToast.cpp @@ -0,0 +1,55 @@ +#include "AchievementToast.h" +#include +#include +#include + +AchievementToast::AchievementToast(const QString& title, + const QString& desc, + const QPixmap& icon, + QWidget* parent) + : QWidget(parent) +{ + setFixedWidth(360); + setAttribute(Qt::WA_TranslucentBackground); + + auto* root = new QHBoxLayout(this); + root->setSpacing(12); + root->setContentsMargins(12, 12, 12, 12); + + QLabel* img = new QLabel; + img->setPixmap(icon.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + root->addWidget(img); + + auto* textCol = new QVBoxLayout; + QLabel* titleLbl = new QLabel(title); + QLabel* descLbl = new QLabel(desc); + + titleLbl->setStyleSheet("color: white; font-weight: bold;"); + descLbl->setStyleSheet("color: #DDD;"); + + textCol->addWidget(titleLbl); + textCol->addWidget(descLbl); + root->addLayout(textCol); + + setStyleSheet(R"( + background-color: rgba(20,20,20,230); + border-radius: 8px; + )"); + + m_anim = new QPropertyAnimation(this, "windowOpacity"); + m_anim->setDuration(250); +} + +void AchievementToast::play() +{ + setWindowOpacity(0.0); + m_anim->setStartValue(0.0); + m_anim->setEndValue(1.0); + m_anim->start(); + + QTimer::singleShot(4000, this, [this] { + m_anim->setDirection(QAbstractAnimation::Backward); + m_anim->start(); + connect(m_anim, &QPropertyAnimation::finished, this, &QObject::deleteLater); + }); +} \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/AchievementToast.h b/src/frontend/qt_sdl/toast/AchievementToast.h new file mode 100644 index 0000000000..6607a35a10 --- /dev/null +++ b/src/frontend/qt_sdl/toast/AchievementToast.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include + +class AchievementToast : public QWidget +{ + Q_OBJECT +public: + AchievementToast(const QString& title, + const QString& desc, + const QPixmap& icon, + QWidget* parent = nullptr); + + void play(); + +private: + QPropertyAnimation* m_anim; +}; \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/BadgeCache.cpp b/src/frontend/qt_sdl/toast/BadgeCache.cpp new file mode 100644 index 0000000000..4ed8dfcb60 --- /dev/null +++ b/src/frontend/qt_sdl/toast/BadgeCache.cpp @@ -0,0 +1,49 @@ +#include "BadgeCache.h" +#include +#include + +BadgeCache& BadgeCache::Get() +{ + static BadgeCache inst; + return inst; +} + +BadgeCache::BadgeCache(QObject* parent) + : QObject(parent) +{} + +QPixmap BadgeCache::GetBadge(const QString& url) +{ + if (m_cache.contains(url)) + return m_cache[url]; + + // Jeśli nie ma w cache, pobierz asynchronicznie + DownloadBadge(url, [](const QPixmap&){ /* nic nie robimy tu */ }); + + return QPixmap(":/ra/placeholder.png"); +} + +void BadgeCache::DownloadBadge(const QString& url, std::function callback) +{ + if (m_cache.contains(url)) { + callback(m_cache[url]); + return; + } + + QNetworkRequest req{ QUrl(url) }; + auto* reply = m_net.get(req); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply, url, callback]() { + QPixmap pix; + if (reply->error() == QNetworkReply::NoError) + pix.loadFromData(reply->readAll()); + + if (pix.isNull()) + pix = QPixmap(":/ra/placeholder.png"); + + m_cache[url] = pix; + emit BadgeReady(url, pix); + callback(pix); + reply->deleteLater(); + }); +} \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/BadgeCache.h b/src/frontend/qt_sdl/toast/BadgeCache.h new file mode 100644 index 0000000000..fd63e122bb --- /dev/null +++ b/src/frontend/qt_sdl/toast/BadgeCache.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include +#include +#include + +class BadgeCache : public QObject +{ + Q_OBJECT +public: + static BadgeCache& Get(); + + QPixmap GetBadge(const QString& url); + void DownloadBadge(const QString& url, std::function callback); + +private: + BadgeCache(QObject* parent = nullptr); + QNetworkAccessManager m_net; + QHash m_cache; +signals: + void BadgeReady(const QString& url, const QPixmap& pix); // <-- dodaj tu +}; \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastManager.cpp b/src/frontend/qt_sdl/toast/ToastManager.cpp new file mode 100644 index 0000000000..7f4c34ccf1 --- /dev/null +++ b/src/frontend/qt_sdl/toast/ToastManager.cpp @@ -0,0 +1,43 @@ +#include "ToastManager.h" +#include "toast/ToastOverlay.h" +#include "toast/AchievementToast.h" + +ToastManager& ToastManager::Get() +{ + static ToastManager inst; + return inst; +} + +void ToastManager::Init(QWidget* widget) +{ + if (m_overlay) return; + + // Używamy 'widget' zamiast 'parent' + m_overlay = new ToastOverlay(widget); + + // Ustaw overlay na cały rozmiar okna + m_overlay->setGeometry(widget->rect()); + widget->installEventFilter(m_overlay); + m_overlay->show(); + m_overlay->raise(); // Ważne: musi być na wierzchu! +} +void ToastManager::ShowAchievement(const QString& title, const QString& description, const QPixmap& icon) +{ + // JEŚLI TU JEST CRASH, TO ZNACZY ŻE m_overlay TO NULL + if (!m_overlay) { + printf("ToastManager: Próba pokazania toasta przed Init()!\n"); + return; + } + + m_overlay->ShowToast(title, description, icon); +} +void TestAchievementToast() +{ + QPixmap icon(":/ra/placeholder.png"); + + ToastManager::Get().ShowAchievement( + "Test Achievement", + "To jest testowy toast", + icon + ); +} \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastManager.h b/src/frontend/qt_sdl/toast/ToastManager.h new file mode 100644 index 0000000000..2f84dcfbd9 --- /dev/null +++ b/src/frontend/qt_sdl/toast/ToastManager.h @@ -0,0 +1,19 @@ +#pragma once +#include + +class ToastOverlay; + +class ToastManager : public QObject +{ + Q_OBJECT +public: + static ToastManager& Get(); + + void Init(QWidget* screenWidget); + void ShowAchievement(const QString& title, + const QString& desc, + const QPixmap& icon); + +private: + ToastOverlay* m_overlay = nullptr; +}; \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastOverlay.cpp b/src/frontend/qt_sdl/toast/ToastOverlay.cpp new file mode 100644 index 0000000000..ed4c61b81f --- /dev/null +++ b/src/frontend/qt_sdl/toast/ToastOverlay.cpp @@ -0,0 +1,129 @@ +#include "ToastOverlay.h" +#include +#include +#include +#include +#include + +// --- IMPLEMENTACJA ToastWidget --- + +ToastWidget::ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent) + : QWidget(parent) +{ + // Prosty layout dla pojedynczego toasta + auto* layout = new QHBoxLayout(this); + layout->setContentsMargins(10, 10, 10, 10); + + auto* iconLabel = new QLabel; + iconLabel->setPixmap(icon.scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + layout->addWidget(iconLabel); + + auto* textLayout = new QVBoxLayout; + auto* titleLabel = new QLabel(title); + titleLabel->setStyleSheet("font-weight: bold; color: white;"); + auto* descLabel = new QLabel(description); + descLabel->setStyleSheet("color: #ddd;"); + + textLayout->addWidget(titleLabel); + textLayout->addWidget(descLabel); + layout->addLayout(textLayout); + + setAttribute(Qt::WA_StyledBackground, false); + setAttribute(Qt::WA_TranslucentBackground); +} + +QSize ToastWidget::sizeHint() const +{ + // Zwracamy sugerowany rozmiar + return layout()->sizeHint(); +} + +void ToastWidget::paintEvent(QPaintEvent*) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setBrush(QColor(40, 40, 40, 230)); // Ciemne tło + p.setPen(Qt::NoPen); + p.drawRoundedRect(rect(), 8, 8); +} + +// --- IMPLEMENTACJA ToastOverlay --- + +ToastOverlay::ToastOverlay(QWidget* parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_TranslucentBackground); + + m_soundEffect.setSource(QUrl("qrc:/ra/sounds/unlock.wav")); + m_soundEffect.setVolume(0.7f); // 70% głośności +} + +void ToastOverlay::ShowToast(const QString& title, const QString& description, const QPixmap& icon) +{ + auto* toast = new ToastWidget(title, description, icon, this); + toast->show(); + m_toasts.append(toast); + + m_soundEffect.play(); + + RepositionToasts(); + + // Auto-usuwanie po 3 sekundach + QTimer::singleShot(3000, toast, [this, toast]() { + m_toasts.removeOne(toast); + toast->deleteLater(); + RepositionToasts(); + }); +} + +void ToastOverlay::resizeEvent(QResizeEvent* event) +{ + // Gdy okno zmienia rozmiar, overlay też musi + if (parentWidget()) + setGeometry(parentWidget()->rect()); + + RepositionToasts(); + QWidget::resizeEvent(event); +} + +void ToastOverlay::paintEvent(QPaintEvent*) +{ + // Overlay jest przezroczysty, nic nie rysuje +} + +void ToastOverlay::RepositionToasts() +{ + if (!parentWidget()) + return; + + const int margin = 20; + const int topOffset = 60; // 👈 TO JEST KLUCZ (menu height buffer) + + int y = topOffset; + const int rightEdge = width() - margin; + + for (auto* toast : m_toasts) + { + toast->adjustSize(); + + int x = rightEdge - toast->width(); + toast->move(x, y); + + y += toast->height() + 10; + } +} +bool ToastOverlay::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == parentWidget()) + { + if (event->type() == QEvent::Resize || + event->type() == QEvent::WindowStateChange) + { + setGeometry(parentWidget()->rect()); + RepositionToasts(); + } + } + + return QWidget::eventFilter(obj, event); +} \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastOverlay.h b/src/frontend/qt_sdl/toast/ToastOverlay.h new file mode 100644 index 0000000000..2791ae7a48 --- /dev/null +++ b/src/frontend/qt_sdl/toast/ToastOverlay.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +// 1. Definicja klasy ToastWidget (pojedynczy dymek) +class ToastWidget : public QWidget +{ + Q_OBJECT +public: + ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent = nullptr); + QSize sizeHint() const override; // To naprawia błąd sizeHint + +protected: + void paintEvent(QPaintEvent* event) override; +}; + +// 2. Definicja klasy ToastOverlay (warstwa zarządzająca) +class ToastOverlay : public QWidget +{ + Q_OBJECT +public: + explicit ToastOverlay(QWidget* parent = nullptr); // Tylko jedna deklaracja! + void ShowToast(const QString& title, const QString& description, const QPixmap& icon); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void resizeEvent(QResizeEvent* event) override; // To naprawia błąd resizeEvent + void paintEvent(QPaintEvent* event) override; + +private: + QList m_toasts; + void RepositionToasts(); + QSoundEffect m_soundEffect; +}; \ No newline at end of file From e9a02115ef96a50525273a8eb87b27b8ca5e1fbb Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:14:45 +0100 Subject: [PATCH 28/41] Rcheevos files --- src/rcheevos/test/Makefile | 224 + src/rcheevos/test/libretro.h | 205 + src/rcheevos/test/rapi/test_rc_api_common.c | 941 ++ src/rcheevos/test/rapi/test_rc_api_editor.c | 931 ++ src/rcheevos/test/rapi/test_rc_api_info.c | 537 + src/rcheevos/test/rapi/test_rc_api_runtime.c | 2213 ++++ src/rcheevos/test/rapi/test_rc_api_user.c | 998 ++ src/rcheevos/test/rcheevos-test.sln | 46 + src/rcheevos/test/rcheevos-test.vcxproj | 239 + .../test/rcheevos-test.vcxproj.filters | 335 + src/rcheevos/test/rcheevos/mock_memory.h | 32 + src/rcheevos/test/rcheevos/test_condition.c | 570 + src/rcheevos/test/rcheevos/test_condset.c | 5170 ++++++++ src/rcheevos/test/rcheevos/test_consoleinfo.c | 203 + src/rcheevos/test/rcheevos/test_format.c | 112 + src/rcheevos/test/rcheevos/test_lboard.c | 746 ++ src/rcheevos/test/rcheevos/test_memref.c | 520 + src/rcheevos/test/rcheevos/test_operand.c | 692 ++ src/rcheevos/test/rcheevos/test_rc_validate.c | 500 + .../test/rcheevos/test_richpresence.c | 1542 +++ src/rcheevos/test/rcheevos/test_runtime.c | 1667 +++ .../test/rcheevos/test_runtime_progress.c | 1821 +++ src/rcheevos/test/rcheevos/test_timing.c | 166 + src/rcheevos/test/rcheevos/test_trigger.c | 2492 ++++ src/rcheevos/test/rcheevos/test_value.c | 868 ++ src/rcheevos/test/rhash/data.c | 657 + src/rcheevos/test/rhash/data.h | 32 + src/rcheevos/test/rhash/mock_filereader.c | 236 + src/rcheevos/test/rhash/mock_filereader.h | 31 + src/rcheevos/test/rhash/test_cdreader.c | 920 ++ src/rcheevos/test/rhash/test_hash.c | 310 + src/rcheevos/test/rhash/test_hash_disc.c | 1450 +++ src/rcheevos/test/rhash/test_hash_rom.c | 899 ++ src/rcheevos/test/rhash/test_hash_zip.c | 551 + src/rcheevos/test/test.c | 113 + src/rcheevos/test/test_framework.h | 205 + src/rcheevos/test/test_rc_client.c | 10329 ++++++++++++++++ src/rcheevos/test/test_rc_client_external.c | 2170 ++++ .../test/test_rc_client_raintegration.c | 441 + src/rcheevos/test/test_rc_libretro.c | 952 ++ src/rcheevos/test/test_types.natvis | 9 + src/rcheevos/validator/Makefile | 25 + src/rcheevos/validator/validator.c | 658 + src/rcheevos/validator/validator.vcxproj | 152 + .../validator/validator.vcxproj.filters | 82 + 45 files changed, 43992 insertions(+) create mode 100644 src/rcheevos/test/Makefile create mode 100644 src/rcheevos/test/libretro.h create mode 100644 src/rcheevos/test/rapi/test_rc_api_common.c create mode 100644 src/rcheevos/test/rapi/test_rc_api_editor.c create mode 100644 src/rcheevos/test/rapi/test_rc_api_info.c create mode 100644 src/rcheevos/test/rapi/test_rc_api_runtime.c create mode 100644 src/rcheevos/test/rapi/test_rc_api_user.c create mode 100644 src/rcheevos/test/rcheevos-test.sln create mode 100644 src/rcheevos/test/rcheevos-test.vcxproj create mode 100644 src/rcheevos/test/rcheevos-test.vcxproj.filters create mode 100644 src/rcheevos/test/rcheevos/mock_memory.h create mode 100644 src/rcheevos/test/rcheevos/test_condition.c create mode 100644 src/rcheevos/test/rcheevos/test_condset.c create mode 100644 src/rcheevos/test/rcheevos/test_consoleinfo.c create mode 100644 src/rcheevos/test/rcheevos/test_format.c create mode 100644 src/rcheevos/test/rcheevos/test_lboard.c create mode 100644 src/rcheevos/test/rcheevos/test_memref.c create mode 100644 src/rcheevos/test/rcheevos/test_operand.c create mode 100644 src/rcheevos/test/rcheevos/test_rc_validate.c create mode 100644 src/rcheevos/test/rcheevos/test_richpresence.c create mode 100644 src/rcheevos/test/rcheevos/test_runtime.c create mode 100644 src/rcheevos/test/rcheevos/test_runtime_progress.c create mode 100644 src/rcheevos/test/rcheevos/test_timing.c create mode 100644 src/rcheevos/test/rcheevos/test_trigger.c create mode 100644 src/rcheevos/test/rcheevos/test_value.c create mode 100644 src/rcheevos/test/rhash/data.c create mode 100644 src/rcheevos/test/rhash/data.h create mode 100644 src/rcheevos/test/rhash/mock_filereader.c create mode 100644 src/rcheevos/test/rhash/mock_filereader.h create mode 100644 src/rcheevos/test/rhash/test_cdreader.c create mode 100644 src/rcheevos/test/rhash/test_hash.c create mode 100644 src/rcheevos/test/rhash/test_hash_disc.c create mode 100644 src/rcheevos/test/rhash/test_hash_rom.c create mode 100644 src/rcheevos/test/rhash/test_hash_zip.c create mode 100644 src/rcheevos/test/test.c create mode 100644 src/rcheevos/test/test_framework.h create mode 100644 src/rcheevos/test/test_rc_client.c create mode 100644 src/rcheevos/test/test_rc_client_external.c create mode 100644 src/rcheevos/test/test_rc_client_raintegration.c create mode 100644 src/rcheevos/test/test_rc_libretro.c create mode 100644 src/rcheevos/test/test_types.natvis create mode 100644 src/rcheevos/validator/Makefile create mode 100644 src/rcheevos/validator/validator.c create mode 100644 src/rcheevos/validator/validator.vcxproj create mode 100644 src/rcheevos/validator/validator.vcxproj.filters diff --git a/src/rcheevos/test/Makefile b/src/rcheevos/test/Makefile new file mode 100644 index 0000000000..2abac55f3d --- /dev/null +++ b/src/rcheevos/test/Makefile @@ -0,0 +1,224 @@ +# supported parameters +# ARCH architecture - "x86" or "x64" [detected if not set] +# BUILD use flags for specified upstream consumer - "c89" or "retroarch" [default to "c89"] +# DEBUG if set to anything, builds with DEBUG symbols +# HAVE_HASH if set to 0, excludes all hash functionality +# HAVE_HASH_ROM if set to 0, excludes rom hash generation +# HAVE_HASH_DISC if set to 0, excludes disc hash generation +# HAVE_HASH_ZIP if set to 0, excludes zip hash generation +# HAVE_HASH_ENCRYPTED if set to 0, excludes encrypted hash generation + +RC_SRC=../src +RC_CHEEVOS_SRC=$(RC_SRC)/rcheevos +RC_HASH_SRC=$(RC_SRC)/rhash +RC_API_SRC=$(RC_SRC)/rapi + +# default parameter values +ifeq ($(ARCH),) + UNAME := $(shell uname -s) + ifeq ($(findstring MINGW64, $(UNAME)), MINGW64) + ARCH=x64 + else ifeq ($(findstring MINGW32, $(UNAME)), MINGW32) + ARCH=x86 + else + $(error Could not determine ARCH) + endif +endif + +ifeq ($(BUILD),) + BUILD=c89 +endif + +# OS specific stuff +ifeq ($(OS),Windows_NT) + EXE=.exe +else ifeq ($(findstring mingw, $(CC)), mingw) + EXE=.exe +else + EXE= +endif + +# source files +OBJ=$(RC_SRC)/rc_compat.o \ + $(RC_SRC)/rc_client.o \ + $(RC_SRC)/rc_util.o \ + $(RC_SRC)/rc_version.o \ + $(RC_CHEEVOS_SRC)/alloc.o \ + $(RC_CHEEVOS_SRC)/condition.o \ + $(RC_CHEEVOS_SRC)/condset.o \ + $(RC_CHEEVOS_SRC)/consoleinfo.o \ + $(RC_CHEEVOS_SRC)/format.o \ + $(RC_CHEEVOS_SRC)/lboard.o \ + $(RC_CHEEVOS_SRC)/memref.o \ + $(RC_CHEEVOS_SRC)/operand.o \ + $(RC_CHEEVOS_SRC)/rc_validate.o \ + $(RC_CHEEVOS_SRC)/richpresence.o \ + $(RC_CHEEVOS_SRC)/runtime.o \ + $(RC_CHEEVOS_SRC)/runtime_progress.o \ + $(RC_CHEEVOS_SRC)/trigger.o \ + $(RC_CHEEVOS_SRC)/value.o \ + $(RC_HASH_SRC)/md5.o \ + $(RC_API_SRC)/rc_api_common.o \ + $(RC_API_SRC)/rc_api_editor.o \ + $(RC_API_SRC)/rc_api_info.o \ + $(RC_API_SRC)/rc_api_runtime.o \ + $(RC_API_SRC)/rc_api_user.o \ + rcheevos/test_condition.o \ + rcheevos/test_condset.o \ + rcheevos/test_consoleinfo.o \ + rcheevos/test_format.o \ + rcheevos/test_lboard.o \ + rcheevos/test_memref.o \ + rcheevos/test_operand.o \ + rcheevos/test_rc_validate.o \ + rcheevos/test_richpresence.o \ + rcheevos/test_runtime.o \ + rcheevos/test_runtime_progress.o \ + rcheevos/test_timing.o \ + rcheevos/test_trigger.o \ + rcheevos/test_value.o \ + rapi/test_rc_api_common.o \ + rapi/test_rc_api_editor.o \ + rapi/test_rc_api_info.o \ + rapi/test_rc_api_runtime.o \ + rapi/test_rc_api_user.o \ + test_rc_client.o \ + test.o + +# compile flags +CFLAGS=-Wall -Werror -Wno-long-long -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 +INCLUDES=-I../include -I$(RC_CHEEVOS_SRC) -I. + +ifeq ($(ARCH), x86) + CFLAGS += -m32 + LDFLAGS += -m32 +else ifeq ($(ARCH), x64) + CFLAGS += -m64 + LDFLAGS += -m64 +else + $(error unknown ARCH "$(ARCH)") +endif + +EXTRA= +ifdef DEBUG + CFLAGS += -O0 -g + EXTRA += DEBUG +else + CFLAGS += -O3 +endif + +# more strict validation for source files to eliminate warnings/errors in upstream consumers +# -Wextra includes -Wsign-compare -Wtype-limits -Wimplicit-fallthrough=3 -Wunused-parameter and more +# -pedantic issues errors when using GNU extensions or other things that are not strictly ISO C +SRC_CFLAGS=-Wextra -Wshadow -pedantic +# 3DS build (retroarch) doesn't support signed char +SRC_CFLAGS += -fno-signed-char + +ifeq ($(BUILD), c89) + CFLAGS += -std=c89 +else ifeq ($(BUILD), c99) + CFLAGS += -std=c99 +else ifeq ($(BUILD), retroarch) + # RetroArch builds with gcc8 and gnu99 which adds some extra warning validations + SRC_CFLAGS += -std=gnu99 -D_GNU_SOURCE -Wall -Warray-bounds=2 -Wincompatible-pointer-types + # Also include the RC_CLIENT_SUPPORTS_EXTERNAL flag to ensure we do the full level of validation on the + # most code. The c89 build will ensure things still build without the RC_CLIENT_SUPPORTS_EXTERNAL flag. + CFLAGS += -DRC_CLIENT_SUPPORTS_EXTERNAL + OBJ += $(RC_SRC)/rc_client_external.o test_rc_client_external.o +else + $(error unknown BUILD "$(BUILD)") +endif + +# support for building without hash subdirectory +ifeq ($(HAVE_HASH), 0) + EXTRA += |NO_HASH +else + CFLAGS += -DRC_CLIENT_SUPPORTS_HASH + OBJ += $(RC_HASH_SRC)/hash.o \ + $(RC_SRC)/rc_libretro.o \ + rhash/data.o \ + rhash/mock_filereader.o \ + rhash/test_hash.o \ + test_rc_libretro.o + + ifeq ($(HAVE_HASH_ROM), 0) + EXTRA += |NO_HASH_ROM + CFLAGS += -DRC_HASH_NO_ROM + else + OBJ += $(RC_HASH_SRC)/hash_rom.o \ + rhash/test_hash_rom.o + endif + + ifeq ($(HAVE_HASH_DISC), 0) + EXTRA += |NO_HASH_DISC + CFLAGS += -DRC_HASH_NO_DISC + else + OBJ += $(RC_HASH_SRC)/cdreader.o \ + $(RC_HASH_SRC)/hash_disc.o \ + rhash/test_cdreader.o \ + rhash/test_hash_disc.o + endif + + ifeq ($(HAVE_HASH_ZIP), 0) + EXTRA += |NO_HASH_ZIP + CFLAGS += -DRC_HASH_NO_ZIP + else + OBJ += $(RC_HASH_SRC)/hash_zip.o \ + rhash/test_hash_zip.o + endif + + ifeq ($(HAVE_HASH_ENCRYPTED), 0) + EXTRA += |NO_HASH_ENCRYPTED + CFLAGS += -DRC_HASH_NO_ENCRYPTED + else + OBJ += $(RC_HASH_SRC)/hash_encrypted.o \ + $(RC_HASH_SRC)/aes.o + endif +endif + +# recipes +$(info ==== rcheevos test [$(BUILD)/$(ARCH)$(EXTRA)] ====) + +all: test + +$(RC_SRC)/%.o: $(RC_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(RC_CHEEVOS_SRC)/%.o: $(RC_CHEEVOS_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(RC_HASH_SRC)/%.o: $(RC_HASH_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +$(RC_API_SRC)/%.o: $(RC_API_SRC)/%.c + $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ + +%.o: %.c + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +test: $(OBJ) + $(CC) $(LDFLAGS) -o $@$(EXE) $+ -lm + @echo --------------------------------- + +check_ctype: + @if grep -rnI "isalpha([^(u]" ../src/*; \ + then echo "*** Error: isalpha without unsigned char cast" && false; \ + fi + @if grep -rnI "isalnum([^(u]" ../src/*; \ + then echo "*** Error: isalnum without unsigned char cast" && false; \ + fi + @if grep -rnI "isdigit([^(u]" ../src/*; \ + then echo "*** Error: isdigit without unsigned char cast" && false; \ + fi + @if grep -rnI "isspace([^(u]" ../src/*; \ + then echo "*** Error: isspace without unsigned char cast" && false; \ + fi + +runtests: test + @./test$(EXE) + +valgrind: test + @valgrind --leak-check=full --error-exitcode=1 ./test$(EXE) + +clean: + rm -f test$(EXE) $(OBJ) diff --git a/src/rcheevos/test/libretro.h b/src/rcheevos/test/libretro.h new file mode 100644 index 0000000000..30009f5056 --- /dev/null +++ b/src/rcheevos/test/libretro.h @@ -0,0 +1,205 @@ +/** + * Provides copies of structures and constants from + * https://github.com/libretro/RetroArch/blob/master/libretro-common/include/libretro.h + * for unit testing without pulling in the entire libretro project. + */ + +#ifndef LIBRETRO_H__ +#define LIBRETRO_H__ + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Regular save RAM. This RAM is usually found on a game cartridge, + * backed up by a battery. + * If save game data is too complex for a single memory buffer, + * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment + * callback can be used. */ +#define RETRO_MEMORY_SAVE_RAM 0 + +/* Some games have a built-in clock to keep track of time. + * This memory is usually just a couple of bytes to keep track of time. + */ +#define RETRO_MEMORY_RTC 1 + +/* System ram lets a frontend peek into a game systems main RAM. */ +#define RETRO_MEMORY_SYSTEM_RAM 2 + +/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */ +#define RETRO_MEMORY_VIDEO_RAM 3 + + +#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ +#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ +#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */ +#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */ +#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */ +#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ +#define RETRO_MEMDESC_ALIGN_4 (2 << 16) +#define RETRO_MEMDESC_ALIGN_8 (3 << 16) +#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ +#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) +#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) +struct retro_memory_descriptor +{ + uint64_t flags; + + /* Pointer to the start of the relevant ROM or RAM chip. + * It's strongly recommended to use 'offset' if possible, rather than + * doing math on the pointer. + * + * If the same byte is mapped my multiple descriptors, their descriptors + * must have the same pointer. + * If 'start' does not point to the first byte in the pointer, put the + * difference in 'offset' instead. + * + * May be NULL if there's nothing usable here (e.g. hardware registers and + * open bus). No flags should be set if the pointer is NULL. + * It's recommended to minimize the number of descriptors if possible, + * but not mandatory. */ + void *ptr; + size_t offset; + + /* This is the location in the emulated address space + * where the mapping starts. */ + size_t start; + + /* Which bits must be same as in 'start' for this mapping to apply. + * The first memory descriptor to claim a certain byte is the one + * that applies. + * A bit which is set in 'start' must also be set in this. + * Can be zero, in which case each byte is assumed mapped exactly once. + * In this case, 'len' must be a power of two. */ + size_t select; + + /* If this is nonzero, the set bits are assumed not connected to the + * memory chip's address pins. */ + size_t disconnect; + + /* This one tells the size of the current memory area. + * If, after start+disconnect are applied, the address is higher than + * this, the highest bit of the address is cleared. + * + * If the address is still too high, the next highest bit is cleared. + * Can be zero, in which case it's assumed to be infinite (as limited + * by 'select' and 'disconnect'). */ + size_t len; + + /* To go from emulated address to physical address, the following + * order applies: + * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */ + + /* The address space name must consist of only a-zA-Z0-9_-, + * should be as short as feasible (maximum length is 8 plus the NUL), + * and may not be any other address space plus one or more 0-9A-F + * at the end. + * However, multiple memory descriptors for the same address space is + * allowed, and the address space name can be empty. NULL is treated + * as empty. + * + * Address space names are case sensitive, but avoid lowercase if possible. + * The same pointer may exist in multiple address spaces. + * + * Examples: + * blank+blank - valid (multiple things may be mapped in the same namespace) + * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace) + * 'A'+'B' - valid (neither is a prefix of each other) + * 'S'+blank - valid ('S' is not in 0-9A-F) + * 'a'+blank - valid ('a' is not in 0-9A-F) + * 'a'+'A' - valid (neither is a prefix of each other) + * 'AR'+blank - valid ('R' is not in 0-9A-F) + * 'ARB'+blank - valid (the B can't be part of the address either, because + * there is no namespace 'AR') + * blank+'B' - not valid, because it's ambigous which address space B1234 + * would refer to. + * The length can't be used for that purpose; the frontend may want + * to append arbitrary data to an address, without a separator. */ + const char *addrspace; + + /* TODO: When finalizing this one, add a description field, which should be + * "WRAM" or something roughly equally long. */ + + /* TODO: When finalizing this one, replace 'select' with 'limit', which tells + * which bits can vary and still refer to the same address (limit = ~select). + * TODO: limit? range? vary? something else? */ + + /* TODO: When finalizing this one, if 'len' is above what 'select' (or + * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len' + * and 'select' != 0, and the mappings don't tell how the system switches the + * banks. */ + + /* TODO: When finalizing this one, fix the 'len' bit removal order. + * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00. + * Algorithm: Take bits highest to lowest, but if it goes above len, clear + * the most recent addition and continue on the next bit. + * TODO: Can the above be optimized? Is "remove the lowest bit set in both + * pointer and 'len'" equivalent? */ + + /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing + * the emulated memory in 32-bit chunks, native endian. But that's nothing + * compared to Darek Mihocka + * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE + * RAM backwards! I'll want to represent both of those, via some flags. + * + * I suspect MAME either didn't think of that idea, or don't want the #ifdef. + * Not sure which, nor do I really care. */ + + /* TODO: Some of those flags are unused and/or don't really make sense. Clean + * them up. */ +}; + +/* The frontend may use the largest value of 'start'+'select' in a + * certain namespace to infer the size of the address space. + * + * If the address space is larger than that, a mapping with .ptr=NULL + * should be at the end of the array, with .select set to all ones for + * as long as the address space is big. + * + * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags): + * SNES WRAM: + * .start=0x7E0000, .len=0x20000 + * (Note that this must be mapped before the ROM in most cases; some of the + * ROM mappers + * try to claim $7E0000, or at least $7E8000.) + * SNES SPC700 RAM: + * .addrspace="S", .len=0x10000 + * SNES WRAM mirrors: + * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000 + * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000 + * SNES WRAM mirrors, alternate equivalent descriptor: + * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF + * (Various similar constructions can be created by combining parts of + * the above two.) + * SNES LoROM (512KB, mirrored a couple of times): + * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024 + * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024 + * SNES HiROM (4MB): + * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024 + * SNES ExHiROM (8MB): + * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024 + * Clarify the size of the address space: + * .ptr=NULL, .select=0xFFFFFF + * .len can be implied by .select in many of them, but was included for clarity. + */ + +struct retro_memory_map +{ + const struct retro_memory_descriptor *descriptors; + unsigned num_descriptors; +}; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/rcheevos/test/rapi/test_rc_api_common.c b/src/rcheevos/test/rapi/test_rc_api_common.c new file mode 100644 index 0000000000..d054718247 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_common.c @@ -0,0 +1,941 @@ +#include "../rapi/rc_api_common.h" + +#include "rc_api_runtime.h" /* for rc_fetch_image */ + +#include "../rc_compat.h" + +#include "../test_framework.h" + +#define IMAGEREQUEST_URL "https://media.retroachievements.org" + +static void _assert_json_parse_response(rc_api_response_t* response, rc_json_field_t* field, const char* json, int expected_result) { + int result; + rc_api_server_response_t server_response; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buffer_init(&response->buffer); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = json; + server_response.body_length = strlen(json); + + result = rc_json_parse_server_response(response, &server_response, fields, sizeof(fields)/sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, expected_result); + + ASSERT_NUM_EQUALS(response->succeeded, 1); + ASSERT_PTR_NULL(response->error_message); + + memcpy(field, &fields[2], sizeof(fields[2])); +} +#define assert_json_parse_response(response, field, json, expected_result) ASSERT_HELPER(_assert_json_parse_response(response, field, json, expected_result), "assert_json_parse_operand") + +static void _assert_field_value(rc_json_field_t* field, const char* expected_value) { + char buffer[256]; + + ASSERT_PTR_NOT_NULL(field->value_start); + ASSERT_PTR_NOT_NULL(field->value_end); + ASSERT_NUM_LESS(field->value_end - field->value_start, sizeof(buffer)); + + memcpy(buffer, field->value_start, field->value_end - field->value_start); + buffer[field->value_end - field->value_start] = '\0'; + ASSERT_STR_EQUALS(buffer, expected_value); +} +#define assert_field_value(field, expected_value) ASSERT_HELPER(_assert_field_value(field, expected_value), "assert_field_value") + +static void test_json_parse_response_empty() { + rc_api_response_t response; + rc_json_field_t field; + + assert_json_parse_response(&response, &field, "{}", RC_OK); + + ASSERT_STR_EQUALS(field.name, "Test"); + ASSERT_PTR_NULL(field.value_start); + ASSERT_PTR_NULL(field.value_end); +} + +static void test_json_parse_response_field(const char* json, const char* value) { + rc_api_response_t response; + rc_json_field_t field; + + assert_json_parse_response(&response, &field, json, RC_OK); + + ASSERT_STR_EQUALS(field.name, "Test"); + assert_field_value(&field, value); +} + +static void test_json_parse_response_non_json() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* error_message = "This is an error."; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buffer_init(&response.buffer); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = error_message; + server_response.body_length = strlen(error_message); + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "This is an error."); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_parse_response_non_json_bounded() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* error_message = "This is an error.\r\nShould not be seen"; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buffer_init(&response.buffer); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = error_message; + server_response.body_length = 16; /* "This is an error" (no period, newline, etc) */ + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "This is an error"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_parse_response_451() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* error_message = "" + "Unavailable For Legal Reasons" + "" + "

Unavailable For Legal Reasons

" + "

This request may not be serviced in the Roman Province" + "of Judea due to the Lex Julia Majestatis, which disallows" + "access to resources hosted on servers deemed to be" + "operated by the People's Front of Judea.

" + "" + ""; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buffer_init(&response.buffer); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = error_message; + server_response.body_length = strlen(error_message); + server_response.http_status_code = 451; + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Unavailable For Legal Reasons"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_parse_response_error_from_server() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* json; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buffer_init(&response.buffer); + + json = "{\"Success\":false,\"Error\":\"Oops\"}"; + memset(&server_response, 0, sizeof(server_response)); + server_response.body = json; + server_response.body_length = strlen(json); + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields)/sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_OK); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Oops"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_parse_response_incorrect_size() { + int result; + rc_api_server_response_t server_response; + rc_api_response_t response; + const char* json; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Test") + }; + rc_buffer_init(&response.buffer); + + json = "{\"Success\":false,\"Error\":\"Oops\"}"; + memset(&server_response, 0, sizeof(server_response)); + server_response.body = json; + server_response.body_length = strlen(json) - 1; + + result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + + /* the error message was found before the parser failed */ + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Oops"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_string(const char* escaped, const char* expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[256]; + const char *value = NULL; + snprintf(buffer, sizeof(buffer), "{\"Test\":\"%s\"}", escaped); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + ASSERT_TRUE(rc_json_get_string(&value, &response.buffer, &field, "Test")); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, expected); +} + +static void test_json_get_optional_string() { + rc_api_response_t response; + rc_json_field_t field; + const char *value = NULL; + + assert_json_parse_response(&response, &field, "{\"Test\":\"Value\"}", RC_OK); + + rc_json_get_optional_string(&value, &response, &field, "Test", "Default"); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, "Value"); + + assert_json_parse_response(&response, &field, "{\"Test2\":\"Value\"}", RC_OK); + + rc_json_get_optional_string(&value, &response, &field, "Test", "Default"); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, "Default"); +} + +static void test_json_get_required_string() { + rc_api_response_t response; + rc_json_field_t field; + const char *value = NULL; + + assert_json_parse_response(&response, &field, "{\"Test\":\"Value\"}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_string(&value, &response, &field, "Test")); + ASSERT_PTR_NOT_NULL(value); + ASSERT_STR_EQUALS(value, "Value"); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":\"Value\"}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_string(&value, &response, &field, "Test")); + ASSERT_PTR_NULL(value); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_num(const char* input, int expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + int value = 0; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected) { + ASSERT_TRUE(rc_json_get_num(&value, &field, "Test")); + } + else { + ASSERT_FALSE(rc_json_get_num(&value, &field, "Test")); + } + + ASSERT_NUM_EQUALS(value, expected); +} + +static void test_json_get_optional_num() { + rc_api_response_t response; + rc_json_field_t field; + int value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + rc_json_get_optional_num(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 12345678); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + rc_json_get_optional_num(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 9999); +} + +static void test_json_get_required_num() { + rc_api_response_t response; + rc_json_field_t field; + int value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_num(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 12345678); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_num(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_unum(const char* input, uint32_t expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + uint32_t value = 0; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected) { + ASSERT_TRUE(rc_json_get_unum(&value, &field, "Test")); + } + else { + ASSERT_FALSE(rc_json_get_unum(&value, &field, "Test")); + } + + ASSERT_NUM_EQUALS(value, expected); +} + +static void test_json_get_optional_unum() { + rc_api_response_t response; + rc_json_field_t field; + uint32_t value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + rc_json_get_optional_unum(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 12345678); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + rc_json_get_optional_unum(&value, &field, "Test", 9999); + ASSERT_NUM_EQUALS(value, 9999); +} + +static void test_json_get_required_unum() { + rc_api_response_t response; + rc_json_field_t field; + uint32_t value = 0; + + assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_unum(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 12345678); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_unum(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_float(const char* input, float expected) +{ + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + float value = 0.0; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected) + { + ASSERT_TRUE(rc_json_get_float(&value, &field, "Test")); + } + else + { + ASSERT_FALSE(rc_json_get_float(&value, &field, "Test")); + } + + ASSERT_FLOAT_EQUALS(value, expected); +} + +static void test_json_get_optional_float() +{ + rc_api_response_t response; + rc_json_field_t field; + float value = 0.0; + + assert_json_parse_response(&response, &field, "{\"Test\":1.5}", RC_OK); + + rc_json_get_optional_float(&value, &field, "Test", 9999); + ASSERT_FLOAT_EQUALS(value, 1.5); + + assert_json_parse_response(&response, &field, "{\"Test2\":1.5}", RC_OK); + + rc_json_get_optional_float(&value, &field, "Test", 2.5); + ASSERT_FLOAT_EQUALS(value, 2.5); +} + +static void test_json_get_required_float() +{ + rc_api_response_t response; + rc_json_field_t field; + float value = 0.0; + + assert_json_parse_response(&response, &field, "{\"Test\":1.5}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_float(&value, &response, &field, "Test")); + ASSERT_FLOAT_EQUALS(value, 1.5f); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":1.5}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_float(&value, &response, &field, "Test")); + ASSERT_FLOAT_EQUALS(value, 0.0f); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_bool(const char* input, int expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + int value = 2; + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected != -1) { + ASSERT_TRUE(rc_json_get_bool(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, expected); + } + else { + ASSERT_FALSE(rc_json_get_bool(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + } +} + +static void test_json_get_optional_bool() { + rc_api_response_t response; + rc_json_field_t field; + int value = 3; + + assert_json_parse_response(&response, &field, "{\"Test\":true}", RC_OK); + + rc_json_get_optional_bool(&value, &field, "Test", 2); + ASSERT_NUM_EQUALS(value, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":true}", RC_OK); + + rc_json_get_optional_bool(&value, &field, "Test", 2); + ASSERT_NUM_EQUALS(value, 2); +} + +static void test_json_get_required_bool() { + rc_api_response_t response; + rc_json_field_t field; + int value = 3; + + assert_json_parse_response(&response, &field, "{\"Test\":true}", RC_OK); + + ASSERT_TRUE(rc_json_get_required_bool(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 1); + + ASSERT_PTR_NULL(response.error_message); + ASSERT_NUM_EQUALS(response.succeeded, 1); + + assert_json_parse_response(&response, &field, "{\"Test2\":True}", RC_OK); + + ASSERT_FALSE(rc_json_get_required_bool(&value, &response, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + + ASSERT_PTR_NOT_NULL(response.error_message); + ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); + ASSERT_NUM_EQUALS(response.succeeded, 0); +} + +static void test_json_get_datetime(const char* input, int expected) { + rc_api_response_t response; + rc_json_field_t field; + char buffer[64]; + time_t value = 2; + snprintf(buffer, sizeof(buffer), "{\"Test\":\"%s\"}", input); + + assert_json_parse_response(&response, &field, buffer, RC_OK); + + if (expected != -1) { + ASSERT_TRUE(rc_json_get_datetime(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, (time_t)expected); + } + else { + ASSERT_FALSE(rc_json_get_datetime(&value, &field, "Test")); + ASSERT_NUM_EQUALS(value, 0); + } +} + +static void test_json_get_unum_array(const char* input, uint32_t expected_count, int expected_result) { + rc_api_response_t response; + rc_json_field_t field; + int result; + uint32_t count = 0xFFFFFFFF; + uint32_t*values; + char buffer[128]; + + snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); + assert_json_parse_response(&response, &field, buffer, RC_OK); + + result = rc_json_get_required_unum_array(&values, &count, &response, &field, "Test"); + ASSERT_NUM_EQUALS(result, expected_result); + ASSERT_NUM_EQUALS(count, expected_count); + + rc_buffer_destroy(&response.buffer); +} + +static void test_json_get_unum_array_trailing_comma() { + rc_api_response_t response; + rc_json_field_t field; + + assert_json_parse_response(&response, &field, "{\"Test\":[1,2,3,]}", RC_INVALID_JSON); +} + +static void test_url_build_dorequest_url_default_host() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + + rc_api_url_build_dorequest_url(&request, NULL); + ASSERT_STR_EQUALS(request.url, "https://retroachievements.org/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request_hosted(&request, &api_params, NULL); + ASSERT_STR_EQUALS(request.url, "https://media.retroachievements.org/Badge/12345.png"); + rc_api_destroy_request(&request); +} + +static void test_url_build_dorequest_url_default_host_nonssl() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + rc_api_host_t host; + + memset(&host, 0, sizeof(host)); + host.host = "http://retroachievements.org"; + + rc_api_url_build_dorequest_url(&request, &host); + ASSERT_STR_EQUALS(request.url, "http://retroachievements.org/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request_hosted(&request, &api_params, &host); + ASSERT_STR_EQUALS(request.url, "http://media.retroachievements.org/Badge/12345.png"); + rc_api_destroy_request(&request); +} + +static void test_url_build_dorequest_url_custom_host() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + rc_api_host_t host; + + memset(&host, 0, sizeof(host)); + host.host = "http://localhost"; + + rc_api_url_build_dorequest_url(&request, &host); + ASSERT_STR_EQUALS(request.url, "http://localhost/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request_hosted(&request, &api_params, &host); + ASSERT_STR_EQUALS(request.url, "http://localhost/Badge/12345.png"); + rc_api_destroy_request(&request); +} + +static void test_url_build_dorequest_url_custom_host_no_protocol() { + rc_api_request_t request; + rc_api_fetch_image_request_t api_params; + rc_api_host_t host; + + memset(&host, 0, sizeof(host)); + host.host = "my.host"; + + rc_api_url_build_dorequest_url(&request, &host); + ASSERT_STR_EQUALS(request.url, "http://my.host/dorequest.php"); + rc_api_destroy_request(&request); + + api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + api_params.image_name = "12345"; + rc_api_init_fetch_image_request_hosted(&request, &api_params, &host); + ASSERT_STR_EQUALS(request.url, "http://my.host/Badge/12345.png"); + rc_api_destroy_request(&request); +} + +static void test_url_builder_append_encoded_str(const char* input, const char* expected) { + rc_api_url_builder_t builder; + rc_buffer_t buffer; + const char* output; + + rc_buffer_init(&buffer); + rc_url_builder_init(&builder, &buffer, 128); + rc_url_builder_append_encoded_str(&builder, input); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, expected); + + rc_buffer_destroy(&buffer); +} + +static void test_url_builder_append_str_param() { + rc_api_url_builder_t builder; + rc_buffer_t buffer; + const char* output; + + rc_buffer_init(&buffer); + rc_url_builder_init(&builder, &buffer, 64); + rc_url_builder_append_str_param(&builder, "a", "Apple"); + rc_url_builder_append_str_param(&builder, "b", "Banana"); + rc_url_builder_append_str_param(&builder, "t", "Test 1"); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, "a=Apple&b=Banana&t=Test+1"); + + rc_buffer_destroy(&buffer); +} + +static void test_url_builder_append_unum_param() { + rc_api_url_builder_t builder; + rc_buffer_t buffer; + const char* output; + + rc_buffer_init(&buffer); + rc_url_builder_init(&builder, &buffer, 32); + rc_url_builder_append_unum_param(&builder, "a", 0); + rc_url_builder_append_unum_param(&builder, "b", 123456); + rc_url_builder_append_unum_param(&builder, "t", (unsigned)-1); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, "a=0&b=123456&t=4294967295"); + + rc_buffer_destroy(&buffer); +} + +static void test_url_builder_append_num_param() { + rc_api_url_builder_t builder; + rc_buffer_t buffer; + const char* output; + + rc_buffer_init(&buffer); + rc_url_builder_init(&builder, &buffer, 32); + rc_url_builder_append_num_param(&builder, "a", 0); + rc_url_builder_append_num_param(&builder, "b", 123456); + rc_url_builder_append_num_param(&builder, "t", -1); + output = rc_url_builder_finalize(&builder); + + ASSERT_STR_EQUALS(output, "a=0&b=123456&t=-1"); + + rc_buffer_destroy(&buffer); +} + +static void test_init_fetch_image_request_game() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "0123324"; + fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Images/0123324.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_achievement() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "135764"; + fetch_image_request.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Badge/135764.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_achievement_locked() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "135764"; + fetch_image_request.image_type = RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Badge/135764_lock.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_user() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "Username"; + fetch_image_request.image_type = RC_IMAGE_TYPE_USER; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); + ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/UserPic/Username.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_unknown() { + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "12345"; + fetch_image_request.image_type = -1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_custom_host() +{ + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + rc_api_host_t host; + + memset(&host, 0, sizeof(host)); + host.host = "http://localhost"; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "0123324"; + fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request_hosted(&request, &fetch_image_request, &host), RC_OK); + ASSERT_STR_EQUALS(request.url, "http://localhost/Images/0123324.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_explicit_default_host() +{ + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + rc_api_host_t host; + + memset(&host, 0, sizeof(host)); + host.host = rc_api_default_host(); + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "0123324"; + fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request_hosted(&request, &fetch_image_request, &host), RC_OK); + ASSERT_STR_EQUALS(request.url, "https://media.retroachievements.org/Images/0123324.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_image_request_explicit_nonssl_host() +{ + rc_api_fetch_image_request_t fetch_image_request; + rc_api_request_t request; + rc_api_host_t host; + + memset(&host, 0, sizeof(host)); + host.host = "http://retroachievements.org"; + + memset(&fetch_image_request, 0, sizeof(fetch_image_request)); + fetch_image_request.image_name = "0123324"; + fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request_hosted(&request, &fetch_image_request, &host), RC_OK); + ASSERT_STR_EQUALS(request.url, "http://media.retroachievements.org/Images/0123324.png"); + ASSERT_PTR_NULL(request.post_data); + + rc_api_destroy_request(&request); +} + +void test_rapi_common(void) { + TEST_SUITE_BEGIN(); + + /* rc_json_parse_response */ + TEST(test_json_parse_response_empty); + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":\"Test\"}", "\"Test\""); /* string */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":\"Te\\\"st\"}", "\"Te\\\"st\""); /* escaped string */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":12345678}", "12345678"); /* integer */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":+12345678}", "+12345678"); /* positive integer */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":-12345678}", "-12345678"); /* negatvie integer */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":1234.5678}", "1234.5678"); /* decimal */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":+1234.5678}", "+1234.5678"); /* positive decimal */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":-1234.5678}", "-1234.5678"); /* negatvie decimal */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":[1,2,3]}", "[1,2,3]"); /* array */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":{\"Foo\":1}}", "{\"Foo\":1}"); /* object */ + TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":null}", "null"); /* null */ + TEST_PARAMS2(test_json_parse_response_field, "{ \"Test\" : 0 }", "0"); /* ignore whitespace */ + TEST_PARAMS2(test_json_parse_response_field, "{ \"Other\" : 1, \"Test\" : 2 }", "2"); /* preceding field */ + TEST_PARAMS2(test_json_parse_response_field, "{ \"Test\" : 1, \"Other\" : 2 }", "1"); /* trailing field */ + TEST(test_json_parse_response_non_json); + TEST(test_json_parse_response_non_json_bounded); + TEST(test_json_parse_response_451); + TEST(test_json_parse_response_error_from_server); + TEST(test_json_parse_response_incorrect_size); + + /* rc_json_get_string */ + TEST_PARAMS2(test_json_get_string, "", ""); + TEST_PARAMS2(test_json_get_string, "Banana", "Banana"); + TEST_PARAMS2(test_json_get_string, "A \\\"Quoted\\\" String", "A \"Quoted\" String"); + TEST_PARAMS2(test_json_get_string, "This\\r\\nThat", "This\r\nThat"); + TEST_PARAMS2(test_json_get_string, "This\\/That", "This/That"); + TEST_PARAMS2(test_json_get_string, "\\u0065", "e"); + TEST_PARAMS2(test_json_get_string, "\\u00a9", "\xc2\xa9"); + TEST_PARAMS2(test_json_get_string, "\\u2260", "\xe2\x89\xa0"); + TEST_PARAMS2(test_json_get_string, "\\ud83d\\udeb6", "\xf0\x9f\x9a\xb6"); /* surrogate pair */ + TEST_PARAMS2(test_json_get_string, "\\ud83d", "\xef\xbf\xbd"); /* surrogate lead with no tail */ + TEST_PARAMS2(test_json_get_string, "\\udeb6", "\xef\xbf\xbd"); /* surrogate tail with no lead */ + TEST(test_json_get_optional_string); + TEST(test_json_get_required_string); + + /* rc_json_get_num */ + TEST_PARAMS2(test_json_get_num, "Banana", 0); + TEST_PARAMS2(test_json_get_num, "True", 0); + TEST_PARAMS2(test_json_get_num, "2468", 2468); + TEST_PARAMS2(test_json_get_num, "+55", 55); + TEST_PARAMS2(test_json_get_num, "-16", -16); + TEST_PARAMS2(test_json_get_num, "3.14159", 3); + TEST(test_json_get_optional_num); + TEST(test_json_get_required_num); + + /* rc_json_get_unum */ + TEST_PARAMS2(test_json_get_unum, "Banana", 0); + TEST_PARAMS2(test_json_get_unum, "True", 0); + TEST_PARAMS2(test_json_get_unum, "2468", 2468); + TEST_PARAMS2(test_json_get_unum, "+55", 0); + TEST_PARAMS2(test_json_get_unum, "-16", 0); + TEST_PARAMS2(test_json_get_unum, "3.14159", 3); + TEST(test_json_get_optional_unum); + TEST(test_json_get_required_unum); + + /* rc_json_get_num */ + TEST_PARAMS2(test_json_get_float, "Banana", 0.0f); + TEST_PARAMS2(test_json_get_float, "True", 0.0f); + TEST_PARAMS2(test_json_get_float, "2468", 2468.0f); + TEST_PARAMS2(test_json_get_float, "+55", 55.0f); + TEST_PARAMS2(test_json_get_float, "-16", -16.0f); + TEST_PARAMS2(test_json_get_float, "3.14159", 3.14159f); + TEST_PARAMS2(test_json_get_float, "-6.7", -6.7f); + TEST(test_json_get_optional_float); + TEST(test_json_get_required_float); + + /* rc_json_get_bool */ + TEST_PARAMS2(test_json_get_bool, "true", 1); + TEST_PARAMS2(test_json_get_bool, "false", 0); + TEST_PARAMS2(test_json_get_bool, "TRUE", 1); + TEST_PARAMS2(test_json_get_bool, "True", 1); + TEST_PARAMS2(test_json_get_bool, "Banana", -1); + TEST_PARAMS2(test_json_get_bool, "1", 1); + TEST_PARAMS2(test_json_get_bool, "0", 0); + TEST(test_json_get_optional_bool); + TEST(test_json_get_required_bool); + + /* rc_json_get_datetime */ + TEST_PARAMS2(test_json_get_datetime, "", -1); + TEST_PARAMS2(test_json_get_datetime, "2015-01-01 08:15:00", 1420100100); + TEST_PARAMS2(test_json_get_datetime, "2016-02-29 20:01:47", 1456776107); + + /* rc_json_get_unum_array */ + TEST_PARAMS3(test_json_get_unum_array, "[]", 0, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "1", 0, RC_MISSING_VALUE); + TEST_PARAMS3(test_json_get_unum_array, "[1]", 1, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[ 1 ]", 1, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[1,2,3,4]", 4, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[ 1 , 2 ]", 2, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[1,1,1]", 3, RC_OK); + TEST_PARAMS3(test_json_get_unum_array, "[A,B,C]", 3, RC_MISSING_VALUE); + TEST(test_json_get_unum_array_trailing_comma); + + /* rc_api_url_build_dorequest_url / rc_api_url_build_dorequest_url_hosted */ + TEST(test_url_build_dorequest_url_default_host); + TEST(test_url_build_dorequest_url_default_host_nonssl); + TEST(test_url_build_dorequest_url_custom_host); + TEST(test_url_build_dorequest_url_custom_host_no_protocol); + + /* rc_api_url_builder_append_encoded_str */ + TEST_PARAMS2(test_url_builder_append_encoded_str, "", ""); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Apple", "Apple"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Test 1", "Test+1"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Test+1", "Test%2b1"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "Test%1", "Test%251"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "%Test%", "%25Test%25"); + TEST_PARAMS2(test_url_builder_append_encoded_str, "%%", "%25%25"); + + /* rc_api_url_builder_append_param */ + TEST(test_url_builder_append_str_param); + TEST(test_url_builder_append_num_param); + TEST(test_url_builder_append_unum_param); + + /* fetch_image */ + TEST(test_init_fetch_image_request_game); + TEST(test_init_fetch_image_request_achievement); + TEST(test_init_fetch_image_request_achievement_locked); + TEST(test_init_fetch_image_request_user); + TEST(test_init_fetch_image_request_unknown); + TEST(test_init_fetch_image_request_custom_host); + TEST(test_init_fetch_image_request_explicit_default_host); + TEST(test_init_fetch_image_request_explicit_nonssl_host); + + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_editor.c b/src/rcheevos/test/rapi/test_rc_api_editor.c new file mode 100644 index 0000000000..e01adc6d59 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_editor.c @@ -0,0 +1,931 @@ +#include "rc_api_editor.h" +#include "rc_api_runtime.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" +#include "../rc_compat.h" +#include "rc_consoles.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_fetch_code_notes_request() +{ + rc_api_fetch_code_notes_request_t fetch_code_notes_request; + rc_api_request_t request; + + memset(&fetch_code_notes_request, 0, sizeof(fetch_code_notes_request)); + fetch_code_notes_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_code_notes_request(&request, &fetch_code_notes_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=codenotes2&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_code_notes_request_no_game_id() +{ + rc_api_fetch_code_notes_request_t fetch_code_notes_request; + rc_api_request_t request; + + memset(&fetch_code_notes_request, 0, sizeof(fetch_code_notes_request)); + + ASSERT_NUM_EQUALS(rc_api_init_fetch_code_notes_request(&request, &fetch_code_notes_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_code_notes_response_empty_array() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_PTR_NULL(fetch_code_notes_response.notes); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 0); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_one_item() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_several_items() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}," + "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," + "{\"User\":\"User2\",\"Address\":\"0x002002\",\"Note\":\"Sad\"}," + "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"Banana\\n0=a\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 4); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[1].address, 0x2000); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].note, "Happy"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[2].address, 0x2002); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].author, "User2"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].note, "Sad"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[3].address, 0x2ABC); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].note, "Banana\n0=a"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_deleted_items() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"\"}," + "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," + "{\"User\":\"User2\",\"Address\":\"0x002002\",\"Note\":\"''\"}," + "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x2000); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "Happy"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_null_user() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}," + "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," + "{\"User\":null,\"Address\":\"0x002002\",\"Note\":\"Sad\"}," + "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"Banana\\n0=a\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 4); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[1].address, 0x2000); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].note, "Happy"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[2].address, 0x2002); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].author, ""); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].note, "Sad"); + + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[3].address, 0x2ABC); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].note, "Banana\n0=a"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_fetch_code_notes_response_unsigned() +{ + rc_api_fetch_code_notes_response_t fetch_code_notes_response; + const char* server_response = "{\"Success\":true,\"CodeNotes\":[" + "{\"User\":\"User\",\"Address\":\"0x98765432\",\"Note\":\"01=true\"}" + "]}"; + memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); + ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); + ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x98765432); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); + ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); + + rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); +} + +static void test_init_update_code_note_request() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.game_id = 1234; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = "flags\n1=first\n2=second"; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168&n=flags%0a1%3dfirst%0a2%3dsecond"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_request_no_game_id() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = "flags\n1=first\n2=second"; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_request_no_note() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.game_id = 1234; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = NULL; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_request_empty_note() +{ + rc_api_update_code_note_request_t update_code_note_request; + rc_api_request_t request; + + memset(&update_code_note_request, 0, sizeof(update_code_note_request)); + update_code_note_request.username = "Dev"; + update_code_note_request.api_token = "API_TOKEN"; + update_code_note_request.game_id = 1234; + update_code_note_request.address = 0x1C00; + update_code_note_request.note = ""; + + ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_code_note_response() +{ + rc_api_update_code_note_response_t update_code_note_response; + const char* server_response = "{\"Success\":true,\"GameID\":1234,\"Address\":7168,\"Note\":\"test\"}"; + memset(&update_code_note_response, 0, sizeof(update_code_note_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_code_note_response(&update_code_note_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_code_note_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_code_note_response.response.error_message); + + rc_api_destroy_update_code_note_response(&update_code_note_response); +} + +static void test_init_update_code_note_response_invalid_credentials() +{ + rc_api_update_code_note_response_t update_code_note_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&update_code_note_response, 0, sizeof(update_code_note_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_code_note_response(&update_code_note_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_code_note_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_code_note_response.response.error_message, "Credentials invalid (0)"); + + rc_api_destroy_update_code_note_response(&update_code_note_response); +} + +static void test_init_update_achievement_request() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.game_id = 1234; + update_achievement_request.achievement_id = 5555; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=&h=7cd9d3f0bfdf84734968353b5a430cfd"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_request_new() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.game_id = 1234; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=5&b=123456&x=&h=10dd1fd6e0201f634b1b7536d4860ccb"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_request_no_game_id() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.achievement_id = 5555; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_request_type() +{ + rc_api_update_achievement_request_t update_achievement_request; + rc_api_request_t request; + + memset(&update_achievement_request, 0, sizeof(update_achievement_request)); + update_achievement_request.username = "Dev"; + update_achievement_request.api_token = "API_TOKEN"; + update_achievement_request.game_id = 1234; + update_achievement_request.achievement_id = 5555; + update_achievement_request.title = "Title"; + update_achievement_request.description = "Description"; + update_achievement_request.badge = "123456"; + update_achievement_request.trigger = "0xH1234=1"; + update_achievement_request.points = 5; + update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; + update_achievement_request.type = RC_ACHIEVEMENT_TYPE_MISSABLE; + + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=missable&h=7cd9d3f0bfdf84734968353b5a430cfd"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + update_achievement_request.type = RC_ACHIEVEMENT_TYPE_PROGRESSION; + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=progression&h=7cd9d3f0bfdf84734968353b5a430cfd"); + + update_achievement_request.type = RC_ACHIEVEMENT_TYPE_WIN; + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=win_condition&h=7cd9d3f0bfdf84734968353b5a430cfd"); + + update_achievement_request.type = RC_ACHIEVEMENT_TYPE_STANDARD; + ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=&h=7cd9d3f0bfdf84734968353b5a430cfd"); + + rc_api_destroy_request(&request); +} + +static void test_init_update_achievement_response() +{ + rc_api_update_achievement_response_t update_achievement_response; + const char* server_response = "{\"Success\":true,\"AchievementID\":1234}"; + memset(&update_achievement_response, 0, sizeof(update_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 1234); + + rc_api_destroy_update_achievement_response(&update_achievement_response); +} + +static void test_init_update_achievement_response_invalid_credentials() +{ + rc_api_update_achievement_response_t update_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&update_achievement_response, 0, sizeof(update_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); + + rc_api_destroy_update_achievement_response(&update_achievement_response); +} + +static void test_init_update_achievement_response_invalid_perms() +{ + rc_api_update_achievement_response_t update_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; + memset(&update_achievement_response, 0, sizeof(update_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); + ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); + + rc_api_destroy_update_achievement_response(&update_achievement_response); +} + +static void test_init_update_leaderboard_request() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.leaderboard_id = 5555; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_new() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=739e28608a9e93d7351103d2f43fc6dc"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_no_game_id() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_no_description() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.leaderboard_id = 5555; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = ""; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_response() +{ + rc_api_update_leaderboard_response_t update_leaderboard_response; + const char* server_response = "{\"Success\":true,\"LeaderboardID\":1234}"; + memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_leaderboard_response.response.error_message); + ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 1234); + + rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); +} + +static void test_init_update_leaderboard_response_invalid_credentials() +{ + rc_api_update_leaderboard_response_t update_leaderboard_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); + + rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); +} + +static void test_init_update_leaderboard_response_invalid_perms() +{ + rc_api_update_leaderboard_response_t update_leaderboard_response; + const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; + memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); + + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); + ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); + + rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); +} + +static void test_init_update_rich_presence_request() +{ + rc_api_update_rich_presence_request_t update_rich_presence_request; + rc_api_request_t request; + + memset(&update_rich_presence_request, 0, sizeof(update_rich_presence_request)); + update_rich_presence_request.username = "Dev"; + update_rich_presence_request.api_token = "API_TOKEN"; + update_rich_presence_request.game_id = 1234; + update_rich_presence_request.script = "Display:\nThis is a test."; + + ASSERT_NUM_EQUALS(rc_api_init_update_rich_presence_request(&request, &update_rich_presence_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitrichpresence&u=Dev&t=API_TOKEN&g=1234&d=Display%3a%0aThis+is+a+test."); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_rich_presence_request_no_game_id() +{ + rc_api_update_rich_presence_request_t update_rich_presence_request; + rc_api_request_t request; + + memset(&update_rich_presence_request, 0, sizeof(update_rich_presence_request)); + update_rich_presence_request.username = "Dev"; + update_rich_presence_request.api_token = "API_TOKEN"; + update_rich_presence_request.script = "Display:\nThis is a test."; + + ASSERT_NUM_EQUALS(rc_api_init_update_rich_presence_request(&request, &update_rich_presence_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_rich_presence_request_no_script() +{ + rc_api_update_rich_presence_request_t update_rich_presence_request; + rc_api_request_t request; + + memset(&update_rich_presence_request, 0, sizeof(update_rich_presence_request)); + update_rich_presence_request.username = "Dev"; + update_rich_presence_request.api_token = "API_TOKEN"; + update_rich_presence_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_update_rich_presence_request(&request, &update_rich_presence_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_update_rich_presence_response() +{ + rc_api_server_response_t response_obj; + rc_api_update_rich_presence_response_t update_rich_presence_response; + const char* server_response = "{\"Success\":true}"; + memset(&update_rich_presence_response, 0, sizeof(update_rich_presence_response)); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = strlen(server_response); + + ASSERT_NUM_EQUALS(rc_api_process_update_rich_presence_server_response(&update_rich_presence_response, &response_obj), RC_OK); + ASSERT_NUM_EQUALS(update_rich_presence_response.response.succeeded, 1); + ASSERT_PTR_NULL(update_rich_presence_response.response.error_message); + + rc_api_destroy_update_rich_presence_response(&update_rich_presence_response); +} + +static void test_init_update_rich_presence_response_invalid_credentials() +{ + rc_api_server_response_t response_obj; + rc_api_update_rich_presence_response_t update_rich_presence_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Invalid user/token combination.\",\"Code\":\"invalid_credentials\",\"Status\":401}"; + memset(&update_rich_presence_response, 0, sizeof(update_rich_presence_response)); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = strlen(server_response); + + ASSERT_NUM_EQUALS(rc_api_process_update_rich_presence_server_response(&update_rich_presence_response, &response_obj), RC_INVALID_CREDENTIALS); + ASSERT_NUM_EQUALS(update_rich_presence_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_rich_presence_response.response.error_message, "Invalid user/token combination."); + + rc_api_destroy_update_rich_presence_response(&update_rich_presence_response); +} + +static void test_init_update_rich_presence_response_invalid_perms() +{ + rc_api_server_response_t response_obj; + rc_api_update_rich_presence_response_t update_rich_presence_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Access denied.\",\"Code\":\"access_denied\",\"Status\":400}"; + memset(&update_rich_presence_response, 0, sizeof(update_rich_presence_response)); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = strlen(server_response); + + ASSERT_NUM_EQUALS(rc_api_process_update_rich_presence_server_response(&update_rich_presence_response, &response_obj), RC_ACCESS_DENIED); + ASSERT_NUM_EQUALS(update_rich_presence_response.response.succeeded, 0); + ASSERT_STR_EQUALS(update_rich_presence_response.response.error_message, "Access denied."); + + rc_api_destroy_update_rich_presence_response(&update_rich_presence_response); +} + +static void test_init_fetch_badge_range_request() +{ + rc_api_fetch_badge_range_request_t fetch_badge_range_request; + rc_api_request_t request; + + memset(&fetch_badge_range_request, 0, sizeof(fetch_badge_range_request)); + + ASSERT_NUM_EQUALS(rc_api_init_fetch_badge_range_request(&request, &fetch_badge_range_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=badgeiter"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_badge_range_response() +{ + rc_api_fetch_badge_range_response_t fetch_badge_range_response; + const char* server_response = "{\"Success\":true,\"FirstBadge\":12,\"NextBadge\":123456}"; + memset(&fetch_badge_range_response, 0, sizeof(fetch_badge_range_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_badge_range_response(&fetch_badge_range_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_badge_range_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_badge_range_response.response.error_message); + ASSERT_UNUM_EQUALS(fetch_badge_range_response.first_badge_id, 12); + ASSERT_UNUM_EQUALS(fetch_badge_range_response.next_badge_id, 123456); + + rc_api_destroy_fetch_badge_range_response(&fetch_badge_range_response); +} + +static void test_init_add_game_hash_request() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.game_id = 1234; + add_game_hash_request.title = "Game Name"; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&i=Game+Name&g=1234&d=Game+Name+%5bNo+Intro%5d.nes"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_game_id() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.title = "Game Name"; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&i=Game+Name&d=Game+Name+%5bNo+Intro%5d.nes"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_console_id() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.game_id = 1234; + add_game_hash_request.title = "Game Name"; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_title() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.game_id = 1234; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + /* title is not required when a game id is provided (at least at the client + * level - the server will generate an error, but that could change) */ + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&g=1234&d=Game+Name+%5bNo+Intro%5d.nes"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_request_no_title_or_game_id() +{ + rc_api_add_game_hash_request_t add_game_hash_request; + rc_api_request_t request; + + memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); + add_game_hash_request.username = "Dev"; + add_game_hash_request.api_token = "API_TOKEN"; + add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; + add_game_hash_request.hash = "NEW_HASH"; + add_game_hash_request.hash_description = "Game Name [No Intro].nes"; + + ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_add_game_hash_response() +{ + rc_api_add_game_hash_response_t add_game_hash_response; + const char* server_response = "{\"Success\":true,\"Response\":{\"GameID\":1234}}"; + memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 1); + ASSERT_PTR_NULL(add_game_hash_response.response.error_message); + ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 1234); + + rc_api_destroy_add_game_hash_response(&add_game_hash_response); +} + +static void test_init_add_game_hash_response_error() +{ + rc_api_add_game_hash_response_t add_game_hash_response; + const char* server_response = "{\"Success\":false,\"Error\":\"The ROM you are trying to load is not in the database.\"}"; + memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 0); + ASSERT_STR_EQUALS(add_game_hash_response.response.error_message, "The ROM you are trying to load is not in the database."); + ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 0); + + rc_api_destroy_add_game_hash_response(&add_game_hash_response); +} + +static void test_init_add_game_hash_response_invalid_credentials() +{ + rc_api_add_game_hash_response_t add_game_hash_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 0); + ASSERT_STR_EQUALS(add_game_hash_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 0); + + rc_api_destroy_add_game_hash_response(&add_game_hash_response); +} + +void test_rapi_editor(void) { + TEST_SUITE_BEGIN(); + + /* fetch code notes */ + TEST(test_init_fetch_code_notes_request); + TEST(test_init_fetch_code_notes_request_no_game_id); + + TEST(test_init_fetch_code_notes_response_empty_array); + TEST(test_init_fetch_code_notes_response_one_item); + TEST(test_init_fetch_code_notes_response_several_items); + TEST(test_init_fetch_code_notes_response_deleted_items); + TEST(test_init_fetch_code_notes_response_null_user); + TEST(test_init_fetch_code_notes_response_unsigned); + + /* update code note */ + TEST(test_init_update_code_note_request); + TEST(test_init_update_code_note_request_no_game_id); + TEST(test_init_update_code_note_request_no_note); + TEST(test_init_update_code_note_request_empty_note); + + TEST(test_init_update_code_note_response); + TEST(test_init_update_code_note_response_invalid_credentials); + + /* update achievement */ + TEST(test_init_update_achievement_request); + TEST(test_init_update_achievement_request_new); + TEST(test_init_update_achievement_request_no_game_id); + TEST(test_init_update_achievement_request_type); + + TEST(test_init_update_achievement_response); + TEST(test_init_update_achievement_response_invalid_credentials); + TEST(test_init_update_achievement_response_invalid_perms); + + /* update leaderboard */ + TEST(test_init_update_leaderboard_request); + TEST(test_init_update_leaderboard_request_new); + TEST(test_init_update_leaderboard_request_no_game_id); + TEST(test_init_update_leaderboard_request_no_description); + + TEST(test_init_update_leaderboard_response); + TEST(test_init_update_leaderboard_response_invalid_credentials); + TEST(test_init_update_leaderboard_response_invalid_perms); + + /* update rich presence */ + TEST(test_init_update_rich_presence_request); + TEST(test_init_update_rich_presence_request_no_game_id); + TEST(test_init_update_rich_presence_request_no_script); + + TEST(test_init_update_rich_presence_response); + TEST(test_init_update_rich_presence_response_invalid_credentials); + TEST(test_init_update_rich_presence_response_invalid_perms); + + /* fetch badge range */ + TEST(test_init_fetch_badge_range_request); + + TEST(test_init_fetch_badge_range_response); + + /* add game hash */ + TEST(test_init_add_game_hash_request); + TEST(test_init_add_game_hash_request_no_game_id); + TEST(test_init_add_game_hash_request_no_console_id); + TEST(test_init_add_game_hash_request_no_title); + TEST(test_init_add_game_hash_request_no_title_or_game_id); + + TEST(test_init_add_game_hash_response); + TEST(test_init_add_game_hash_response_error); + TEST(test_init_add_game_hash_response_invalid_credentials); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_info.c b/src/rcheevos/test/rapi/test_rc_api_info.c new file mode 100644 index 0000000000..82b4628541 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_info.c @@ -0,0 +1,537 @@ +#include "rc_api_info.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" +#include "../rc_compat.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_fetch_achievement_info_request() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.first_entry = 100; + fetch_achievement_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&o=99&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_achievement_info_request_no_first() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_achievement_info_request_one_first() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.first_entry = 1; + fetch_achievement_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_achievement_info_request_friends_only() { + rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; + rc_api_request_t request; + + memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); + fetch_achievement_info_request.username = "Username"; + fetch_achievement_info_request.api_token = "API_TOKEN"; + fetch_achievement_info_request.achievement_id = 1234; + fetch_achievement_info_request.count = 50; + fetch_achievement_info_request.friends_only = 1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&f=1&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_achievement_info_response() { + rc_api_fetch_achievement_info_response_t fetch_achievement_info_response; + rc_api_achievement_awarded_entry_t* entry; + const char* server_response = "{\"Success\":true,\"AchievementID\":1234,\"Response\":{" + "\"NumEarned\":17,\"GameID\":2345,\"TotalPlayers\":25," + "\"RecentWinner\":[{\"User\":\"Player1\",\"DateAwarded\":1615654895,\"AvatarUrl\":\"http://host/UserPic/PLAYER1.png\"}," + "{\"User\":\"Player2\",\"DateAwarded\":1600604303}]" + "}}"; + + memset(&fetch_achievement_info_response, 0, sizeof(fetch_achievement_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_achievement_info_response(&fetch_achievement_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_achievement_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.id, 1234); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.game_id, 2345); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_awarded, 17); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_players, 25); + ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_recently_awarded, 2); + + entry = &fetch_achievement_info_response.recently_awarded[0]; + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->awarded, 1615654895); + ASSERT_STR_EQUALS(entry->avatar_url, "http://host/UserPic/PLAYER1.png"); + entry = &fetch_achievement_info_response.recently_awarded[1]; + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->awarded, 1600604303); + ASSERT_STR_EQUALS(entry->avatar_url, "https://media.retroachievements.org/UserPic/Player2.png"); + + rc_api_destroy_fetch_achievement_info_response(&fetch_achievement_info_response); +} + +static void test_init_fetch_leaderboard_info_request() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.first_entry = 101; + fetch_leaderboard_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&o=100&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_leaderboard_info_request_no_first() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.count = 50; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&c=50"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_leaderboard_info_request_for_user() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.username = "Username"; + fetch_leaderboard_info_request.count = 20; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&u=Username&c=20"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_leaderboard_info_request_for_user_with_offset() { + rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; + rc_api_request_t request; + + memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); + fetch_leaderboard_info_request.leaderboard_id = 1234; + fetch_leaderboard_info_request.username = "Username"; + fetch_leaderboard_info_request.first_entry = 11; /* should be ignored */ + fetch_leaderboard_info_request.count = 20; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&u=Username&c=20"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_leaderboard_info_response() { + rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; + rc_api_lboard_info_entry_t* entry; + const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":1234,\"GameID\":2345," + "\"LowerIsBetter\":1,\"LBTitle\":\"Title\",\"LBDesc\":\"Description\",\"LBFormat\":\"TIME\"," + "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":12," + "\"Entries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1615654895,\"AvatarUrl\":\"http://host/UserPic/PLAYER1.png\"}," + "{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2,\"Index\":6,\"DateSubmitted\":1600604303}]" + "}}"; + + memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 1234); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2345); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 1); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.author); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.total_entries, 12); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1382307141); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1623658699); + + entry = &fetch_leaderboard_info_response.entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 8765); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + ASSERT_STR_EQUALS(entry->avatar_url, "http://host/UserPic/PLAYER1.png"); + entry = &fetch_leaderboard_info_response.entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 7654); + ASSERT_NUM_EQUALS(entry->submitted, 1600604303); + ASSERT_STR_EQUALS(entry->avatar_url, "https://media.retroachievements.org/UserPic/Player2.png"); + + rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); +} + +static void test_process_fetch_leaderboard_info_response2() { + rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; + rc_api_lboard_info_entry_t* entry; + const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":9999,\"GameID\":2222," + "\"LowerIsBetter\":0,\"LBTitle\":\"Title2\",\"LBDesc\":\"Description2\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":\"AuthorName\"," + "\"LBCreated\":\"2021-06-18 15:32:16\",\"LBUpdated\":\"2021-06-18 15:32:16\",\"TotalEntries\":12," + "\"Entries\":[{\"User\":\"Player1\",\"Score\":1013580,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1624055310}," + "{\"User\":\"Player2\",\"Score\":133340,\"Rank\":1,\"Index\":6,\"DateSubmitted\":1624166772}]" + "}}"; + + memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 9999); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2222); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 0); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title2"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description2"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.author, "AuthorName"); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.total_entries, 12); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1624030336); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1624030336); + + entry = &fetch_leaderboard_info_response.entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 1013580); + ASSERT_NUM_EQUALS(entry->submitted, 1624055310); + entry = &fetch_leaderboard_info_response.entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 133340); + ASSERT_NUM_EQUALS(entry->submitted, 1624166772); + + rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); +} + +static void test_process_fetch_leaderboard_info_response_iso8601() { + rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; + rc_api_lboard_info_entry_t* entry; + const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":1234,\"GameID\":2345," + "\"LowerIsBetter\":1,\"LBTitle\":\"Title\",\"LBDesc\":\"Description\",\"LBFormat\":\"TIME\"," + "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20T22:12:21.000000Z\",\"LBUpdated\":\"2021-06-14T08:18:19.000000Z\",\"TotalEntries\":12," + "\"Entries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1615654895}," + "{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2,\"Index\":6,\"DateSubmitted\":1600604303}]" + "}}"; + + memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 1234); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2345); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 1); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description"); + ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); + ASSERT_PTR_NULL(fetch_leaderboard_info_response.author); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.total_entries, 12); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1382307141); + ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1623658699); + + entry = &fetch_leaderboard_info_response.entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 8765); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + entry = &fetch_leaderboard_info_response.entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 7654); + ASSERT_NUM_EQUALS(entry->submitted, 1600604303); + + rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); +} + +static void test_init_fetch_games_list_request() { + rc_api_fetch_games_list_request_t fetch_games_list_request; + rc_api_request_t request; + + memset(&fetch_games_list_request, 0, sizeof(fetch_games_list_request)); + fetch_games_list_request.console_id = 12; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_games_list_request(&request, &fetch_games_list_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameslist&c=12"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_games_list_response() { + rc_api_fetch_games_list_response_t fetch_games_list_response; + rc_api_game_list_entry_t* entry; + const char* server_response = "{\"Success\":true,\"Response\":{" + "\"1234\":\"Game Name 1\"," + "\"17\":\"Game Name 2\"," + "\"9923\":\"Game Name 3\"," + "\"12303\":\"Game Name 4\"," + "\"4338\":\"Game Name 5\"," + "\"5437\":\"Game Name 6\"" + "}}"; + + memset(&fetch_games_list_response, 0, sizeof(fetch_games_list_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_games_list_response(&fetch_games_list_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_games_list_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_games_list_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_games_list_response.num_entries, 6); + + entry = &fetch_games_list_response.entries[0]; + ASSERT_NUM_EQUALS(entry->id, 1234); + ASSERT_STR_EQUALS(entry->name, "Game Name 1"); + entry = &fetch_games_list_response.entries[1]; + ASSERT_NUM_EQUALS(entry->id, 17); + ASSERT_STR_EQUALS(entry->name, "Game Name 2"); + entry = &fetch_games_list_response.entries[2]; + ASSERT_NUM_EQUALS(entry->id, 9923); + ASSERT_STR_EQUALS(entry->name, "Game Name 3"); + entry = &fetch_games_list_response.entries[3]; + ASSERT_NUM_EQUALS(entry->id, 12303); + ASSERT_STR_EQUALS(entry->name, "Game Name 4"); + entry = &fetch_games_list_response.entries[4]; + ASSERT_NUM_EQUALS(entry->id, 4338); + ASSERT_STR_EQUALS(entry->name, "Game Name 5"); + entry = &fetch_games_list_response.entries[5]; + ASSERT_NUM_EQUALS(entry->id, 5437); + ASSERT_STR_EQUALS(entry->name, "Game Name 6"); + + rc_api_destroy_fetch_games_list_response(&fetch_games_list_response); +} + +static void test_init_fetch_game_titles_request() { + rc_api_fetch_game_titles_request_t fetch_game_titles_request; + rc_api_request_t request; + uint32_t game_ids[] = { 3, 4, 7, 10 }; + + memset(&fetch_game_titles_request, 0, sizeof(fetch_game_titles_request)); + fetch_game_titles_request.game_ids = game_ids; + fetch_game_titles_request.num_game_ids = 3; /* purposefully only ask for 3/4 */ + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_titles_request(&request, &fetch_game_titles_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameinfolist&g=3,4,7"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_game_titles_response() { + rc_api_fetch_game_titles_response_t fetch_game_titles_response; + rc_api_server_response_t fetch_game_titles_server_response; + rc_api_game_title_entry_t* entry; + const char* server_response = "{\"Success\":true,\"Response\":[" + "{\"ID\": 3, \"Title\":\"Game Name 3\", \"ImageIcon\": \"/Images/010003.png\"}," + "{\"ID\": 4, \"Title\":\"Game Name 4\", \"ImageIcon\": \"/Images/010004.png\"}," + "{\"ID\": 7, \"Title\":\"Game Name 7\", \"ImageIcon\": \"/Images/010007.png\"}" + "]}"; + + memset(&fetch_game_titles_response, 0, sizeof(fetch_game_titles_response)); + memset(&fetch_game_titles_server_response, 0, sizeof(fetch_game_titles_server_response)); + fetch_game_titles_server_response.body = server_response; + fetch_game_titles_server_response.body_length = strlen(server_response); + fetch_game_titles_server_response.http_status_code = 200; + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_titles_server_response(&fetch_game_titles_response, &fetch_game_titles_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_titles_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_titles_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_titles_response.num_entries, 3); + + entry = &fetch_game_titles_response.entries[0]; + ASSERT_NUM_EQUALS(entry->id, 3); + ASSERT_STR_EQUALS(entry->title, "Game Name 3"); + ASSERT_STR_EQUALS(entry->image_name, "010003"); + entry = &fetch_game_titles_response.entries[1]; + ASSERT_NUM_EQUALS(entry->id, 4); + ASSERT_STR_EQUALS(entry->title, "Game Name 4"); + ASSERT_STR_EQUALS(entry->image_name, "010004"); + entry = &fetch_game_titles_response.entries[2]; + ASSERT_NUM_EQUALS(entry->id, 7); + ASSERT_STR_EQUALS(entry->title, "Game Name 7"); + ASSERT_STR_EQUALS(entry->image_name, "010007"); + + rc_api_destroy_fetch_game_titles_response(&fetch_game_titles_response); +} + +static void test_init_fetch_hash_library_request() { + rc_api_fetch_hash_library_request_t fetch_hash_library_request; + rc_api_request_t request; + + memset(&fetch_hash_library_request, 0, sizeof(fetch_hash_library_request)); + fetch_hash_library_request.console_id = 1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_hash_library_request(&request, &fetch_hash_library_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=hashlibrary&c=1"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_hash_library_response() { + rc_api_fetch_hash_library_response_t fetch_hash_library_response; + rc_api_server_response_t fetch_hash_library_server_response; + rc_api_hash_library_entry_t* entry; + const char* server_response = "{\"Success\":true,\"MD5List\":{" + "\"aabbccddeeff00112233445566778899\":1," + "\"99aabbccddeeff001122334455667788\":1," + "\"8899aabbccddeeff0011223344556677\":2" + "}}"; + + memset(&fetch_hash_library_server_response, 0, sizeof(fetch_hash_library_server_response)); + fetch_hash_library_server_response.body = server_response; + fetch_hash_library_server_response.body_length = strlen(server_response); + fetch_hash_library_server_response.http_status_code = 200; + + memset(&fetch_hash_library_response, 0, sizeof(fetch_hash_library_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_hash_library_server_response(&fetch_hash_library_response, &fetch_hash_library_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_hash_library_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_hash_library_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_hash_library_response.num_entries, 3); + + entry = &fetch_hash_library_response.entries[0]; + ASSERT_NUM_EQUALS(entry->game_id, 1); + ASSERT_STR_EQUALS(entry->hash, "aabbccddeeff00112233445566778899"); + entry = &fetch_hash_library_response.entries[1]; + ASSERT_NUM_EQUALS(entry->game_id, 1); + ASSERT_STR_EQUALS(entry->hash, "99aabbccddeeff001122334455667788"); + entry = &fetch_hash_library_response.entries[2]; + ASSERT_NUM_EQUALS(entry->game_id, 2); + ASSERT_STR_EQUALS(entry->hash, "8899aabbccddeeff0011223344556677"); + + rc_api_destroy_fetch_hash_library_response(&fetch_hash_library_response); +} + +static void test_process_fetch_hash_library_empty_response() { + rc_api_fetch_hash_library_response_t fetch_hash_library_response; + rc_api_server_response_t fetch_hash_library_server_response; + const char* server_response = "{\"Success\":true,\"MD5List\":[]}"; /* RAWeb returns a list instead of a map for invalid console */ + + memset(&fetch_hash_library_server_response, 0, sizeof(fetch_hash_library_server_response)); + fetch_hash_library_server_response.body = server_response; + fetch_hash_library_server_response.body_length = strlen(server_response); + fetch_hash_library_server_response.http_status_code = 200; + + memset(&fetch_hash_library_response, 0, sizeof(fetch_hash_library_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_hash_library_server_response(&fetch_hash_library_response, &fetch_hash_library_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_hash_library_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_hash_library_response.response.error_message); + ASSERT_PTR_NULL(fetch_hash_library_response.entries); + ASSERT_NUM_EQUALS(fetch_hash_library_response.num_entries, 0); + + rc_api_destroy_fetch_hash_library_response(&fetch_hash_library_response); +} + +void test_rapi_info(void) { + TEST_SUITE_BEGIN(); + + /* achievement info */ + TEST(test_init_fetch_achievement_info_request); + TEST(test_init_fetch_achievement_info_request_no_first); + TEST(test_init_fetch_achievement_info_request_one_first); + TEST(test_init_fetch_achievement_info_request_friends_only); + + TEST(test_process_fetch_achievement_info_response); + + /* leaderboard info */ + TEST(test_init_fetch_leaderboard_info_request); + TEST(test_init_fetch_leaderboard_info_request_no_first); + TEST(test_init_fetch_leaderboard_info_request_for_user); + TEST(test_init_fetch_leaderboard_info_request_for_user_with_offset); + + TEST(test_process_fetch_leaderboard_info_response); + TEST(test_process_fetch_leaderboard_info_response2); + TEST(test_process_fetch_leaderboard_info_response_iso8601); + + /* games list */ + TEST(test_init_fetch_games_list_request); + + TEST(test_process_fetch_games_list_response); + + /* game titles */ + TEST(test_init_fetch_game_titles_request); + + TEST(test_process_fetch_game_titles_response); + + /* hash library */ + TEST(test_init_fetch_hash_library_request); + + TEST(test_process_fetch_hash_library_response); + TEST(test_process_fetch_hash_library_empty_response); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_runtime.c b/src/rcheevos/test/rapi/test_rc_api_runtime.c new file mode 100644 index 0000000000..64f137a058 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_runtime.c @@ -0,0 +1,2213 @@ +#include "rc_api_runtime.h" + +#include "rc_error.h" +#include "rc_runtime_types.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void init_server_response(rc_api_server_response_t* server_response, + int status_code, const char* body, size_t body_length) { + memset(server_response, 0, sizeof(*server_response)); + server_response->body = body; + server_response->body_length = body_length; + server_response->http_status_code = status_code; +} + +/* ----- resolve hash ----- */ + +static void test_init_resolve_hash_request() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.username = "Username"; /* credentials are ignored - turns out server doesn't validate this API */ + resolve_hash_request.api_token = "API_TOKEN"; + resolve_hash_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameid&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_resolve_hash_request_no_credentials() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=gameid&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_resolve_hash_request_no_hash() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_resolve_hash_request_empty_hash() { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = ""; + + ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_resolve_hash_response_match() { + rc_api_resolve_hash_response_t resolve_hash_response; + const char* server_response = "{\"Success\":true,\"GameID\":1446}"; + + memset(&resolve_hash_response, 0, sizeof(resolve_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_resolve_hash_response(&resolve_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(resolve_hash_response.response.succeeded, 1); + ASSERT_PTR_NULL(resolve_hash_response.response.error_message); + ASSERT_NUM_EQUALS(resolve_hash_response.game_id, 1446); + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +static void test_process_resolve_hash_response_no_match() { + rc_api_resolve_hash_response_t resolve_hash_response; + const char* server_response = "{\"Success\":true,\"GameID\":0}"; + + memset(&resolve_hash_response, 0, sizeof(resolve_hash_response)); + + ASSERT_NUM_EQUALS(rc_api_process_resolve_hash_response(&resolve_hash_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(resolve_hash_response.response.succeeded, 1); + ASSERT_PTR_NULL(resolve_hash_response.response.error_message); + ASSERT_NUM_EQUALS(resolve_hash_response.game_id, 0); + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +/* ----- fetch game data ----- */ + +static void test_init_fetch_game_data_request() { + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_request_t request; + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = "Username"; + fetch_game_data_request.api_token = "API_TOKEN"; + fetch_game_data_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_data_request_by_hash() { + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_request_t request; + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = "Username"; + fetch_game_data_request.api_token = "API_TOKEN"; + fetch_game_data_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_data_request_by_id_and_hash() { + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_request_t request; + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = "Username"; + fetch_game_data_request.api_token = "API_TOKEN"; + fetch_game_data_request.game_id = 1234; + fetch_game_data_request.game_hash = "ABCDEF0123456789"; /* ignored when game_id provided */ + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_data_request_no_id() { + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_request_t request; + + memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); + fetch_game_data_request.username = "Username"; + fetch_game_data_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_game_data_response_empty() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"My Game\",\"ConsoleID\":23,\"ImageIcon\":\"/Images/012345.png\"," + "\"Achievements\":[],\"Leaderboards\":[]" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 177); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "My Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 23); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "012345"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_invalid_credentials() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_game_data_response.response.error_message, "Credentials invalid (0)"); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 0); + ASSERT_PTR_NULL(fetch_game_data_response.title); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 0); + ASSERT_PTR_NULL(fetch_game_data_response.image_name); + ASSERT_PTR_NULL(fetch_game_data_response.rich_presence_script); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_not_found() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Unknown game\",\"Code\":\"not_found\",\"Status\":404}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_NOT_FOUND); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_game_data_response.response.error_message, "Unknown game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 0); + ASSERT_PTR_NULL(fetch_game_data_response.title); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 0); + ASSERT_PTR_NULL(fetch_game_data_response.image_name); + ASSERT_PTR_NULL(fetch_game_data_response.rich_presence_script); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_achievements() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\"," + "\"Created\":1376969412,\"Modified\":1376969412}," + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," + "\"Created\":1504474554,\"Modified\":1504474554}" + "],\"Leaderboards\":[]" + "}}"; + rc_api_achievement_definition_t* achievement; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); + achievement = fetch_game_data_response.achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_STR_EQUALS(achievement->description, "Desc1"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_STR_EQUALS(achievement->description, "Desc2"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 2); + ASSERT_STR_EQUALS(achievement->definition, "0=2"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->created, 1376970283); + ASSERT_NUM_EQUALS(achievement->updated, 1376970283); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_STR_EQUALS(achievement->description, "Desc3"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); + ASSERT_NUM_EQUALS(achievement->points, 0); + ASSERT_STR_EQUALS(achievement->definition, "0=3"); + ASSERT_STR_EQUALS(achievement->author, "User2"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->created, 1376969412); + ASSERT_NUM_EQUALS(achievement->updated, 1376969412); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_STR_EQUALS(achievement->description, "Desc4"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_achievement_types() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Type\":\"\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Type\":\"missable\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Type\":\"progression\"," + "\"Created\":1376969412,\"Modified\":1376969412}," + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"win_condition\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5505,\"Title\":\"Ach5 [m]\",\"Description\":\"Desc5\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5506,\"Title\":\"[m] Ach6\",\"Description\":\"Desc6\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}" + "],\"Leaderboards\":[]" + "}}"; + rc_api_achievement_definition_t* achievement; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 7); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); + achievement = fetch_game_data_response.achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_PROGRESSION); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_WIN); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5505); + ASSERT_STR_EQUALS(achievement->title, "Ach5"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5506); + ASSERT_STR_EQUALS(achievement->title, "Ach6"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5507); + ASSERT_STR_EQUALS(achievement->title, "Ach7"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_achievement_rarity() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Rarity\":100.0,\"RarityHardcore\":66.67," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Rarity\":57.43,\"RarityHardcore\":57.43," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Rarity\":6.8,\"RarityHardcore\":0," + "\"Created\":1376969412,\"Modified\":1376969412}," + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\"," + "\"Created\":1376969412,\"Modified\":1376969412}" + "],\"Leaderboards\":[]" + "}}"; + rc_api_achievement_definition_t* achievement; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); + achievement = fetch_game_data_response.achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.67f); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_FLOAT_EQUALS(achievement->rarity, 57.43f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 57.43f); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_FLOAT_EQUALS(achievement->rarity, 6.8f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 0.0f); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_achievement_null_author() +{ + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"BadgeURL\":\"http://host/Badge/00234.png\",\"BadgeLockedURL\":\"http://host/Badge/00234_lock.png\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":null,\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":null,\"BadgeName\":\"00236\"," + "\"Created\":1376969412,\"Modified\":1376969412}," + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," + "\"Created\":1504474554,\"Modified\":1504474554}" + "],\"Leaderboards\":[]" + "}}"; + rc_api_achievement_definition_t* achievement; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_data_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); + achievement = fetch_game_data_response.achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_STR_EQUALS(achievement->description, "Desc1"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_STR_EQUALS(achievement->badge_url, "http://host/Badge/00234.png"); + ASSERT_STR_EQUALS(achievement->badge_locked_url, "http://host/Badge/00234_lock.png"); + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_STR_EQUALS(achievement->description, "Desc2"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 2); + ASSERT_STR_EQUALS(achievement->definition, "0=2"); + ASSERT_STR_EQUALS(achievement->author, ""); + ASSERT_STR_EQUALS(achievement->badge_name, "00235"); + ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/00235.png"); + ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/00235_lock.png"); + ASSERT_NUM_EQUALS(achievement->created, 1376970283); + ASSERT_NUM_EQUALS(achievement->updated, 1376970283); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_STR_EQUALS(achievement->description, "Desc3"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); + ASSERT_NUM_EQUALS(achievement->points, 0); + ASSERT_STR_EQUALS(achievement->definition, "0=3"); + ASSERT_STR_EQUALS(achievement->author, ""); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/00236.png"); + ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/00236_lock.png"); + ASSERT_NUM_EQUALS(achievement->created, 1376969412); + ASSERT_NUM_EQUALS(achievement->updated, 1376969412); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_STR_EQUALS(achievement->description, "Desc4"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/00236.png"); + ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/00236_lock.png"); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_leaderboards() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[],\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}," + "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," + "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}," + "{\"ID\":4403,\"Title\":\"Leaderboard3\",\"Description\":\"Desc3\"," + "\"Mem\":\"0=1\",\"Format\":\"UNKNOWN\",\"LowerIsBetter\":true,\"Hidden\":false}" + "]}}"; + rc_api_leaderboard_definition_t* leaderboard; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 3); + + ASSERT_PTR_NOT_NULL(fetch_game_data_response.leaderboards); + leaderboard = fetch_game_data_response.leaderboards; + + ASSERT_NUM_EQUALS(leaderboard->id, 4401); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + ++leaderboard; + ASSERT_NUM_EQUALS(leaderboard->id, 4402); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 1); + + ++leaderboard; + ASSERT_NUM_EQUALS(leaderboard->id, 4403); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard3"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc3"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_VALUE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_rich_presence() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," + "\"ImageIconURL\":\"http://host/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]," + "\"RichPresencePatch\":\"Display:\\r\\nTest\\r\\n\"" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_data_response.image_url, "http://host/Images/000001.png"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, "Display:\r\nTest\r\n"); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_rich_presence_null() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]," + "\"RichPresencePatch\":null" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_data_response.image_url, "https://media.retroachievements.org/Images/000001.png"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void test_process_fetch_game_data_response_rich_presence_tab() { + rc_api_fetch_game_data_response_t fetch_game_data_response; + const char* server_response = "{\"Success\":true,\"PatchData\":{" + "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]," + "\"RichPresencePatch\":\"Display:\\r\\nTest\\tTab\\r\\n\"" + "}}"; + + memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); + ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_data_response.image_url, "https://media.retroachievements.org/Images/000001.png"); + ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, "Display:\r\nTest\tTab\r\n"); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); + ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +/* ----- fetch game sets ----- */ + +static void test_init_fetch_game_sets_request() { + rc_api_fetch_game_sets_request_t fetch_game_sets_request; + rc_api_request_t request; + + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = "Username"; + fetch_game_sets_request.api_token = "API_TOKEN"; + fetch_game_sets_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_sets_request_no_hash() { + rc_api_fetch_game_sets_request_t fetch_game_sets_request; + rc_api_request_t request; + + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = "Username"; + fetch_game_sets_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_sets_request_by_id() { + rc_api_fetch_game_sets_request_t fetch_game_sets_request; + rc_api_request_t request; + + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = "Username"; + fetch_game_sets_request.api_token = "API_TOKEN"; + fetch_game_sets_request.game_id = 953; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_game_sets_request_by_hash_and_id() { + rc_api_fetch_game_sets_request_t fetch_game_sets_request; + rc_api_request_t request; + + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = "Username"; + fetch_game_sets_request.api_token = "API_TOKEN"; + fetch_game_sets_request.game_id = 953; + fetch_game_sets_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_fetch_game_sets_response_empty() { + rc_api_achievement_set_definition_t* set; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":177,\"Title\":\"My Game\",\"ConsoleId\":23," + "\"ImageIconUrl\":\"http://server/Images/012345.png\"," + "\"RichPresenceGameId\":177,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":192,\"GameId\":177,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/012345.png\"," + "\"Achievements\":[],\"Leaderboards\":[]" + "}]}"; + + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 177); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "My Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 23); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "012345"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/012345.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 177); + + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 192); + ASSERT_NUM_EQUALS(set->game_id, 177); + ASSERT_STR_EQUALS(set->title, "My Game"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); + ASSERT_STR_EQUALS(set->image_name, "012345"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/012345.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 0); + ASSERT_NUM_EQUALS(set->num_leaderboards, 0); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_invalid_credentials() { + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 403, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_game_sets_response.response.error_message, "Credentials invalid (0)"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 0); + ASSERT_PTR_NULL(fetch_game_sets_response.title); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 0); + ASSERT_PTR_NULL(fetch_game_sets_response.image_name); + ASSERT_PTR_NULL(fetch_game_sets_response.rich_presence_script); + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 0); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_not_found() { + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":false,\"Error\":\"Unknown game\",\"Code\":\"not_found\",\"Status\":404}"; + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 404, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_NOT_FOUND); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_game_sets_response.response.error_message, "Unknown game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 0); + ASSERT_PTR_NULL(fetch_game_sets_response.title); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 0); + ASSERT_PTR_NULL(fetch_game_sets_response.image_name); + ASSERT_PTR_NULL(fetch_game_sets_response.rich_presence_script); + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 0); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_achievements() { + rc_api_achievement_set_definition_t* set; + rc_api_achievement_definition_t* achievement; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":20,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":192,\"GameId\":20,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Type\":\"\"," + "\"Rarity\":100.0,\"RarityHardcore\":66.67,\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Type\":\"missable\"," + "\"Rarity\":57.43,\"RarityHardcore\":57.43,\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Type\":\"progression\"," + "\"Rarity\":6.8,\"RarityHardcore\":0,\"Created\":1376969412,\"Modified\":1376969412}," + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":null,\"BadgeName\":\"00236\",\"Type\":\"win_condition\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5505,\"Title\":\"Ach5 [m]\",\"Description\":\"Desc5\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5506,\"Title\":\"[m] Ach6\",\"Description\":\"Desc6\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}" + "],\"Leaderboards\":[]" + "}]}"; + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 20); + + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 192); + ASSERT_NUM_EQUALS(set->game_id, 20); + ASSERT_STR_EQUALS(set->title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); + ASSERT_STR_EQUALS(set->image_name, "112233"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112233.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 7); + ASSERT_NUM_EQUALS(set->num_leaderboards, 0); + + ASSERT_PTR_NOT_NULL(set->achievements); + achievement = set->achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_STR_EQUALS(achievement->description, "Desc1"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.67f); + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_STR_EQUALS(achievement->description, "Desc2"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 2); + ASSERT_STR_EQUALS(achievement->definition, "0=2"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 57.43f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 57.43f); + ASSERT_NUM_EQUALS(achievement->created, 1376970283); + ASSERT_NUM_EQUALS(achievement->updated, 1376970283); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_STR_EQUALS(achievement->description, "Desc3"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); + ASSERT_NUM_EQUALS(achievement->points, 0); + ASSERT_STR_EQUALS(achievement->definition, "0=3"); + ASSERT_STR_EQUALS(achievement->author, "User2"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_PROGRESSION); + ASSERT_FLOAT_EQUALS(achievement->rarity, 6.8f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 0.0f); + ASSERT_NUM_EQUALS(achievement->created, 1376969412); + ASSERT_NUM_EQUALS(achievement->updated, 1376969412); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_STR_EQUALS(achievement->description, "Desc4"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, ""); /* null author */ + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_WIN); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5505); + ASSERT_STR_EQUALS(achievement->title, "Ach5"); /* [m] stripped */ + ASSERT_STR_EQUALS(achievement->description, "Desc5"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5506); + ASSERT_STR_EQUALS(achievement->title, "Ach6"); /* [m] stripped */ + ASSERT_STR_EQUALS(achievement->description, "Desc6"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5507); + ASSERT_STR_EQUALS(achievement->title, "Ach7"); + ASSERT_STR_EQUALS(achievement->description, "Desc7"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); /* no type specified */ + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_leaderboards() { + rc_api_achievement_set_definition_t* set; + rc_api_leaderboard_definition_t* leaderboard; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":20,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":192,\"GameId\":20,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[],\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}," + "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," + "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}," + "{\"ID\":4403,\"Title\":\"Leaderboard3\",\"Description\":\"Desc3\"," + "\"Mem\":\"0=1\",\"Format\":\"UNKNOWN\",\"LowerIsBetter\":true,\"Hidden\":false}" + "]" + "}]}"; + + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 20); + + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 192); + ASSERT_NUM_EQUALS(set->game_id, 20); + ASSERT_STR_EQUALS(set->title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); + ASSERT_STR_EQUALS(set->image_name, "112233"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112233.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 0); + ASSERT_NUM_EQUALS(set->num_leaderboards, 3); + + ASSERT_PTR_NOT_NULL(set->leaderboards); + leaderboard = set->leaderboards; + + ASSERT_NUM_EQUALS(leaderboard->id, 4401); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + ++leaderboard; + ASSERT_NUM_EQUALS(leaderboard->id, 4402); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 1); + + ++leaderboard; + ASSERT_NUM_EQUALS(leaderboard->id, 4403); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard3"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc3"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_VALUE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_rich_presence() { + rc_api_achievement_set_definition_t* set; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":99,\"Title\":\"Some Other Game\",\"ConsoleId\":2," + "\"ImageIconUrl\":\"http://server/Images/000001.png\"," + "\"RichPresenceGameId\":99,\"RichPresencePatch\":\"Display:\\r\\nTest\\r\\n\",\"Sets\":[{" + "\"AchievementSetId\":106,\"GameId\":99,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]" + "}]}"; + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 99); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/000001.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, "Display:\r\nTest\r\n"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 99); + + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 106); + ASSERT_NUM_EQUALS(set->game_id, 99); + ASSERT_STR_EQUALS(set->title, "Some Other Game"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); + ASSERT_STR_EQUALS(set->image_name, "000001"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/000001.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 0); + ASSERT_NUM_EQUALS(set->num_leaderboards, 0); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_rich_presence_null() { + rc_api_achievement_set_definition_t* set; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":99,\"Title\":\"Some Other Game\",\"ConsoleId\":2," + "\"ImageIconUrl\":\"http://server/Images/000001.png\"," + "\"RichPresenceGameId\":99,\"RichPresencePatch\":null,\"Sets\":[{" + "\"AchievementSetId\":106,\"GameId\":99,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/000001.png\"," + "\"Achievements\":[],\"Leaderboards\":[]" + "}]}"; + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 99); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Some Other Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 2); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "000001"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/000001.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 99); + + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 106); + ASSERT_NUM_EQUALS(set->game_id, 99); + ASSERT_STR_EQUALS(set->title, "Some Other Game"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); + ASSERT_STR_EQUALS(set->image_name, "000001"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/000001.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 0); + ASSERT_NUM_EQUALS(set->num_leaderboards, 0); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_specialty_subset() { + rc_api_achievement_set_definition_t* set; + rc_api_achievement_definition_t* achievement; + rc_api_leaderboard_definition_t* leaderboard; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":20,\"RichPresencePatch\":\"\",\"Sets\":[" + "{" + "\"AchievementSetId\":98,\"GameId\":26,\"Title\":\"Low Level Run\",\"Type\":\"specialty\"," + "\"ImageIconUrl\":\"http://server/Images/112236.png\"," + "\"Achievements\":[" + "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}" + "],\"Leaderboards\":[]" + "},{" + "\"AchievementSetId\":192,\"GameId\":20,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Type\":\"progression\"," + "\"Rarity\":100.0,\"RarityHardcore\":66.67,\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Type\":\"missable\"," + "\"Rarity\":57.43,\"RarityHardcore\":57.43,\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," + "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Type\":\"win_condition\"," + "\"Rarity\":6.8,\"RarityHardcore\":0,\"Created\":1376969412,\"Modified\":1376969412}" + "],\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}" + "]" + "},{" + "\"AchievementSetId\":77,\"GameId\":21,\"Title\":\"Bonus\",\"Type\":\"bonus\"," + "\"ImageIconUrl\":\"http://server/Images/112236.png\"," + "\"Achievements\":[" + "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":null,\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5505,\"Title\":\"Ach5 [m]\",\"Description\":\"Desc5\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}," + "{\"ID\":5506,\"Title\":\"[m] Ach6\",\"Description\":\"Desc6\",\"Flags\":3,\"Points\":10," + "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," + "\"Created\":1504474554,\"Modified\":1504474554}" + "],\"Leaderboards\":[" + "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," + "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}" + "]" + "}" + "]}"; + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 20); + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 3); + + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 98); + ASSERT_NUM_EQUALS(set->game_id, 26); + ASSERT_STR_EQUALS(set->title, "Low Level Run"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_SPECIALTY); + ASSERT_STR_EQUALS(set->image_name, "112236"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112236.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 1); + ASSERT_NUM_EQUALS(set->num_leaderboards, 0); + + ASSERT_PTR_NOT_NULL(set->achievements); + achievement = set->achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5507); + ASSERT_STR_EQUALS(achievement->title, "Ach7"); + ASSERT_STR_EQUALS(achievement->description, "Desc7"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); /* no type specified */ + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + set = &fetch_game_sets_response.sets[1]; + ASSERT_NUM_EQUALS(set->id, 192); + ASSERT_NUM_EQUALS(set->game_id, 20); + ASSERT_STR_EQUALS(set->title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); + ASSERT_STR_EQUALS(set->image_name, "112233"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112233.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 3); + ASSERT_NUM_EQUALS(set->num_leaderboards, 1); + + ASSERT_PTR_NOT_NULL(set->achievements); + achievement = set->achievements; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_STR_EQUALS(achievement->title, "Ach1"); + ASSERT_STR_EQUALS(achievement->description, "Desc1"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_PROGRESSION); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.67f); + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_STR_EQUALS(achievement->title, "Ach2"); + ASSERT_STR_EQUALS(achievement->description, "Desc2"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 2); + ASSERT_STR_EQUALS(achievement->definition, "0=2"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 57.43f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 57.43f); + ASSERT_NUM_EQUALS(achievement->created, 1376970283); + ASSERT_NUM_EQUALS(achievement->updated, 1376970283); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5503); + ASSERT_STR_EQUALS(achievement->title, "Ach3"); + ASSERT_STR_EQUALS(achievement->description, "Desc3"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); + ASSERT_NUM_EQUALS(achievement->points, 0); + ASSERT_STR_EQUALS(achievement->definition, "0=3"); + ASSERT_STR_EQUALS(achievement->author, "User2"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_WIN); + ASSERT_FLOAT_EQUALS(achievement->rarity, 6.8f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 0.0f); + ASSERT_NUM_EQUALS(achievement->created, 1376969412); + ASSERT_NUM_EQUALS(achievement->updated, 1376969412); + + ASSERT_PTR_NOT_NULL(set->leaderboards); + leaderboard = set->leaderboards; + + ASSERT_NUM_EQUALS(leaderboard->id, 4401); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 0); + + set = &fetch_game_sets_response.sets[2]; + ASSERT_NUM_EQUALS(set->id, 77); + ASSERT_NUM_EQUALS(set->game_id, 21); + ASSERT_STR_EQUALS(set->title, "Bonus"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_BONUS); + ASSERT_STR_EQUALS(set->image_name, "112236"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112236.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 3); + ASSERT_NUM_EQUALS(set->num_leaderboards, 1); + + ASSERT_PTR_NOT_NULL(set->achievements); + achievement = set->achievements; + ASSERT_NUM_EQUALS(achievement->id, 5504); + ASSERT_STR_EQUALS(achievement->title, "Ach4"); + ASSERT_STR_EQUALS(achievement->description, "Desc4"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, ""); /* null author */ + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5505); + ASSERT_STR_EQUALS(achievement->title, "Ach5"); /* [m] stripped */ + ASSERT_STR_EQUALS(achievement->description, "Desc5"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + ++achievement; + ASSERT_NUM_EQUALS(achievement->id, 5506); + ASSERT_STR_EQUALS(achievement->title, "Ach6"); /* [m] stripped */ + ASSERT_STR_EQUALS(achievement->description, "Desc6"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 10); + ASSERT_STR_EQUALS(achievement->definition, "0=4"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00236"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); + ASSERT_NUM_EQUALS(achievement->created, 1504474554); + ASSERT_NUM_EQUALS(achievement->updated, 1504474554); + + ASSERT_PTR_NOT_NULL(set->leaderboards); + leaderboard = set->leaderboards; + + ASSERT_NUM_EQUALS(leaderboard->id, 4402); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); + ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); + ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); + ASSERT_NUM_EQUALS(leaderboard->hidden, 1); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static void test_process_fetch_game_sets_response_exclusive_subset() { + rc_api_achievement_set_definition_t* set; + rc_api_achievement_definition_t* achievement; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + const char server_response[] = "{\"Success\":true," + "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":26,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":98,\"GameId\":26,\"Title\":\"Low Level Run\",\"Type\":\"exclusive\"," + "\"ImageIconUrl\":\"http://server/Images/112236.png\"," + "\"Achievements\":[" + "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}" + "],\"Leaderboards\":[]" + "}]}"; + rc_api_server_response_t fetch_game_sets_server_response; + init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); + + memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); + ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); + ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); + ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); + ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); + ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 26); + ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); + + set = &fetch_game_sets_response.sets[0]; + ASSERT_NUM_EQUALS(set->id, 98); + ASSERT_NUM_EQUALS(set->game_id, 26); + ASSERT_STR_EQUALS(set->title, "Low Level Run"); + ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE); + ASSERT_STR_EQUALS(set->image_name, "112236"); + ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112236.png"); + ASSERT_NUM_EQUALS(set->num_achievements, 1); + ASSERT_NUM_EQUALS(set->num_leaderboards, 0); + + ASSERT_PTR_NOT_NULL(set->achievements); + achievement = set->achievements; + + ASSERT_NUM_EQUALS(achievement->id, 5507); + ASSERT_STR_EQUALS(achievement->title, "Ach7"); + ASSERT_STR_EQUALS(achievement->description, "Desc7"); + ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_STR_EQUALS(achievement->definition, "0=1"); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_STR_EQUALS(achievement->badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); /* no type specified */ + ASSERT_NUM_EQUALS(achievement->created, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated, 1376929305); + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +/* ----- ping ----- */ + +static void test_init_ping_request() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_no_game_id() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_rich_presence() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + ping_request.rich_presence = "Level 1, 70% complete"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&m=Level+1%2c+70%25+complete"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_rich_presence_unicode() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1446; + ping_request.rich_presence = "\xf0\x9f\x9a\xb6:3, 1st Quest"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1446&m=%f0%9f%9a%b6%3a3%2c+1st+Quest"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_rich_presence_empty() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + ping_request.rich_presence = ""; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_game_hash_softcore() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + ping_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&h=0&x=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_ping_request_game_hash_hardcore() { + rc_api_ping_request_t ping_request; + rc_api_request_t request; + + memset(&ping_request, 0, sizeof(ping_request)); + ping_request.username = "Username"; + ping_request.api_token = "API_TOKEN"; + ping_request.game_id = 1234; + ping_request.hardcore = 1; + ping_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&h=1&x=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_ping_response() { + rc_api_ping_response_t ping_response; + const char* server_response = "{\"Success\":true}"; + + memset(&ping_response, 0, sizeof(ping_response)); + + ASSERT_NUM_EQUALS(rc_api_process_ping_response(&ping_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(ping_response.response.succeeded, 1); + ASSERT_PTR_NULL(ping_response.response.error_message); + + rc_api_destroy_ping_response(&ping_response); +} + +/* ----- award achievement ----- */ + +static void test_init_award_achievement_request_hardcore() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 1234; + award_achievement_request.hardcore = 1; + award_achievement_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=1&m=ABCDEF0123456789&v=b8aefaad6f9659e2164bc60da0c3b64d"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_non_hardcore() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 1234; + award_achievement_request.hardcore = 0; + award_achievement_request.game_hash = "ABABCBCBDEDEFFFF"; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=0&m=ABABCBCBDEDEFFFF&v=ed81d6ecf825f8cbe3ae1edace098892"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_no_hash() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 5432; + award_achievement_request.hardcore = 1; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=5432&h=1&v=31048257ab1788386e71ab0c222aa5c8"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_no_achievement_id() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.hardcore = 1; + award_achievement_request.game_hash = "ABABCBCBDEDEFFFF"; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_award_achievement_request_delayed() { + rc_api_award_achievement_request_t award_achievement_request; + rc_api_request_t request; + + memset(&award_achievement_request, 0, sizeof(award_achievement_request)); + award_achievement_request.username = "Username"; + award_achievement_request.api_token = "API_TOKEN"; + award_achievement_request.achievement_id = 1234; + award_achievement_request.hardcore = 1; + award_achievement_request.game_hash = "ABCDEF0123456789"; + award_achievement_request.seconds_since_unlock = 17; + + ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=1&m=ABCDEF0123456789&o=17&v=b2326b09d61e9264eb5d3607d947317d"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_award_achievement_response_success() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":true,\"Score\":119102,\"SoftcoreScore\":777,\"AchievementID\":56481,\"AchievementsRemaining\":11}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119102); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56481); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 11); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_hardcore_already_unlocked() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494,\"AchievementsRemaining\":17}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has hardcore and regular achievements awarded."); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 17); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_non_hardcore_already_unlocked() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"User already has this achievement awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has this achievement awarded."); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_generic_failure() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_empty() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = ""; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_NO_RESPONSE); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_invalid_credentials() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Credentials invalid (0)"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_text() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "You do not have access to that resource"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "You do not have access to that resource"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_no_fields() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "{\"Success\":true}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); + ASSERT_PTR_NULL(award_achievement_response.response.error_message); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_429() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = + "\n" + "429 Too Many Requests\n" + "\n" + "

429 Too Many Requests

\n" + "
nginx
\n" + "\n" + ""; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "429 Too Many Requests"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_429_json() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = + "{\"Success\": false,\"Error\":\"Too Many Attempts\"}"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Too Many Attempts"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_503_fancy() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = + "\n" + "\n" + "\n" + " \n" + " \n" + " \n" + " 503 Service Temporarily Unavailable\n" + "\n" + "\n" + "
\n" + "\n" + ""; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "503 Service Temporarily Unavailable"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +static void test_process_award_achievement_response_522_simple() { + rc_api_award_achievement_response_t award_achievement_response; + const char* server_response = "error code: 522"; + + memset(&award_achievement_response, 0, sizeof(award_achievement_response)); + + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); + ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "error code: 522"); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); + + rc_api_destroy_award_achievement_response(&award_achievement_response); +} + +/* ----- submit lboard entry ----- */ + +static void test_init_submit_lboard_entry_request() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1234; + submit_lboard_entry_request.score = 10999; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1234&s=10999&m=ABCDEF0123456789&v=e13c9132ee651256f9d2ee8f06f75d76"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_zero_value() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1111; + submit_lboard_entry_request.score = 0; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1111&s=0&m=ABCDEF0123456789&v=9c2ac665157d68b8a26e83bb71dd8aaf"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_negative_value() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1111; + submit_lboard_entry_request.score = -234781; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1111&s=-234781&m=ABCDEF0123456789&v=fbe290266f2d121a7a37942e1e90f453"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_no_leaderboard_id() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.score = 12345; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_submit_lboard_entry_request_delayed() { + rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; + rc_api_request_t request; + + memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); + submit_lboard_entry_request.username = "Username"; + submit_lboard_entry_request.api_token = "API_TOKEN"; + submit_lboard_entry_request.leaderboard_id = 1234; + submit_lboard_entry_request.score = 10999; + submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; + submit_lboard_entry_request.seconds_since_completion = 33; + + ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1234&s=10999&m=ABCDEF0123456789&o=33&v=7971fc37cf38026f99dd4bae84360ac1"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_submit_lb_entry_response_success() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + rc_api_lboard_entry_t* entry; + const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1},{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 1); + ASSERT_PTR_NULL(submit_lb_entry_response.response.error_message); + ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 1234); + ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 2345); + ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 5); + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 17); + + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 2); + entry = &submit_lb_entry_response.top_entries[0]; + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_STR_EQUALS(entry->username, "Player1"); + ASSERT_NUM_EQUALS(entry->score, 8765); + entry = &submit_lb_entry_response.top_entries[1]; + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_STR_EQUALS(entry->username, "Player2"); + ASSERT_NUM_EQUALS(entry->score, 7654); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +static void test_process_submit_lb_entry_response_no_entries() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," + "\"TopEntries\":[]," + "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 1); + ASSERT_PTR_NULL(submit_lb_entry_response.response.error_message); + ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 1234); + ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 2345); + ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 5); + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 17); + + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 0); + ASSERT_PTR_NULL(submit_lb_entry_response.top_entries); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +static void test_process_submit_lb_entry_response_invalid_credentials() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 0); + ASSERT_STR_EQUALS(submit_lb_entry_response.response.error_message, "Credentials invalid (0)"); + ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 0); + ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 0); + ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 0); + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 0); + + ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 0); + ASSERT_PTR_NULL(submit_lb_entry_response.top_entries); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +static void test_process_submit_lb_entry_response_entries_not_array() { + rc_api_submit_lboard_entry_response_t submit_lb_entry_response; + const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," + "\"TopEntries\":{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1}," + "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; + + memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); + + ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 0); + ASSERT_STR_EQUALS(submit_lb_entry_response.response.error_message, "TopEntries not found in response"); + + rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); +} + +/* ----- harness ----- */ + +void test_rapi_runtime(void) { + TEST_SUITE_BEGIN(); + + /* gameid */ + TEST(test_init_resolve_hash_request); + TEST(test_init_resolve_hash_request_no_credentials); + TEST(test_init_resolve_hash_request_no_hash); + TEST(test_init_resolve_hash_request_empty_hash); + + TEST(test_process_resolve_hash_response_match); + TEST(test_process_resolve_hash_response_no_match); + + /* patch */ + TEST(test_init_fetch_game_data_request); + TEST(test_init_fetch_game_data_request_no_id); + TEST(test_init_fetch_game_data_request_by_hash); + TEST(test_init_fetch_game_data_request_by_id_and_hash); + + TEST(test_process_fetch_game_data_response_empty); + TEST(test_process_fetch_game_data_response_invalid_credentials); + TEST(test_process_fetch_game_data_response_not_found); + TEST(test_process_fetch_game_data_response_achievements); + TEST(test_process_fetch_game_data_response_achievement_types); + TEST(test_process_fetch_game_data_response_achievement_rarity); + TEST(test_process_fetch_game_data_response_achievement_null_author); + TEST(test_process_fetch_game_data_response_leaderboards); + TEST(test_process_fetch_game_data_response_rich_presence); + TEST(test_process_fetch_game_data_response_rich_presence_null); + TEST(test_process_fetch_game_data_response_rich_presence_tab); + + /* hashdata */ + TEST(test_init_fetch_game_sets_request); + TEST(test_init_fetch_game_sets_request_no_hash); + TEST(test_init_fetch_game_sets_request_by_id); + TEST(test_init_fetch_game_sets_request_by_hash_and_id); + + TEST(test_process_fetch_game_sets_response_empty); + TEST(test_process_fetch_game_sets_response_invalid_credentials); + TEST(test_process_fetch_game_sets_response_not_found); + TEST(test_process_fetch_game_sets_response_achievements); + TEST(test_process_fetch_game_sets_response_leaderboards); + TEST(test_process_fetch_game_sets_response_rich_presence); + TEST(test_process_fetch_game_sets_response_rich_presence_null); + TEST(test_process_fetch_game_sets_response_specialty_subset); + TEST(test_process_fetch_game_sets_response_exclusive_subset); + + /* ping */ + TEST(test_init_ping_request); + TEST(test_init_ping_request_no_game_id); + TEST(test_init_ping_request_rich_presence); + TEST(test_init_ping_request_rich_presence_unicode); + TEST(test_init_ping_request_rich_presence_empty); + TEST(test_init_ping_request_game_hash_softcore); + TEST(test_init_ping_request_game_hash_hardcore); + + TEST(test_process_ping_response); + + /* awardachievement */ + TEST(test_init_award_achievement_request_hardcore); + TEST(test_init_award_achievement_request_non_hardcore); + TEST(test_init_award_achievement_request_no_hash); + TEST(test_init_award_achievement_request_no_achievement_id); + TEST(test_init_award_achievement_request_delayed); + + TEST(test_process_award_achievement_response_success); + TEST(test_process_award_achievement_response_hardcore_already_unlocked); + TEST(test_process_award_achievement_response_non_hardcore_already_unlocked); + TEST(test_process_award_achievement_response_generic_failure); + TEST(test_process_award_achievement_response_empty); + TEST(test_process_award_achievement_response_invalid_credentials); + TEST(test_process_award_achievement_response_text); + TEST(test_process_award_achievement_response_no_fields); + TEST(test_process_award_achievement_response_429); + TEST(test_process_award_achievement_response_429_json); + TEST(test_process_award_achievement_response_503_fancy); + TEST(test_process_award_achievement_response_522_simple); + + /* submitlbentry */ + TEST(test_init_submit_lboard_entry_request); + TEST(test_init_submit_lboard_entry_request_zero_value); + TEST(test_init_submit_lboard_entry_request_negative_value); + TEST(test_init_submit_lboard_entry_request_no_leaderboard_id); + TEST(test_init_submit_lboard_entry_request_delayed); + + TEST(test_process_submit_lb_entry_response_success); + TEST(test_process_submit_lb_entry_response_no_entries); + TEST(test_process_submit_lb_entry_response_invalid_credentials); + TEST(test_process_submit_lb_entry_response_entries_not_array); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rapi/test_rc_api_user.c b/src/rcheevos/test/rapi/test_rc_api_user.c new file mode 100644 index 0000000000..9853469985 --- /dev/null +++ b/src/rcheevos/test/rapi/test_rc_api_user.c @@ -0,0 +1,998 @@ +#include "rc_api_user.h" + +#include "../src/rapi/rc_api_common.h" +#include "../test_framework.h" +#include "../rc_compat.h" +#include "../rc_version.h" + +#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" + +static void test_init_start_session_request() +{ + rc_api_start_session_request_t start_session_request; + rc_api_request_t request; + + memset(&start_session_request, 0, sizeof(start_session_request)); + start_session_request.username = "Username"; + start_session_request.api_token = "API_TOKEN"; + start_session_request.game_id = 1234; + + ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&l=" RCHEEVOS_VERSION_STRING); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_start_session_request_no_game() +{ + rc_api_start_session_request_t start_session_request; + rc_api_request_t request; + + memset(&start_session_request, 0, sizeof(start_session_request)); + start_session_request.username = "Username"; + start_session_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_start_session_request_game_hash_softcore() +{ + rc_api_start_session_request_t start_session_request; + rc_api_request_t request; + + memset(&start_session_request, 0, sizeof(start_session_request)); + start_session_request.username = "Username"; + start_session_request.api_token = "API_TOKEN"; + start_session_request.game_id = 1234; + start_session_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&h=0&m=ABCDEF0123456789&l=" RCHEEVOS_VERSION_STRING); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_start_session_request_game_hash_hardcore() +{ + rc_api_start_session_request_t start_session_request; + rc_api_request_t request; + + memset(&start_session_request, 0, sizeof(start_session_request)); + start_session_request.username = "Username"; + start_session_request.api_token = "API_TOKEN"; + start_session_request.game_id = 1234; + start_session_request.hardcore = 1; + start_session_request.game_hash = "ABCDEF0123456789"; + + ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&h=1&m=ABCDEF0123456789&l=" RCHEEVOS_VERSION_STRING); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_process_start_session_response_legacy() +{ + rc_api_start_session_response_t start_session_response; + const char* server_response = "{\"Success\":true}"; + + memset(&start_session_response, 0, sizeof(start_session_response)); + + ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 1); + ASSERT_PTR_NULL(start_session_response.response.error_message); + ASSERT_NUM_EQUALS(start_session_response.num_unlocks, 0); + ASSERT_NUM_EQUALS(start_session_response.num_hardcore_unlocks, 0); + ASSERT_NUM_EQUALS(start_session_response.server_now, 0); + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void test_process_start_session_response() +{ + rc_api_start_session_response_t start_session_response; + /* startsession API only returns HardcoreUnlocks if an achievement has been earned in hardcore, + * even if the softcore unlock has a different timestamp. Unlocks are only returned for things + * only unlocked in softcore. */ + const char* server_response = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":111,\"When\":1234567890}," + "{\"ID\":112,\"When\":1234567891}," + "{\"ID\":113,\"When\":1234567860}" + "],\"Unlocks\":[" + "{\"ID\":114,\"When\":1234567840}" + "],\"ServerNow\":1234577777}"; + + memset(&start_session_response, 0, sizeof(start_session_response)); + + ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 1); + ASSERT_PTR_NULL(start_session_response.response.error_message); + ASSERT_NUM_EQUALS(start_session_response.num_unlocks, 1); + ASSERT_NUM_EQUALS(start_session_response.unlocks[0].achievement_id, 114); + ASSERT_NUM_EQUALS(start_session_response.unlocks[0].when, 1234567840); + ASSERT_NUM_EQUALS(start_session_response.num_hardcore_unlocks, 3); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[0].achievement_id, 111); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[0].when, 1234567890); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[1].achievement_id, 112); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[1].when, 1234567891); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[2].achievement_id, 113); + ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[2].when, 1234567860); + ASSERT_NUM_EQUALS(start_session_response.server_now, 1234577777); + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void test_process_start_session_response_invalid_credentials() +{ + rc_api_start_session_response_t start_session_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + + memset(&start_session_response, 0, sizeof(start_session_response)); + + ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 0); + ASSERT_STR_EQUALS(start_session_response.response.error_message, "Credentials invalid (0)"); + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void test_init_login_request_password() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.password = "Pa$$w0rd!"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&p=Pa%24%24w0rd%21"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_password_long() +{ + char buffer[1024], *ptr, *password_start; + rc_api_login_request_t login_request; + rc_api_request_t request; + int i; + + /* this generates a password that's 830 characters long */ + ptr = password_start = buffer + snprintf(buffer, sizeof(buffer), "r=login2&u=ThisUsernameIsAlsoReallyLongAtRoughlyFiftyCharacters&p="); + for (i = 0; i < 30; i++) + ptr += snprintf(ptr, sizeof(buffer) - (ptr - buffer), "%dABCDEFGHIJKLMNOPQRSTUVWXYZ", i); + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "ThisUsernameIsAlsoReallyLongAtRoughlyFiftyCharacters"; + login_request.password = password_start; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, buffer); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_token() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.api_token = "ABCDEFGHIJKLMNOP"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&t=ABCDEFGHIJKLMNOP"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_password_and_token() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.password = "Pa$$w0rd!"; + login_request.api_token = "ABCDEFGHIJKLMNOP"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&p=Pa%24%24w0rd%21"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_no_password_or_token() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_INVALID_STATE); + + rc_api_destroy_request(&request); +} + +static void test_init_login_request_alternate_host() +{ + rc_api_login_request_t login_request; + rc_api_request_t request; + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = "Username"; + login_request.password = "Pa$$w0rd!"; + + rc_api_set_host("localhost"); + ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); + ASSERT_STR_EQUALS(request.url, "http://localhost/dorequest.php"); + ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&p=Pa%24%24w0rd%21"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_set_host(NULL); + rc_api_destroy_request(&request); +} + +static void test_process_login_response_success() +{ + rc_api_login_response_t login_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 200; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_unique_display_name() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"GamingHero\",\"AvatarUrl\":\"http://host/UserPic/USER.png\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "GamingHero"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); + ASSERT_STR_EQUALS(login_response.display_name, "GamingHero"); + ASSERT_STR_EQUALS(login_response.avatar_url, "http://host/UserPic/USER.png"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_invalid_credentials() +{ + rc_api_login_response_t login_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\", \"Code\":\"invalid_credentials\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_CREDENTIALS); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Invalid User/Password combination. Please try again"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 401; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_INVALID_CREDENTIALS); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Invalid User/Password combination. Please try again"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_access_denied() +{ + rc_api_login_response_t login_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":false,\"Error\":\"Access denied.\",\"Code\":\"access_denied\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_ACCESS_DENIED); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Access denied."); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 403; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_ACCESS_DENIED); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Access denied."); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_expired_token() +{ + rc_api_login_response_t login_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":false,\"Error\":\"The access token has expired. Please log in again.\",\"Code\":\"expired_token\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_EXPIRED_TOKEN); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "The access token has expired. Please log in again."); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 401; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_EXPIRED_TOKEN); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "The access token has expired. Please log in again."); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_generic_failure() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":false}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_empty() +{ + rc_api_login_response_t login_response; + const char* server_response = ""; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_NO_RESPONSE); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_text() +{ + rc_api_login_response_t login_response; + const char* server_response = "You do not have access to that resource"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "You do not have access to that resource"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_html() +{ + rc_api_login_response_t login_response; + const char* server_response = "You do not have access to that resource"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "You do not have access to that resource"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_no_required_fields() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "User not found in response"); + ASSERT_PTR_NULL(login_response.username); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_no_token() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"Username\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); + ASSERT_STR_EQUALS(login_response.response.error_message, "Token not found in response"); + ASSERT_STR_EQUALS(login_response.username, "Username"); + ASSERT_PTR_NULL(login_response.api_token); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_PTR_NULL(login_response.display_name); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_no_optional_fields() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\"}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_process_login_response_null_score() +{ + rc_api_login_response_t login_response; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":null,\"SoftcoreScore\":null}"; + + memset(&login_response, 0, sizeof(login_response)); + + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); + ASSERT_PTR_NULL(login_response.response.error_message); + ASSERT_STR_EQUALS(login_response.username, "USER"); + ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); + ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); + ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); + ASSERT_STR_EQUALS(login_response.display_name, "USER"); + + rc_api_destroy_login_response(&login_response); +} + +static void test_init_fetch_user_unlocks_request_non_hardcore() +{ + rc_api_fetch_user_unlocks_request_t fetch_user_unlocks_request; + rc_api_request_t request; + + memset(&fetch_user_unlocks_request, 0, sizeof(fetch_user_unlocks_request)); + fetch_user_unlocks_request.username = "Username"; + fetch_user_unlocks_request.api_token = "API_TOKEN"; + fetch_user_unlocks_request.game_id = 1234; + fetch_user_unlocks_request.hardcore = 0; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_user_unlocks_request(&request, &fetch_user_unlocks_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=unlocks&u=Username&t=API_TOKEN&g=1234&h=0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_user_unlocks_request_hardcore() +{ + rc_api_fetch_user_unlocks_request_t fetch_user_unlocks_request; + rc_api_request_t request; + + memset(&fetch_user_unlocks_request, 0, sizeof(fetch_user_unlocks_request)); + fetch_user_unlocks_request.username = "Username"; + fetch_user_unlocks_request.api_token = "API_TOKEN"; + fetch_user_unlocks_request.game_id = 2345; + fetch_user_unlocks_request.hardcore = 1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_user_unlocks_request(&request, &fetch_user_unlocks_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=unlocks&u=Username&t=API_TOKEN&g=2345&h=1"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_user_unlocks_response_empty_array() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":true,\"UserUnlocks\":[],\"GameID\":11277,\"HardcoreMode\":false}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); + ASSERT_PTR_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 0); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_user_unlocks_response_invalid_credentials() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_user_unlocks_response.response.error_message, "Credentials invalid (0)"); + ASSERT_PTR_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 0); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_user_unlocks_response_one_item() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":true,\"UserUnlocks\":[1234],\"GameID\":11277,\"HardcoreMode\":false}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 1); + ASSERT_PTR_NOT_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[0], 1234); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_user_unlocks_response_several_items() +{ + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + const char* server_response = "{\"Success\":true,\"UserUnlocks\":[1,2,3,4],\"GameID\":11277,\"HardcoreMode\":false}"; + memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 4); + ASSERT_PTR_NOT_NULL(fetch_user_unlocks_response.achievement_ids); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[0], 1); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[1], 2); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[2], 3); + ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[3], 4); + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void test_init_fetch_followed_users_request() +{ + rc_api_fetch_followed_users_request_t fetch_followed_users_request; + rc_api_request_t request; + + memset(&fetch_followed_users_request, 0, sizeof(fetch_followed_users_request)); + fetch_followed_users_request.username = "Username"; + fetch_followed_users_request.api_token = "API_TOKEN"; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_followed_users_request(&request, &fetch_followed_users_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=getfriendlist&u=Username&t=API_TOKEN"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_followed_users_response_empty_array() +{ + rc_api_fetch_followed_users_response_t fetch_followed_users_response; + rc_api_server_response_t server_response; + memset(&fetch_followed_users_response, 0, sizeof(fetch_followed_users_response)); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = "{\"Success\":true,\"Friends\":[]}"; + server_response.body_length = strlen(server_response.body); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_followed_users_server_response(&fetch_followed_users_response, &server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_followed_users_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_followed_users_response.response.error_message); + ASSERT_PTR_NULL(fetch_followed_users_response.users); + ASSERT_NUM_EQUALS(fetch_followed_users_response.num_users, 0); + + rc_api_destroy_fetch_followed_users_response(&fetch_followed_users_response); +} + +static void test_init_fetch_followed_users_response_invalid_credentials() +{ + rc_api_fetch_followed_users_response_t fetch_followed_users_response; + rc_api_server_response_t server_response; + memset(&fetch_followed_users_response, 0, sizeof(fetch_followed_users_response)); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + server_response.body_length = strlen(server_response.body); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_followed_users_server_response(&fetch_followed_users_response, &server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_followed_users_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_followed_users_response.response.error_message, "Credentials invalid (0)"); + ASSERT_PTR_NULL(fetch_followed_users_response.users); + ASSERT_NUM_EQUALS(fetch_followed_users_response.num_users, 0); + + rc_api_destroy_fetch_followed_users_response(&fetch_followed_users_response); +} + +static void test_init_fetch_followed_users_response_several_items() +{ + rc_api_fetch_followed_users_response_t fetch_followed_users_response; + rc_api_server_response_t server_response; + memset(&fetch_followed_users_response, 0, sizeof(fetch_followed_users_response)); + + memset(&server_response, 0, sizeof(server_response)); + server_response.body = "{\"Success\":true,\"Friends\":[" + "{\"Friend\":\"Bob\",\"AvatarUrl\":\"/User/Bob.png\",\"RAPoints\":1234,\"LastSeen\":\"Doing stuff\"}," /* legacy format */ + "{\"Friend\":\"Jane\",\"AvatarUrl\":\"/User/Jane.png\",\"RAPoints\":5,\"LastSeen\":\"Winning\"," + "\"LastSeenTime\":1234567890,\"LastGameId\":6,\"LastGameTitle\":\"The Game\",\"LastGameIconUrl\":\"/Badges/000006.png\"}," + "{\"Friend\":\"Bill\",\"AvatarUrl\":\"/User/Bill.png\",\"RAPoints\":0,\"LastSeen\":\"Unknown\"," + "\"LastSeenTime\":1234567800,\"LastGameId\":null,\"LastGameTitle\":null,\"LastGameIconUrl\":null}" + "]}"; + server_response.body_length = strlen(server_response.body); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_followed_users_server_response(&fetch_followed_users_response, &server_response), RC_OK); + ASSERT_NUM_EQUALS(fetch_followed_users_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_followed_users_response.response.error_message); + ASSERT_PTR_NOT_NULL(fetch_followed_users_response.users); + ASSERT_NUM_EQUALS(fetch_followed_users_response.num_users, 3); + + ASSERT_STR_EQUALS(fetch_followed_users_response.users[0].display_name, "Bob"); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[0].avatar_url, "/User/Bob.png"); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[0].score, 1234); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[0].recent_activity.description, "Doing stuff"); + ASSERT_PTR_NULL(fetch_followed_users_response.users[0].recent_activity.context); + ASSERT_PTR_NULL(fetch_followed_users_response.users[0].recent_activity.context_image_url); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[0].recent_activity.context_id, 0); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[0].recent_activity.when, 0); + + ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].display_name, "Jane"); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].avatar_url, "/User/Jane.png"); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[1].score, 5); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].recent_activity.description, "Winning"); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].recent_activity.context, "The Game"); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].recent_activity.context_image_url, "/Badges/000006.png"); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[1].recent_activity.context_id, 6); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[1].recent_activity.when, 1234567890); + + ASSERT_STR_EQUALS(fetch_followed_users_response.users[2].display_name, "Bill"); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[2].avatar_url, "/User/Bill.png"); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[2].score, 0); + ASSERT_STR_EQUALS(fetch_followed_users_response.users[2].recent_activity.description, "Unknown"); + ASSERT_PTR_NULL(fetch_followed_users_response.users[2].recent_activity.context); + ASSERT_PTR_NULL(fetch_followed_users_response.users[2].recent_activity.context_image_url); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[2].recent_activity.context_id, 0); + ASSERT_NUM_EQUALS(fetch_followed_users_response.users[2].recent_activity.when, 1234567800); + + rc_api_destroy_fetch_followed_users_response(&fetch_followed_users_response); +} + +static void test_init_fetch_all_user_progress_request() +{ + rc_api_fetch_all_user_progress_request_t fetch_all_user_progress_request; + rc_api_request_t request; + + memset(&fetch_all_user_progress_request, 0, sizeof(fetch_all_user_progress_request)); + fetch_all_user_progress_request.username = "Username"; + fetch_all_user_progress_request.api_token = "API_TOKEN"; + fetch_all_user_progress_request.console_id = 1; + + ASSERT_NUM_EQUALS(rc_api_init_fetch_all_user_progress_request(&request, &fetch_all_user_progress_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=allprogress&u=Username&t=API_TOKEN&c=1"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_fetch_all_user_progress_response_empty_array() +{ + rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":true,\"Response\":[]}"; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 200; + + memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); + + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_all_user_progress_response.response.error_message); + ASSERT_PTR_NULL(fetch_all_user_progress_response.entries); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 0); + + rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); +} + +static void test_init_fetch_all_user_progress_response_invalid_credentials() +{ + rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 200; + + memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); + + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 0); + ASSERT_STR_EQUALS(fetch_all_user_progress_response.response.error_message, "Credentials invalid (0)"); + ASSERT_PTR_NULL(fetch_all_user_progress_response.entries); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 0); + + rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); +} + +static void test_init_fetch_all_user_progress_response_one_item() +{ + rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; + rc_api_server_response_t response_obj; + const char* server_response = "{\"Success\":true,\"Response\":{\"10\":{\"Achievements\":11}}}"; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 200; + + memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_all_user_progress_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 1); + ASSERT_PTR_NOT_NULL(fetch_all_user_progress_response.entries); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].game_id, 10); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_achievements, 11); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements, 0); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements_hardcore, 0); + + rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); +} + +static void test_init_fetch_all_user_progress_response_several_items() +{ + rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; + rc_api_server_response_t response_obj; + + const char* server_response = "{\"Success\":true,\"Response\":{\"10\":{\"Achievements\":11}," + "\"20\":{\"Achievements\":21,\"Unlocked\":22}," + "\"30\":{\"Achievements\":31,\"Unlocked\":32,\"UnlockedHardcore\":33}," + "\"40\":{\"Achievements\":41,\"UnlockedHardcore\":43}}}"; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + response_obj.http_status_code = 200; + + memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); + + ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 1); + ASSERT_PTR_NULL(fetch_all_user_progress_response.response.error_message); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 4); + ASSERT_PTR_NOT_NULL(fetch_all_user_progress_response.entries); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].game_id, 10); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_achievements, 11); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements, 0); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements_hardcore, 0); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].game_id, 20); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].num_achievements, 21); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].num_unlocked_achievements, 22); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].num_unlocked_achievements_hardcore, 0); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].game_id, 30); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].num_achievements, 31); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].num_unlocked_achievements, 32); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].num_unlocked_achievements_hardcore, 33); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].game_id, 40); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].num_achievements, 41); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].num_unlocked_achievements, 0); + ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].num_unlocked_achievements_hardcore, 43); + + rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); +} + +void test_rapi_user(void) { + TEST_SUITE_BEGIN(); + + /* start session */ + TEST(test_init_start_session_request); + TEST(test_init_start_session_request_no_game); + TEST(test_init_start_session_request_game_hash_softcore); + TEST(test_init_start_session_request_game_hash_hardcore); + + TEST(test_process_start_session_response_legacy); + TEST(test_process_start_session_response); + TEST(test_process_start_session_response_invalid_credentials); + + /* login */ + TEST(test_init_login_request_password); + TEST(test_init_login_request_password_long); + TEST(test_init_login_request_token); + TEST(test_init_login_request_password_and_token); + TEST(test_init_login_request_no_password_or_token); + TEST(test_init_login_request_alternate_host); + + TEST(test_process_login_response_success); + TEST(test_process_login_response_unique_display_name); + TEST(test_process_login_response_invalid_credentials); + TEST(test_process_login_response_access_denied); + TEST(test_process_login_response_generic_failure); + TEST(test_process_login_response_expired_token); + TEST(test_process_login_response_empty); + TEST(test_process_login_response_text); + TEST(test_process_login_response_html); + TEST(test_process_login_response_no_required_fields); + TEST(test_process_login_response_no_token); + TEST(test_process_login_response_no_optional_fields); + TEST(test_process_login_response_null_score); + + /* unlocks */ + TEST(test_init_fetch_user_unlocks_request_non_hardcore); + TEST(test_init_fetch_user_unlocks_request_hardcore); + + TEST(test_init_fetch_user_unlocks_response_empty_array); + TEST(test_init_fetch_user_unlocks_response_invalid_credentials); + TEST(test_init_fetch_user_unlocks_response_one_item); + TEST(test_init_fetch_user_unlocks_response_several_items); + + /* followed users */ + TEST(test_init_fetch_followed_users_request); + + TEST(test_init_fetch_followed_users_response_empty_array); + TEST(test_init_fetch_followed_users_response_invalid_credentials); + TEST(test_init_fetch_followed_users_response_several_items); + + /* all user progress */ + TEST(test_init_fetch_all_user_progress_request); + + TEST(test_init_fetch_all_user_progress_response_empty_array); + TEST(test_init_fetch_all_user_progress_response_invalid_credentials); + TEST(test_init_fetch_all_user_progress_response_one_item); + TEST(test_init_fetch_all_user_progress_response_several_items); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos-test.sln b/src/rcheevos/test/rcheevos-test.sln new file mode 100644 index 0000000000..5335edb284 --- /dev/null +++ b/src/rcheevos/test/rcheevos-test.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rcheevos-test", "rcheevos-test.vcxproj", "{74FBBFC4-5AC5-4A86-B292-B2F535E9912C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{900F3B07-2B47-4967-8296-CEC29233458F}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "validator", "..\validator\validator.vcxproj", "{16FABFA7-A2EC-4CD0-9E04-50315A2BB613}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x64.ActiveCfg = Debug|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x64.Build.0 = Debug|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x86.ActiveCfg = Debug|Win32 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x86.Build.0 = Debug|Win32 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x64.ActiveCfg = Release|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x64.Build.0 = Release|x64 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x86.ActiveCfg = Release|Win32 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x86.Build.0 = Release|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x64.ActiveCfg = Debug|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x64.Build.0 = Debug|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x86.ActiveCfg = Debug|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x86.Build.0 = Debug|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x64.ActiveCfg = Release|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x64.Build.0 = Release|x64 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x86.ActiveCfg = Release|Win32 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {02ADF144-5109-40C0-AA02-5BC5585A9883} + EndGlobalSection +EndGlobal diff --git a/src/rcheevos/test/rcheevos-test.vcxproj b/src/rcheevos/test/rcheevos-test.vcxproj new file mode 100644 index 0000000000..232635308d --- /dev/null +++ b/src/rcheevos/test/rcheevos-test.vcxproj @@ -0,0 +1,239 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {74FBBFC4-5AC5-4A86-B292-B2F535E9912C} + rcheevostest + Application + MultiByte + v143 + + + v142 + + + v141 + + + + true + + + false + true + + + true + + + false + true + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + true + _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) + + + Console + + + + + Level3 + Disabled + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + true + _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) + + + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + true + _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) + + + true + true + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;%(AdditionalIncludeDirectories) + true + _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/rcheevos/test/rcheevos-test.vcxproj.filters b/src/rcheevos/test/rcheevos-test.vcxproj.filters new file mode 100644 index 0000000000..5a8d4ec7b6 --- /dev/null +++ b/src/rcheevos/test/rcheevos-test.vcxproj.filters @@ -0,0 +1,335 @@ + + + + + {c1b8966b-c711-43ce-ac8a-1dcca6b41ff3} + + + {43b6b53b-c37e-453e-97bf-df56c515f1a7} + + + {9060be9f-a1f8-4940-a6e6-8ada5c89ae94} + + + {faf643ae-e095-4db5-a701-3ea9ed343cc0} + + + {f890b4f1-8de5-4730-b612-3a7dbf65ca74} + + + {21341552-6b14-4f5b-a26c-d9393ffbdbcc} + + + {d049182b-0721-46b5-b93e-6f8f7f572b45} + + + {0b946a47-0089-4118-a5b2-cd57245dc58b} + + + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rhash + + + tests\rcheevos + + + tests + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + src\rhash + + + tests\rhash + + + tests\rhash + + + tests\rcheevos + + + tests\rcheevos + + + src\rcheevos + + + tests\rcheevos + + + src\rcheevos + + + tests\rcheevos + + + src\rhash + + + tests\rhash + + + tests\rhash + + + src\rapi + + + src\rapi + + + src\rapi + + + tests\rapi + + + tests\rapi + + + tests\rapi + + + src\rapi + + + tests\rapi + + + src\rapi + + + tests\rapi + + + src\rcheevos + + + tests\rcheevos + + + tests\rcheevos + + + src + + + src + + + src + + + tests + + + tests + + + src + + + src + + + tests + + + tests + + + src + + + src\rhash + + + tests\rhash + + + src\rhash + + + src\rhash + + + tests\rhash + + + src\rhash + + + tests\rhash + + + src\rhash + + + src\rcheevos + + + + + src\rcheevos + + + tests\rcheevos + + + tests + + + tests\rhash + + + src\rhash + + + tests\rhash + + + src\rhash + + + src\rcheevos + + + src\rapi + + + src\rapi + + + src\rapi + + + src\rapi + + + tests\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rapi + + + src\rapi + + + src\rcheevos + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src\rhash + + + src\rhash + + + src + + + + + src\rcheevos + + + tests\rcheevos + + + src + + + \ No newline at end of file diff --git a/src/rcheevos/test/rcheevos/mock_memory.h b/src/rcheevos/test/rcheevos/mock_memory.h new file mode 100644 index 0000000000..388c752324 --- /dev/null +++ b/src/rcheevos/test/rcheevos/mock_memory.h @@ -0,0 +1,32 @@ +#ifndef MOCK_MEMORY_H +#define MOCK_MEMORY_H + +typedef struct { + uint8_t* ram; + uint32_t size; +} +memory_t; + +static uint32_t peekb(uint32_t address, memory_t* memory) { + return address < memory->size ? memory->ram[address] : 0; +} + +static uint32_t peek(uint32_t address, uint32_t num_bytes, void* ud) { + memory_t* memory = (memory_t*)ud; + + switch (num_bytes) { + case 1: return peekb(address, memory); + + case 2: return peekb(address, memory) | + peekb(address + 1, memory) << 8; + + case 4: return peekb(address, memory) | + peekb(address + 1, memory) << 8 | + peekb(address + 2, memory) << 16 | + peekb(address + 3, memory) << 24; + } + + return 0; +} + +#endif /* MOCK_MEMORY_H */ diff --git a/src/rcheevos/test/rcheevos/test_condition.c b/src/rcheevos/test/rcheevos/test_condition.c new file mode 100644 index 0000000000..412cc11123 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_condition.c @@ -0,0 +1,570 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_operand(rc_operand_t* self, uint8_t expected_type, uint8_t expected_size, uint32_t expected_address) { + ASSERT_NUM_EQUALS(self->type, expected_type); + + switch (expected_type) { + case RC_OPERAND_ADDRESS: + case RC_OPERAND_DELTA: + case RC_OPERAND_PRIOR: + ASSERT_NUM_EQUALS(self->size, expected_size); + ASSERT_NUM_EQUALS(self->value.memref->address, expected_address); + break; + + case RC_OPERAND_CONST: + ASSERT_NUM_EQUALS(self->value.num, expected_address); + break; + } +} +#define assert_operand(operand, expected_type, expected_size, expected_address) ASSERT_HELPER(_assert_operand(operand, expected_type, expected_size, expected_address), "assert_operand") + +static void _assert_parse_condition( + const char* memaddr, uint8_t expected_type, + uint8_t expected_left_type, uint8_t expected_left_size, uint32_t expected_left_value, + uint8_t expected_operator, + uint8_t expected_right_type, uint8_t expected_right_size, uint32_t expected_right_value, + uint32_t expected_required_hits +) { + rc_condition_t* self; + rc_parse_state_t parse; + rc_memrefs_t memrefs; + char buffer[512]; + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + self = rc_parse_condition(&memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_EQUALS(self->type, expected_type); + assert_operand(&self->operand1, expected_left_type, expected_left_size, expected_left_value); + ASSERT_NUM_EQUALS(self->oper, expected_operator); + assert_operand(&self->operand2, expected_right_type, expected_right_size, expected_right_value); + ASSERT_NUM_EQUALS(self->required_hits, expected_required_hits); +} +#define assert_parse_condition(memaddr, expected_type, expected_left_type, expected_left_size, expected_left_value, \ + expected_operator, expected_right_type, expected_right_size, expected_right_value, expected_required_hits) \ + ASSERT_HELPER(_assert_parse_condition(memaddr, expected_type, expected_left_type, expected_left_size, expected_left_value, \ + expected_operator, expected_right_type, expected_right_size, expected_right_value, expected_required_hits), "assert_parse_condition") + +static void test_parse_condition(const char* memaddr, uint8_t expected_type, uint8_t expected_left_type, + uint8_t expected_operator, uint32_t expected_required_hits) { + if (expected_operator == RC_OPERATOR_NONE) { + assert_parse_condition(memaddr, expected_type, + expected_left_type, RC_MEMSIZE_8_BITS, 0x1234U, + expected_operator, + RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U, + expected_required_hits + ); + } + else { + assert_parse_condition(memaddr, expected_type, + expected_left_type, RC_MEMSIZE_8_BITS, 0x1234U, + expected_operator, + RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 8U, + expected_required_hits + ); + } +} + +static void test_parse_operands(const char* memaddr, + uint8_t expected_left_type, uint8_t expected_left_size, uint32_t expected_left_value, + uint8_t expected_right_type, uint8_t expected_right_size, uint32_t expected_right_value) { + assert_parse_condition(memaddr, RC_CONDITION_STANDARD, + expected_left_type, expected_left_size, expected_left_value, + RC_OPERATOR_EQ, + expected_right_type, expected_right_size, expected_right_value, + 0 + ); +} + +static void test_parse_modifier(const char* memaddr, uint8_t expected_operator, uint8_t expected_operand, double expected_multiplier) { + assert_parse_condition(memaddr, RC_CONDITION_ADD_SOURCE, + RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, + expected_operator, + expected_operand, RC_MEMSIZE_8_BITS, (int)expected_multiplier, + 0 + ); +} + +static void test_parse_modifier_shorthand(const char* memaddr, uint8_t expected_type) { + assert_parse_condition(memaddr, expected_type, + RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, + RC_OPERATOR_NONE, + RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1U, + 0 + ); +} + +static void test_parse_condition_error(const char* memaddr, int expected_error) { + if (expected_error == RC_OK) { + ASSERT_NUM_GREATER(rc_trigger_size(memaddr), 0); + } else { + ASSERT_NUM_EQUALS(rc_trigger_size(memaddr), expected_error); + } +} + +static int evaluate_condition(rc_condition_t* cond, memory_t* memory, rc_memrefs_t* memrefs) { + rc_eval_state_t eval_state; + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + rc_update_memref_values(memrefs, peek, memory); + return rc_test_condition(cond, &eval_state); +} + +static void test_evaluate_condition(const char* memaddr, uint8_t expected_comparator, int expected_result) { + rc_condition_t* self; + rc_parse_state_t parse; + char buffer[512]; + rc_memrefs_t memrefs; + int ret; + uint8_t ram[] = {0x00, 0x11, 0x34, 0xAB, 0x56}; + memory_t memory; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + self = rc_parse_condition(&memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*memaddr, 0); + + rc_update_memref_values(&memrefs, peek, &memory); /* capture delta for ram[1] */ + ram[1] = 0x12; + + ASSERT_NUM_EQUALS(self->optimized_comparator, expected_comparator); + ret = evaluate_condition(self, &memory, &memrefs); + + if (expected_result) { + ASSERT_NUM_EQUALS(ret, 1); + } else { + ASSERT_NUM_EQUALS(ret, 0); + } +} + +static void test_default_comparator(const char* memaddr) { + rc_condset_t* condset; + rc_condition_t* condition; + rc_parse_state_t parse; + char buffer[512]; + rc_memrefs_t memrefs; + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + condset = rc_parse_condset(&memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*memaddr, 0); + + condition = condset->conditions; + while (condition->next) + condition = condition->next; + + /* expect last condition to have default comparator - that's the point of this test */ + ASSERT_NUM_EQUALS(condition->optimized_comparator, RC_PROCESSING_COMPARE_DEFAULT); +} + +static void test_evaluate_condition_float(const char* memaddr, int expected_result) { + rc_condition_t* self; + rc_parse_state_t parse; + char buffer[512]; + rc_memrefs_t memrefs; + int ret; + uint8_t ram[] = {0x00, 0x00, 0x00, 0x40, 0x83, 0x49, 0x0F, 0xDB}; /* FF0=2, FF4=2*pi */ + memory_t memory; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + self = rc_parse_condition(&memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*memaddr, 0); + + ret = evaluate_condition(self, &memory, &memrefs); + + if (expected_result) { + ASSERT_NUM_EQUALS(ret, 1); + } else { + ASSERT_NUM_EQUALS(ret, 0); + } +} + +static void test_condition_compare_delta() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condition_t* cond; + rc_parse_state_t parse; + char buffer[512]; + rc_memrefs_t memrefs; + + const char* cond_str = "0xH0001>d0xH0001"; + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + cond = rc_parse_condition(&cond_str, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*cond_str, 0); + memory.ram = ram; + memory.size = sizeof(ram); + + /* initial delta value is 0, 0x12 > 0 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* delta value is now 0x12, 0x12 = 0x12 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* delta value is now 0x12, 0x11 < 0x12 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* delta value is now 0x13, 0x12 > 0x11 */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); +} + +static void test_condition_delta_24bit() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condition_t* cond; + rc_parse_state_t parse; + char buffer[512]; + rc_memrefs_t memrefs; + + const char* cond_str = "0xW0001>d0xW0001"; + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + cond = rc_parse_condition(&cond_str, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*cond_str, 0); + memory.ram = ram; + memory.size = sizeof(ram); + + /* initial delta value is 0x000000, 0xAB3412 > 0x000000 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* delta value is now 0xAB3412, 0xAB3412 == 0xAB3412 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* value changes to 0xAB3411, delta value is now 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* value changes to 0xAB3412, delta value is now 0xAB3411, 0xAB3412 > 0xAB3411 */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* ram[4] should not affect the 24-bit value, 0xAB3412 == 0xAB3412 */ + ram[4] = 0xAC; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* value changes to 0xAB3411, delta is still 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* ram[4] should not affect the 24-bit value, 0xAB3411 == 0xAB3411 */ + ram[4] = 0xAD; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); +} + +static void test_condition_prior_24bit() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condition_t* cond; + rc_parse_state_t parse; + char buffer[512]; + rc_memrefs_t memrefs; + + const char* cond_str = "0xW0001>p0xW0001"; + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + cond = rc_parse_condition(&cond_str, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_EQUALS(*cond_str, 0); + memory.ram = ram; + memory.size = sizeof(ram); + + /* initial prior value is 0x000000, 0xAB3412 > 0x000000 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* delta value is now 0xAB3412, but prior is still 0x000000, 0xAB3412 > 0x000000 */ + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* value changes to 0xAB3411, delta and prior values are now 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* value changes to 0xAB3412, delta and prior values are now 0xAB3411, 0xAB3412 > 0xAB3411 */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* ram[4] should not affect the 24-bit value, 0xAB3412 > 0xAB3411 */ + ram[4] = 0xAC; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* ram[4] should not affect the 24-bit value, 0xAB3412 > 0xAB3411 */ + ram[4] = 0xAD; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); + + /* value changes to 0xAB3411, delta and prior values are now 0xAB3412, 0xAB3411 < 0xAB3412 */ + ram[1] = 0x11; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* ram[4] should not affect the 24-bit value, 0xAB3411 < 0xAB3412 */ + ram[4] = 0xAE; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); + + /* ram[4] should not affect the 24-bit value, 0xAB3411 < 0xAB3412 */ + ram[4] = 0xAF; + ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); +} + +void test_condition(void) { + TEST_SUITE_BEGIN(); + + /* different comparison operators */ + TEST_PARAMS5(test_parse_condition, "0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234==8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234!=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_NE, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234<8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LT, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234<=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LE, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234>8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_GT, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234>=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_GE, 0); + TEST_PARAMS5(test_parse_condition, "0xH1234<8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LT, 0); + + /* special accessors */ + TEST_PARAMS5(test_parse_condition, "d0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_DELTA, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "p0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_PRIOR, RC_OPERATOR_EQ, 0); + + /* flags */ + TEST_PARAMS5(test_parse_condition, "R:0xH1234=8", RC_CONDITION_RESET_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "P:0xH1234=8", RC_CONDITION_PAUSE_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234=8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); + TEST_PARAMS5(test_parse_condition, "B:0xH1234=8", RC_CONDITION_SUB_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); + TEST_PARAMS5(test_parse_condition, "C:0xH1234=8", RC_CONDITION_ADD_HITS, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "D:0xH1234=8", RC_CONDITION_SUB_HITS, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "M:0xH1234=8", RC_CONDITION_MEASURED, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "G:0xH1234=8", RC_CONDITION_MEASURED, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "Q:0xH1234=8", RC_CONDITION_MEASURED_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "I:0xH1234=8", RC_CONDITION_ADD_ADDRESS, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); + TEST_PARAMS5(test_parse_condition, "T:0xH1234=8", RC_CONDITION_TRIGGER, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + TEST_PARAMS5(test_parse_condition, "Z:0xH1234=8", RC_CONDITION_RESET_NEXT_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); + + /* modifiers (only valid with some flags, use A:) */ + TEST_PARAMS5(test_parse_condition, "A:0xH1234*8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_MULT, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234/8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_DIV, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234&8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_AND, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234^8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_XOR, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234%8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_MOD, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234+8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_ADD, 0); + TEST_PARAMS5(test_parse_condition, "A:0xH1234-8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_SUB, 0); + + TEST_PARAMS4(test_parse_modifier, "A:0xH1234", RC_OPERATOR_NONE, RC_OPERAND_CONST, 1); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*1", RC_OPERATOR_MULT, RC_OPERAND_CONST, 1); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*3", RC_OPERATOR_MULT, RC_OPERAND_CONST, 3); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*f0.5", RC_OPERATOR_MULT, RC_OPERAND_FP, 0.5); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*f.5", RC_OPERATOR_MULT, RC_OPERAND_FP, 0.5); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*-1", RC_OPERATOR_MULT, RC_OPERAND_CONST, -1); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234*0xH3456", RC_OPERATOR_MULT, RC_OPERAND_ADDRESS, 0x3456); + + /* legacy serializers would include whatever happened to be in the right side before it was converted to a modifier. + * they should be ignored */ + TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234!=0xH1234", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0.60.", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0(60)", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); + + /* hit counts */ + TEST_PARAMS5(test_parse_condition, "0xH1234=8(1)", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1); + TEST_PARAMS5(test_parse_condition, "0xH1234=8.1.", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1); /* legacy format */ + TEST_PARAMS5(test_parse_condition, "0xH1234=8(1000)", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1000); + + /* hex value is interpreted as a 16-bit memory reference */ + TEST_PARAMS7(test_parse_operands, "0xH1234=0x80", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x80U); + + TEST_PARAMS7(test_parse_operands, "0xL1234=0xU3456", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U, RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x3456U); + + /* shorthard for modifier conditions */ + TEST_PARAMS2(test_parse_modifier_shorthand, "A:0xH1234", RC_CONDITION_ADD_SOURCE); + TEST_PARAMS2(test_parse_modifier_shorthand, "B:0xH1234", RC_CONDITION_SUB_SOURCE); + TEST_PARAMS2(test_parse_modifier_shorthand, "I:0xH1234", RC_CONDITION_ADD_ADDRESS); + + /* parse errors */ + TEST_PARAMS2(test_parse_condition_error, "0xH1234==0", RC_OK); + TEST_PARAMS2(test_parse_condition_error, "H0x1234==0", RC_INVALID_CONST_OPERAND); + TEST_PARAMS2(test_parse_condition_error, "0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "C:0x1234", RC_INVALID_OPERATOR); /* shorthand only valid on modifier conditions */ + TEST_PARAMS2(test_parse_condition_error, "N:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "O:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "P:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "R:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "M:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "G:0x1234", RC_INVALID_OPERATOR); + TEST_PARAMS2(test_parse_condition_error, "Y:0x1234", RC_INVALID_CONDITION_TYPE); + TEST_PARAMS2(test_parse_condition_error, "0x1234=1.2", RC_INVALID_REQUIRED_HITS); + TEST_PARAMS2(test_parse_condition_error, "0.1234==0", RC_INVALID_OPERATOR); /* period is assumed to be operator */ + TEST_PARAMS2(test_parse_condition_error, "0==0.1234", RC_INVALID_REQUIRED_HITS); /* period is assumed to be start of hit target, no end marker */ + + /* simple evaluations (ram[1] = 18, delta(ram[1]) = 17, ram[2] = 52, delta(ram[2]) = 52) */ + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>0", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001<=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001<18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0001=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001!=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0002=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002!=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001<0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001=0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001<=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0001>=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xH0002=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0002!=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0002<=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0002>=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0002d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0001=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001!=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001<0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0001>0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xH0002=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002!=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002<0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xH0002>0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xM0001=1", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=1", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xK0001=2", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xK0001!=2", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xM0001=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xM0001!=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xM0002=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "d0xM0002!=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 1); + + TEST_PARAMS3(test_evaluate_condition, "0xM0001=0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xM0001=0xH0000", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=0xH0000", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); + TEST_PARAMS3(test_evaluate_condition, "0xH0000=0xM0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0000!=0xM0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xM0001=d0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "0xM0001!=d0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "d0xM0001=0xN0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, 1); + TEST_PARAMS3(test_evaluate_condition, "d0xM0001!=0xN0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, 0); + + TEST_PARAMS3(test_evaluate_condition, "0xH0001=0xH0001", RC_PROCESSING_COMPARE_ALWAYS_TRUE, 1); + TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0xH0001", RC_PROCESSING_COMPARE_ALWAYS_FALSE, 0); + TEST_PARAMS3(test_evaluate_condition, "1=1", RC_PROCESSING_COMPARE_ALWAYS_TRUE, 1); + TEST_PARAMS3(test_evaluate_condition, "0=1", RC_PROCESSING_COMPARE_ALWAYS_FALSE, 0); + + TEST_PARAMS1(test_default_comparator, "I:0xH0000_0xH0001=0"); /* indirect cannot be optimized */ + TEST_PARAMS1(test_default_comparator, "fF0001=f2.0"); /* float is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "p0xH0001=0"); /* prior is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "b0xH0001=0"); /* bcd is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "~0xH0001=0"); /* inverted is not common enough to be optimized */ + TEST_PARAMS1(test_default_comparator, "d0xH0001=0x 0001"); /* delta comparison only optimized for same address, same size */ + TEST_PARAMS1(test_default_comparator, "0xH0001=d0x 0001"); /* delta comparison only optimized for same address, same size */ + TEST_PARAMS1(test_default_comparator, "d0xH0001=0xH0002"); /* delta comparison only optimized for same address, same size */ + TEST_PARAMS1(test_default_comparator, "0xH0001=d0xH0002"); /* delta comparison only optimized for same address, same size */ + + /* float evaluations (ram[0] = 2.0, ram[4] = 3.14159 */ + TEST_PARAMS2(test_evaluate_condition_float, "fF0000=f2.0", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=f2.0", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000<=f2.0", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000>=f2.0", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000f2.0", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000f1.999999", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000f2.000001", 0); + + TEST_PARAMS2(test_evaluate_condition_float, "fF0000=2", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=2", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000<=2", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000>=2", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000<2", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000>2", 0); + + TEST_PARAMS2(test_evaluate_condition_float, "fM0004=f6.283185", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004!=f6.283185", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004<=f6.283185", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004>=f6.283185", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283185", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283183", 1); /* binary to float, the last decimal digit may */ + TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283187", 0); /* ensure we cover an epsilon gap */ + + TEST_PARAMS2(test_evaluate_condition_float, "fM0004=6", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004!=6", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004<=6", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004>=6", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004<6", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fM0004>6", 1); + + TEST_PARAMS2(test_evaluate_condition_float, "fF0000==fF0000", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=fF0000", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000==fM0004", 0); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=fM0004", 1); + TEST_PARAMS2(test_evaluate_condition_float, "fF0000fM0004", 0); + + TEST(test_condition_compare_delta); + TEST(test_condition_delta_24bit); + TEST(test_condition_prior_24bit); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_condset.c b/src/rcheevos/test/rcheevos/test_condset.c new file mode 100644 index 0000000000..7996328608 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_condset.c @@ -0,0 +1,5170 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +#include "../src/rc_compat.h" + +static void _assert_parse_condset(rc_condset_t** condset, rc_memrefs_t* memrefs, void* buffer, const char* memaddr) +{ + rc_parse_state_t parse; + int size; + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, memrefs); + + *condset = rc_parse_condset(&memaddr, &parse); + size = parse.offset; + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(size, 0); + ASSERT_PTR_NOT_NULL(*condset); +} +#define assert_parse_condset(condset, memrefs_out, buffer, memaddr) ASSERT_HELPER(_assert_parse_condset(condset, memrefs_out, buffer, memaddr), "assert_parse_condset") + +static void _assert_evaluate_condset(rc_condset_t* condset, rc_memrefs_t* memrefs, memory_t* memory, int expected_result) { + int result; + rc_eval_state_t eval_state; + + rc_update_memref_values(memrefs, peek, memory); + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + result = rc_test_condset(condset, &eval_state); + + /* NOTE: reset normally handled by trigger since it's not group specific */ + if (eval_state.was_reset) + rc_reset_condset(condset); + + ASSERT_NUM_EQUALS(result, expected_result); +} +#define assert_evaluate_condset(condset, memrefs, memory, expected_result) ASSERT_HELPER(_assert_evaluate_condset(condset, &memrefs, memory, expected_result), "assert_evaluate_condset") + +static rc_condition_t* condset_get_cond(rc_condset_t* condset, int cond_index) { + rc_condition_t* cond = condset->conditions; + + while (cond_index-- != 0) { + if (cond == NULL) + break; + + cond = cond->next; + } + + return cond; +} + +static void _assert_hit_count(rc_condset_t* condset, int cond_index, uint32_t expected_hit_count) { + rc_condition_t* cond = condset_get_cond(condset, cond_index); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hit_count); +} +#define assert_hit_count(condset, cond_index, expected_hit_count) ASSERT_HELPER(_assert_hit_count(condset, cond_index, expected_hit_count), "assert_hit_count") + + +static void test_hitcount_increment_when_true() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18"); /* one condition, true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1U); + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2U); +} + +static void test_hitcount_does_not_increment_when_false() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001!=18"); /* one condition, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0U); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0U); +} + +static void test_hitcount_target() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=20(2)_0xH0002=52"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + + /* hit target met, overall is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); /* hit target met, not incremented */ + assert_hit_count(condset, 1, 4); + + /* first condition no longer true, but hit count was met so it acts true */ + ram[1] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 5); +} + +static void test_hitcount_two_conditions(const char* memaddr, int expected_result, uint32_t expected_hitcount1, uint32_t expected_hitcount2) { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, memaddr); + assert_evaluate_condset(condset, memrefs, &memory, expected_result); + assert_hit_count(condset, 0, expected_hitcount1); + assert_hit_count(condset, 1, expected_hitcount2); +} + +static void test_hitcount_three_conditions(const char* memaddr, int expected_result, uint32_t expected_hitcount1, + uint32_t expected_hitcount2, uint32_t expected_hitcount3) { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, memaddr); + assert_evaluate_condset(condset, memrefs, &memory, expected_result); + assert_hit_count(condset, 0, expected_hitcount1); + assert_hit_count(condset, 1, expected_hitcount2); + assert_hit_count(condset, 2, expected_hitcount3); +} + +static void test_pauseif() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52_P:0xL0x0004=6"); + + /* first condition true, but ignored because both pause conditions are true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); /* Also true, but processing stops on first PauseIf */ + + /* first pause condition no longer true, but second still is */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); /* PauseIf goes to 0 when false */ + assert_hit_count(condset, 2, 1); + + /* both pause conditions not true, set will trigger */ + ram[4] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_pauseif_hitcount_one() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.1."); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* pause condition no longer true, but hitcount prevents trigger */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); +} + +static void test_pauseif_hitcount_two() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.2."); + + /* pause hit target has not been met, group is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* pause hit target has been met, group is false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + + /* pause condition is no longer true, but hitcount prevents trigger */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); +} + +static void test_pauseif_hitcount_with_reset() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.1._R:0xH0003=1"); + + /* pauseif triggered, non-pauseif conditions ignored */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* pause condition is no longer true, but hitcount prevents trigger */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* pause has precedence over reset, reset in group is ignored */ + ram[3] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); +} + +static void test_pauseif_resetnextif() +{ + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0002=0_0xH0001=18_Z:0xH0003=1_N:0xH0000=0_P:0xH0002=25.1."); + + /* accumulate hit counts */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 0); + + /* pauseif triggered, non-pauseif conditions ignored */ + ram[2] = 25; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 2); + assert_hit_count(condset, 4, 1); + + /* pause condition is no longer true, but its hitcount prevents the non-pauseif conditions from being processed */ + ram[2] = 10; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 3); + assert_hit_count(condset, 4, 1); + + /* reset next if clears hit on pause, but not on non-pauseif conditions */ + ram[3] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); /* resetnextif keeps its own hitcount */ + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 0); + + /* trigger is true */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); /* resetnextif keeps its own hitcount */ + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 0); +} + +static void test_pauseif_does_not_increment_hits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_P:0xL0004=4"); + + /* both conditions true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* pause condition is true, other conditions should not tally hits */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* pause condition not true, other conditions should tally hits */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* pause condition is true, other conditions should not tally hits */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + + /* pause condition not true, other conditions should tally hits */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); +} + +static void test_pauseif_delta_updated() { + uint8_t ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_d0xH0002=60"); + + /* upaused, delta = 0, current = 52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* paused, delta = 52, current = 44 */ + ram[1] = 1; + ram[2] = 44; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + /* paused, delta = 44, current = 60 */ + ram[2] = 60; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* unpaused, delta = 60, current = 97 */ + ram[1] = 0; + ram[2] = 97; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); +} + +static void test_pauseif_indirect_delta_updated() { + uint8_t ram[] = {0x00, 0x00, 0x34, 0x3C, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_I:0xH0000_d0xH0002=60"); + + /* upaused, delta = 0, current = 52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 2, 0); + + /* paused, delta = 52, current = 44 */ + ram[1] = 1; + ram[2] = 44; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 2, 0); + + /* paused, delta = 44, current = 60 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 2, 0); + + /* unpaused, delta = 60, current = 97 */ + ram[1] = 0; + ram[3] = 97; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 2, 1); +} + +static void test_pauseif_short_circuit() { + uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* evaluation of an achievement stops at the first true pauseif condition + * + * this allows achievements to prevent accumulating hits on a pauselock farther down + * in a group. a better solution would be to use an AndNext on the pauselock, but + * there are achievements in the wild relying on this behavior. + * see https://retroachievements.org/achievement/66804, which has a PauseIf 2040 frames + * pass (condition 5), but don't tally those frames if the game is paused (condition 3). + * similarly, https://retroachievements.org/achievement/154804 has a PauseIf 480 frames + * (condition 5), but don't tally those frames if the map is visible (condition 4). + */ + assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_P:0xH0002=1.3._0xH0003=1.4."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* non-pauseif true */ + ram[3] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* second pauseif tallies a hit, but it's not enough to pause the non-pauseif */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + + /* first pauseif is true, pauses the second pauseif and the non-pauseif */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + + /* first pauseif is false, the second pauseif and the non-pauseif can update */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 3); + + /* second pauseif reaches hitcount, non-pauseif does not update */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); + + /* pauseif hitcount still met, non-pauseif does not update */ + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); +} + +static void test_resetif() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_R:0xH0002=50_R:0xL0x0004=4"); + + /* first condition true, neither reset true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + + /* first reset true */ + ram[2] = 50; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); /* hitcount reset */ + + /* both resets true */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + + /* only second reset is true */ + ram[2] = 52; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + + /* neither reset true */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_cond_with_hittarget() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4"); + + /* both conditions true, reset not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + + /* reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* reset no longer true, hit target not met */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_hitcount() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4.2."); + + /* hitcounts on conditions 1 and 2 are incremented */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* hitcounts on conditions 1 and 2 are incremented. cond 1 now true, so entire set is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* hitcount on condition 2 is incremented, cond 1 already at its target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + + /* reset condition is true, but its hitcount is not met */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 1); + ASSERT_NUM_EQUALS(condset_get_cond(condset, 2)->is_true, 1); + + /* second hit on reset condition should reset everything */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + /* is_true=3 indicates the condition was true this frame, and its hit target was met before it got reset */ + ASSERT_NUM_EQUALS(condset_get_cond(condset, 2)->is_true, 3); +} + +static void test_resetif_hitcount_one() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4.1."); + + /* hitcounts on conditions 1 and 2 are incremented */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* hitcounts on conditions 1 and 2 are incremented. cond 1 now true, so entire set is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* hitcount on condition 2 is incremented, cond 1 already at its target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + + /* reset condition is true, its hitcount is met, so all hitcounts (including the resetif) should be reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); +} + +static void test_resetif_hitcount_addhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* never(repeated(3, byte(1) == 18 || low(4) == 6)) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_R:0xL0004=6(3)"); + + /* result is true, no non-reset conditions */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + ASSERT_NUM_EQUALS(condset_get_cond(condset, 0)->is_true, 1); + ASSERT_NUM_EQUALS(condset_get_cond(condset, 1)->is_true, 1); + + /* total hitcount is met (2 for first condition, 1 for second, need 3 total) , everything resets */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + ASSERT_NUM_EQUALS(condset_get_cond(condset, 0)->is_true, 1); + /* is_true=2 indicates the condition was not true this frame, but its hit target was met before it got reset due to the AddHits */ + ASSERT_NUM_EQUALS(condset_get_cond(condset, 1)->is_true, 2); +} + +static void test_pauseif_resetif_hitcounts() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_R:0xH0002=50_P:0xL0004=4"); + + /* first condition is true, pauseif and resetif are not, so it gets a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* pause is true, hit not incremented or reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* reset if true, but set is still paused */ + ram[2] = 50; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* set no longer paused, reset clears hitcount */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + + /* reset no longer true, hits increment again */ + ram[2] = 52; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + + /* hitcount met, set is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); +} + +static void test_resetnextif() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_0xH0001=18(2)_0xH0002=52.4."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 3); + + /* trigger resetnextif, last condition should not be reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 4); + + /* reset no longer true, hit target not met */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 4); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 4); +} + +static void test_resetnextif_non_hitcount_condition() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* resetnextif for non-hitcount condition will still set the hitcount to 0 and make it false */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_0xH0001=18_0xH0002=52.4."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* conditions continue to tally */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); + + /* target hitcount met, condset true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 4); + + /* trigger resetnextif, last condition should not be reset */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 4); + + /* reset no longer true (hit count on reset kept), condset is true again */ + ram[4] = 0x56; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 4); +} + +static void test_resetnextif_addhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AddHits byte(0x0001)=18 <-- ResetNextIf resets hits on this condition before it's added to the accumulator + * upper4(0x0003)=10 (4) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_C:0xH0001=18_0xU0003=10(4)_0xH0002=52.1."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* total tallies match limit, trigger */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* resetnextif resets hits on condition 2, but not condition 3 - total will be 3/4 - does not trigger */ + ram[4] = 0x54; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_addhits_chain() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AddHits byte(0x0001)=18 + * ResetNextIf low4(0x0004)=5 + * AddHits byte(0x0000)=0 + * upper4(0x0003)=10 (6) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_C:0xH0001=18_Z:0xL0004=5_C:0xH0000=0_0xU0003=10(6)_0xH0002=52.1."); + + /* resetnextifs not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 1); /* <- total hits is 3/6 */ + assert_hit_count(condset, 5, 1); + + /* first resetnextif true, only affects first addhits condition */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 2); + assert_hit_count(condset, 4, 2); /* <- total hits is 4/6 */ + assert_hit_count(condset, 5, 1); + + /* second resetnextif true, only affects second addhits condition */ + ram[4] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 3); /* <- total hits is 4/6 */ + assert_hit_count(condset, 5, 1); + + /* total hits reaches hit target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 4); /* <- total hits is 6/6 */ + assert_hit_count(condset, 5, 1); +} + +static void test_resetnextif_addhits_chain_total() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AddHits byte(0x0001)=18 + * AddHits byte(0x0000)=0 + * ResetNextIf low4(0x0004)=4 + * upper4(0x0003)=10 (6) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_C:0xH0000=0_Z:0xL0004=4_0xU0003=10(6)_0xH0002=52.1."); + + /* resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); /* <- total hits is 3/6 */ + assert_hit_count(condset, 4, 1); + + /* resetnextif true, only affects that condition, not total */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); /* <- total hits is 4/6 */ + assert_hit_count(condset, 4, 1); + + /* resetnextif still true, total matches target */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 0); /* <- total hits is 6/6 */ + assert_hit_count(condset, 4, 1); +} + +static void test_resetnextif_using_andnext() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext byte(0x0001)=18 + * ResetNextIf low4(0x0004)=4 + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=18_Z:0xL0004=4_0xU0003=10(3)_0xH0002=52.1."); + + /* conditions 1, 3, and 4 true; resetnextif relies on conditions 1 and 2, so it not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* both resetnextif conditions true */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* first part of resetnextif not true */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* both resetnextif conditions true */ + ram[1] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_andnext() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AndNext byte(0x0001)=18 + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_N:0xH0001=18_0xU0003=10(3)_0xH0002=52.1."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* partial andnext not true */ + ram[3] = 0x86; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* andnext true again */ + ram[3] = 0xA0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* resetnextif resets all hits in the andnext chain */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_andnext_hitchain() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * AndNext byte(0x0001)=18 (2) + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_N:0xH0001=18.2._0xU0003=10(3)_0xH0002=52.1."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); /* condition 2 must meet hitcount before condition three tallies a hit */ + assert_hit_count(condset, 3, 1); + + /* resetnextif true, should reset conditions 2 and 3 (3 was already 0) */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* resetnextif not true, condition 2 tallies a hit */ + ram[4] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* resetnextif still not true, condition 2 tallies another hit, which allows condition 3 to tally a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* resetnextif true, should reset both conditions 2 and 3 */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_andnext_chain() +{ + uint8_t ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf byte(0x0001)!=0 + * AndNext byte(0x0002)=0 + * ResetNextIf byte(0x0001)=0 (2) + * byte(0x0003)=1 (5) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xH0001!=0_N:0xH0002=0_Z:0xH0001=0.2._0xH0003=1.5."); + + /* first resetnextif not true, conditions 1, 2 and 3 are true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, conditions 1 and 2 should reset, but not 3 */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 2); + + /* first resetnextif not true, condition 1, 2 and 3 are true */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 3); + + /* hitcount on condition 2 reached, condition 3 is reset */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 0); +} + +static void test_resetnextif_addaddress_andnext_chain() +{ + uint8_t ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AddAddress byte(0x0004) + * ResetNextIf byte(0x0001)!=0 + * AndNext byte(0x0002)=0 + * AddAddress byte(0x0004) + * ResetNextIf byte(0x0001)=0 (2) + * AddAddress byte(0x0004) + * byte(0x0003)=1 (5) + * Trigger byte(0x0003)=6 + */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0004_Z:0xH0001!=0_N:0xH0002=0_A:0xH0004_Z:0xH0001=0.2._A:0xH0004_0xH0003=1.5._T:0xH0003=6"); + + /* first resetnextif not true, conditions 2, 4 and 6 are true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 4, 1); + assert_hit_count(condset, 6, 1); + + /* first resetnextif true, conditions 2 and 4 should reset, but not 6 */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 4, 0); + assert_hit_count(condset, 6, 2); + + /* first resetnextif not true, conditions 2, 4 and 6 are true */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 4, 1); + assert_hit_count(condset, 6, 3); + + /* hitcount on condition 4 reached, condition 6 is reset */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 4, 2); + assert_hit_count(condset, 6, 0); + + /* allow last condition to reach hit target */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 6); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 4, 0); + assert_hit_count(condset, 6, 5); + + /* first resetnextif not true, conditions 2, 4 and 6 are true */ + ram[1] = 0; + ram[3] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 6); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 4, 1); + assert_hit_count(condset, 6, 5); + + /* first resetnextif true, conditions 2 and 4 should reset, but not 6 */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 7); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 4, 0); + assert_hit_count(condset, 6, 5); +} + +static void test_resetnextif_addaddress() { + uint8_t ram[] = {0x00, 0x00, 0x02, 0x03, 0x04}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_Z:0xH0001=1_I:0xH0000_0xH0002=2(3)_0xH0004=4.8."); + + /* both conditions true, resetnextif not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 1); + + /* resetnextif true */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 2); + + /* pointer changes. resetnextif not true, condition not true */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 3); + + /* condition true, resetnextif not true */ + ram[3] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 4); + + /* resetnextif true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 5); + + /* pointer changes, resetnextif and condition true */ + ram[0] = 0; + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 6); + + /* resetnextif not true */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 7); +} + +static void test_resetnextif_chain() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * ResetNextIf byte(0x0001)=1 + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_Z:0xH0001=1_0xU0003=10(3)_0xH0002=52.1."); + + /* both conditions true, resetnextifs not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* second resetnextif true, resets first hit count */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, disables second, allows hitcount */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* second resetnextif no longer true, first still keeps it disabled */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* first resetnextif no longer true (hit count on resetnextif itself is not reset), second already false */ + ram[4] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_chain_andnext() { + uint8_t ram[] = {0x00, 0x00, 0x01}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext byte(0x0000)=0 + * ResetNextIf byte(0x0001)=1 + * AndNext byte(0x0000)=1 + * ResetNextIf byte(0x0001)=1 + * byte(0x0002)=1 (5) + */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0000=0_Z:0xH0001=1_N:0xH0000=1_Z:0xH0001=1_0xH0002=1.5."); + + /* no ResetNextIf true. hit count incremented */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); /* hit captured on byte(0) == 0 */ + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 1); + + /* second ResetNextIf clause true */ + ram[0] = ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); /* ResetNextIf on condition 3 doesn't affect condition 1 */ + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 0); + + /* no ResetNextIf true, hit count incremented */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 1); + + /* first ResetNextIf clause true */ + ram[0] = 0; + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); /* first ResetNextIf affects second ResetNextIf */ + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 2); /* but not final clause */ +} + +static void test_resetnextif_chain_with_hits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf low4(0x0004)=4 + * ResetNextIf byte(0x0001)=1 (2) + * upper4(0x0003)=10 (3) + * byte(0x0002)=52 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_Z:0xH0001=1(2)_0xU0003=10(8)_0xH0002=52.1."); + + /* both conditions true, resetnextifs not true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* second resetnextif true, but hit target not met */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, resets second, allows hitcount */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 1); + + /* second resetnextif no longer true, first still keeps it disabled */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 4); + assert_hit_count(condset, 3, 1); + + /* first resetnextif no longer true (hit count on resetnextif itself is not reset), second already false */ + ram[4] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 5); + assert_hit_count(condset, 3, 1); + + /* second resetnextif true again, but hitcount not met */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 6); + assert_hit_count(condset, 3, 1); + + /* second resetnextif hitcount met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* second resetnextif condition no longer true, but hitcount keeps it active */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 1); + + /* first resetnextif true, resets second, allows hit on third condition */ + ram[4] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); +} + +static void test_resetnextif_pause_lock() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf byte(0x0002)=1 + * PauseIf byte(0x0001)=1 (1) + */ + assert_parse_condset(&condset, &memrefs, buffer, "Z:0xH0002=1_P:0xH0001=1(1)"); + + /* both conditions false */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* reset next true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + /* reset next and pause true */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* only pause true */ + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + + /* both conditions true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + + /* only pause true */ + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); + + /* both conditions false */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); + + /* reset next true */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 0); +} + +static void test_resetnextif_unfinished() { + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[512]; + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=0_Z:0xH0002=22"); + + ASSERT_NUM_EQUALS(condset->num_other_conditions, 2); +} + +static void test_addsource() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ + + /* first condition is true, but not sum */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* first condition is true, sum is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_overflow() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* adding two bytes will result in a value larger than 256, don't truncate to a byte */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_0xH0002=22"); + + /* sum is 0x102 (0x12 + 0xF0) */ + ram[2] = 0xF0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* sum is 0x122 (0x32 + 0xF0) */ + ram[1] = 0x32; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* NOTE: SubSource subtracts the first item from the second! */ + /* byte(1) - byte(2) == 14 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0_0xH0001=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ + + /* first condition is true, but not difference */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* first condition is true, difference is negative inverse of expected value */ + ram[2] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct again */ + ram[1] = 28; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_legacy_garbage() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) - byte(2) == 14 */ + /* old serializers would store the comparison and right-side value from the condition before it was converted to SubSource */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0xH0000_0xH0001=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ + + /* first condition is true, but not difference */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* first condition is true, difference is negative inverse of expected value */ + ram[2] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct again */ + ram[1] = 28; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_overflow() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* subtracting two bytes will result in a very large positive number, don't truncate to a byte */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0_0xH0001=14"); + + /* difference is -10 (8 - 18) */ + ram[2] = 8; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* difference is 0xFFFFFF0E (8 - 0xFA) */ + ram[1] = 0xFA; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addsource_subsource() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) - low(2) + low(4) == 14 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_B:0xL0002=0_0xL0004=14"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* sum is correct */ + ram[1] = 12; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* first condition is true, but not sum */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* byte(4) would make sum true, but not low(4) */ + ram[4] = 0x12; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); + + /* difference is correct again */ + ram[2] = 1; + ram[4] = 15; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 2); +} + +static void test_addsource_multiply() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) * 3 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*3_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 19; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_multiply() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(2) - byte(1) * 3 == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001*3_0xH0002=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 32; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct */ + ram[2] = 17; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_multiply_fraction() { + uint8_t ram[] = {0x00, 0x08, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) * 0.75 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*f0.75_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 15; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_multiply_address() { + uint8_t ram[] = {0x00, 0x06, 0x04, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) * byte(0) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*0xH00000_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 19; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_divide() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) / 3 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/3_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_compare_percentage() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* (byte(0)/byte(1) > 0.5) => (byte(1) * 0.5) < byte(0) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*f0.5_0<0xH0000"); + + /* 0/6 > 50% = false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + ram[0] = 2; assert_evaluate_condset(condset, memrefs, &memory, 0); /* 2/6 > 50% = false */ + ram[0] = 4; assert_evaluate_condset(condset, memrefs, &memory, 1); /* 4/6 > 50% = true */ + ram[1] = 7; assert_evaluate_condset(condset, memrefs, &memory, 1); /* 4/7 > 50% = true */ + ram[0] = 3; assert_evaluate_condset(condset, memrefs, &memory, 0); /* 3/7 > 50% = false */ +} + +static void test_subsource_divide() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(2) - byte(1) / 3 == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001/3_0xH0002=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is not correct */ + ram[1] = 14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_divide_address() { + uint8_t ram[] = {0x00, 0x06, 0x10, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) / byte(0) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/0xH00000_0xH0002=22"); + + /* sum is not correct (divide by zero) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 21; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_divide_self() { + uint8_t ram[] = {0x00, 0x06, 0x10, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) / byte(1) + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/0xH00001_0xH0002=22"); + + /* sum is not correct (1 + 16 != 22) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct (1 + 21 == 22) */ + ram[2] = 21; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct (0 + 21 == 22) */ + ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct (0 + 22 == 22) */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_mask() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) & 0x07 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001&h7_0xH0002=22"); + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is not correct */ + ram[1] = 0x74; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_xor() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(1) ^ 0x05 + byte(2) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001^h5_0xH0002=22"); + + /* sum (6 ^ 5 + 52 == 22) is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* sum ((6 ^ 5 = 3) + 19 = 22) is correct */ + ram[2] = 19; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum ((17 ^ 5 = 20) + 19 = 22) is not correct */ + ram[1] = 0x11; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* sum ((17 ^ 5 = 20) + 2 = 22) is correct */ + ram[2] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_addsource_float_first() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F}; /* fF0004 = 1.5 */ + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* float(4) + float(4) + float(4) + 1 == 5.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:fF0004_A:fF0004_A:fF0004_1=f5.5"); + + /* +1 (int) will convert the intermediate value to an integer + * (1.5 + 1.5 + 1.5) => 4.5 => 4 + 1 = 5 + * which will not equal 5.5, so logic will fail. */ + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* float(4) + float(4) + float(4) + 1.0 == 5.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:fF0004_A:fF0004_A:fF0004_f1.0=f5.5"); + + /* +1.0 (float) will not convert the intermediate value to an integer + * (1.5 + 1.5 + 1.5) => 4.5 + 1.0 = 5.5 + * which will equal 5.5, so logic will succeed. */ + + /* sum is correct */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addsource_float_second() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F}; /* fF0004 = 1.5 */ + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* 1 + float(4) + float(4) + float(4) == 5.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_fF0004=f5.5"); + + /* the first value defines the type of the accumulator. since it's an integer, each float will be + * truncated when it is added to the accumulator, so 1 + (1.5) + (1.5) is 3. when the accumulator is + * added to the final value, the accumulator is converted to match the final value, so 3.0 + 1.5 = 4.5. */ + + /* sum is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 + float(4) + float(4) + float(4) == 4.5 (note: two intermediary floats are converted to int, but not last) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_fF0004=f4.5"); + /* sum is correct */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 1 + float(4) + float(4) + float(4) + 0 == 4.0 (note: all intermediate floats are converted to int) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_A:fF0004_0=f4.0"); + /* sum is correct */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_mask() { + uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(2) - byte(1) & 0x06 == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001&6_0xH0002=14"); + + /* difference is not correct */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + + /* difference is correct */ + ram[2] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is not correct */ + ram[1] = 10; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + /* difference is correct */ + ram[2] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); +} + +static void test_subsource_overflow_comparison_equal() { + uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "A==B" can be expressed as "-A+B==0" */ + + /* - byte(0) + byte(1) = 0 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_0xH0001=0"); + + /* 1 == 0 = false */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 == 1 = true */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 == 0 = true */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 == 1 = false */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 == 255 = false */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 == 255 = true */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 == 254 = false */ + ram[0] = 255; ram[1] = 254; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 == 0 = false */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_overflow_comparison_greater() { + uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "A>B" can be expressed as "-A+B>M" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) + byte(1) > 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_0xH0001>256"); + + /* 1 > 0 = true */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 1 > 1 = false */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 > 0 = false */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 > 1 = false */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 > 255 = false */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 > 255 = false */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 > 254 = true */ + ram[0] = 255; ram[1] = 254; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 > 0 = true */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_overflow_comparison_greater_or_equal() { + uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "A>=B" can be expressed as "-A-1+B>=M" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) - 1 + byte(1) > 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_B:1_0xH0001>=256"); + + /* 1 >= 0 = true */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 1 >= 1 = true */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 >= 0 = true */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 >= 1 = false */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 >= 255 = false */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 255 >= 255 = true */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 >= 254 = true */ + ram[0] = 255; ram[1] = 254; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 >= 0 = true */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_overflow_comparison_lesser() { + uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "AM" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) + byte(1) + 256 > 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_A:0xH0001_256>256"); + + /* 1 < 0 = false */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 < 1 = false */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 < 0 = false */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 0 < 1 = true */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 < 255 = true */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 < 255 = false */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 254 < 255 = true */ + ram[0] = 254; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 < 0 = false */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_overflow_comparison_lesser_or_equal() { + uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ASSERT: "AM" where M is the largest number that cannot be + * represented by A or B */ + + /* - byte(0) + byte(1) + 256 >= 256 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_A:0xH0001_256>=256"); + + /* 1 <= 0 = false */ + ram[0] = 1; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 1 <= 1 = true */ + ram[0] = 1; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 <= 0 = true */ + ram[0] = 0; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 <= 1 = true */ + ram[0] = 0; ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 0 <= 255 = true */ + ram[0] = 0; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 <= 255 = true */ + ram[0] = 255; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 254 <= 255 = true */ + ram[0] = 254; ram[1] = 255; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 255 <= 0 = false */ + ram[0] = 255; ram[1] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_float() { + uint8_t ram[] = {0x06, 0x00, 0x00, 0x00, 0x92, 0x44, 0x9A, 0x42}; /* fF0004 = 77.133926 */ + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* float(0x0004) - word(0) * 1.666667 > 65.8 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0x 0000*f1.666667_fF0004>f65.8"); + + /* 77.133926 - (6 * 1.666667) = 77.133926 - 10 = 67.133926 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 77.133926 - (7 * 1.666667) = 77.133926 - 11.6667 = 65.4672 */ + ram[0] = 7; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addsource_bits_change() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* bit0(0x01) + bit1(0x01) + bit2(0x01) == 3 && delta(bit0(0x01)) + delta(bit1(0x01)) + delta(bit2(0x01)) == 2 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xM0001_A:0xN0001_0xO0001=3_A:d0xM0001_A:d0xN0001_d0xO0001=2"); + + /* bit sum = 1 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 5, 0); + + /* bit sum = 3 (delta = 1) */ + ram[1] = 0x17; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 5, 0); + + /* bit sum = 2 (delta = 3) */ + ram[1] = 0x16; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 5, 0); + + /* bit sum = 1 (delta = 2) */ + ram[1] = 0x14; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 5, 1); + + /* bit sum = 2 (delta = 1) */ + ram[1] = 0x16; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 5, 1); + + /* bit sum = 3 (delta = 2) */ + ram[1] = 0x17; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 5, 2); +} + +static void test_addsource_bcd() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* bcd(byte(0)) * 100 + bcd(byte(1)) > 4990 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:b0xH0000*100_b0xH0001>4990"); + + /* 0*100+12 = 12 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 49*100+89 = 4989 */ + ram[0] = 0x49; + ram[1] = 0x89; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 49*100+99 = 4999 */ + ram[1] = 0x99; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 50*100+00 = 5000 */ + ram[0] = 0x50; + ram[1] = 0x00; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addsource_invert() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ~low4(0) + ~high4(0) > 20 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:~0xL0000_~0xU0000>20"); + + /* ~0 (F) + ~0 (F) = 1E */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* ~4 (B) + ~9 (6) = 11 */ + ram[0] = 0x49; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* ~9 (6) + ~9 (6) = 0C */ + ram[1] = 0x99; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* ~5 (A) + ~1 (E) = 18 */ + ram[0] = 0x51; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_subsource_mem_delta() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* -byte(0) + prior(byte(0)) = 10 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_d0xH0000=10"); + + /* -0 + 0 = 10 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* -5 + 0 = 10 */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* -15 + 5 = 10 */ + ram[0] = 15; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* -5 + 15 = 10 */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* -5 + 5 = 10 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_subsource_mem_prior() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* -byte(0) + prior(byte(0)) = 10 */ + assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_p0xH0000=10"); + + /* -0 + 0 = 10 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* -5 + 0 = 10 */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* -15 + 5 = 10 */ + ram[0] = 15; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* -5 + 15 = 10 */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* -5 + 15 = 10 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addsource_unfinished() { + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[512]; + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=0_A:0xH0002=22"); + + ASSERT_NUM_EQUALS(condset->num_other_conditions, 1); + ASSERT_NUM_EQUALS(condset->num_indirect_conditions, 1); +} + +static void test_addsource_float_division() +{ + uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0x80, 0x40 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0) / remember(float(4)) > 0.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0000/f4.0_f0.0>f1.4"); + + /* 0 / 4.0 > 1.4 = false (0.00) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 6 / 4.0 > 1.4 = true (1.50) */ + ram[0] = 6; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 5 / 4.0 > 1.4 = false (1.25) */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 7 / 4.0 > 1.4 = true (1.75) */ + ram[0] = 7; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addsource_recall_float_division() { + uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0x80, 0x40}; /* fF0004 = 4.0 */ + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0) / remember(float(4)) > 0.5 */ + assert_parse_condset(&condset, &memrefs, buffer, "K:fF0004_A:0xH0000/{recall}_f0.0>f1.4"); + + /* 0 / 4.0 > 1.4 = false (0.00) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 6 / 4.0 > 1.4 = true (1.50) */ + ram[0] = 6; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* 5 / 4.0 > 1.4 = false (1.25) */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 7 / 4.0 > 1.4 = true (1.75) */ + ram[0] = 7; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addsource_long_chain() { + /* assume 22 bytes per cond (10 for base, 11 for delta, 1 for separator) + * and approximately 320 bytes per cond when convered to data structures. + * 320+22 = 342. Round that up to 384 just to be safe. + */ + const size_t cond_count = 1500; + const size_t buffer_size = 384 * cond_count; + char* buffer = (char*)malloc(buffer_size); + char* ptr = buffer; + const char* memaddr = buffer; + rc_condset_t* condset; + rc_memrefs_t memrefs; + rc_parse_state_t parse; + clock_t start, end; + size_t i, len, remaining; + + ASSERT_PTR_NOT_NULL(buffer); + + remaining = buffer_size; + for (i = 0; i < cond_count; i++) { + len = snprintf(ptr, remaining, "A:0xH%04x_", (uint32_t)i); + remaining -= len; + ptr += len; + } + len = snprintf(ptr, remaining, "0=500"); + remaining -= len; + ptr += len; + for (i = 0; i < cond_count; i++) { + len = snprintf(ptr, remaining, "_A:d0xH%04x", (uint32_t)i); + remaining -= len; + ptr += len; + } + ptr += snprintf(ptr, remaining, "=499"); + + ptr = (char*)RC_ALIGN((size_t)ptr); + remaining = buffer_size - (ptr - buffer); + + rc_init_parse_state(&parse, ptr); + rc_init_parse_state_memrefs(&parse, &memrefs); + + start = clock(); + condset = rc_parse_condset(&memaddr, &parse); + end = clock(); + + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER(parse.offset, 0); + ASSERT_NUM_LESS(parse.offset, remaining); + ASSERT_PTR_NOT_NULL(condset); + + double elapsed = (double)(end - start) * 1000 / CLOCKS_PER_SEC; + /* this shouldn't take more than 20-40ms (depending on the machine its running on). + * allow up to 1 full second before this errors. it was taking over 10s when reported */ + ASSERT_NUM_LESS(elapsed, 1000); + + /* each condition and its delta should share a memref */ + ASSERT_NUM_EQUALS(rc_memrefs_count_memrefs(&memrefs), cond_count); + /* one modified memref should be generated for each condition past the first + * plus one for the final condition: cond_count - 1 + 1 */ + ASSERT_NUM_EQUALS(rc_memrefs_count_modified_memrefs(&memrefs), cond_count); + + free(buffer); +} + +static void test_addhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, byte(1) == 18 || low(4) == 6) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18(2)_0xL0004=6(4)"); + + /* both conditions true, total not met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* total hits met (two for each condition) */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + + /* target met for first, it stops incrementing, second continues */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + + /* reset hit counts */ + rc_reset_condset(condset); + + /* both conditions true, total not met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + + /* first condition not true, total not met*/ + ram[1] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 2); + + /* total met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 3); +} + +static void test_addhits_multiple() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, byte(1) == 18 || low(4) == 6) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18(2)_0xL0004=6(4)_0xL0004=6(3)"); + + /* both conditions true, total not met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* total hits met (two for each condition), third condition not yet met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + /* target met for first, it stops incrementing, second and third continue */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); +} + +static void test_addhits_no_target() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AddHits is not a substitution for OrNext */ + /* since the second condition doesn't have a target hit count, the hits tallied by the first condition are ignored */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_0xH0000=1"); + + /* first condition true, but ignored */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + /* second condition true, overall is true */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + + /* second condition no longer true, overall is not true */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); +} + +static void test_addhits_with_addsource() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(2, (byte(1) + byte(2) == 70) || byte(0) == 0) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001_C:0xH0002=70_0xH0000=0(2)"); + + /* addsource (conditions 1 and 2) is true, condition 3 is true, total of two hits, overall is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* repeated(2, byte(0) == 0 || (byte(1) + byte(2) == 70)) */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0000=0_A:0xH0001=0_0xH0002=70(2)"); + + /* condition 1 is true, addsource (conditions 2 and 3) is true, total of two hits, overall is true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 1); +} + +static void test_subhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, low(4) == 6, deducting = repeated(2, byte(1) == 16)) */ + /* NOTE: cannot have SubHits without AddHits as there's no way to reach the final hit target + * if hits are subtracted but not added */ + assert_parse_condset(&condset, &memrefs, buffer, "D:0xH0001=16(2)_C:0xL0004=6_0=1(4)"); + + /* second condition true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 2); + + /* both conditions true 1+3 == 4, not -1+3 != 4, no trigger */ + ram[1] = 16; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 3); + + /* -2+4 != 4 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 4); + + /* first condition target met, -2+5 != 4 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 5); + + /* total met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 6); +} + +static void test_subhits_below_zero() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(4, low(4) == 6, deducting = repeated(2, byte(1) == 16)) */ + /* NOTE: cannot have SubHits without AddHits as there's no way to reach the final hit target + * if hits are subtracted but not added */ + assert_parse_condset(&condset, &memrefs, buffer, "D:0xH0001=18(2)_C:0xL0002=6_0=1(4)"); + + /* first condition true. -1 less than 0. target hit count is unsigned. + make sure comparison doesn't treat -1 as unsigned */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* first condition target met */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + + /* both conditions true. takes 6 counts on second condition to reach hit target because + first condition is currently -2 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 3); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 4); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 5); + + /* total met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 6); +} + +static void test_addhits_unfinished() { + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[512]; + + assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=0_C:0xH0002=22"); + + ASSERT_NUM_EQUALS(condset->num_other_conditions, 2); +} + +static void test_addhits_float_coercion() { + uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F }; /* fF0004 = 1.5 */ + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* 0 + float(4) * 10 - prev(float(4)) * 10 + 0 + 0 = 1 */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0_A:fF0004*10_B:dfF0004*10_A:0_0=1"); + + /* float(4) = 1.5, prev(float(4)) = 0.0. 0+15-0+0+0=1 = false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* float(4) = 2.0, prev(float(4)) = 1.5. 0+20-15+0+0=1 = false */ + ram[6] = 0x00; + ram[7] = 0x40; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* float(4) = 2.1, prev(float(4)) = 2.0. 0+21-20+0+0=1 = true */ + ram[6] = 0x06; + ram[5] = 0x66; + ram[4] = 0x66; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_andnext() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(3, byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=20_N:0xH0002=20_0xH0003=20.3."); + + /* all conditions are false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* first two are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* all three are true, tally hits. hits are tallied for each true statement starting with the first */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* middle condition not true, only first tallies a hit */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* all three conditions are true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + /* third condition not true, first two tally hits */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + + /* all three conditions are true, hit target reached */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 3); + + /* hit target previously reached */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 6); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 3); + + /* second condition no longer true, only first condition tallied, hit target was previously met */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 7); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 3); +} + +static void test_andnext_boundaries() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000) == 0 && once(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) && byte(0x0000) == 0 */ + assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_0xH0003=20.1._0xH0000=0"); + + /* first and last condition are true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 1); + + /* final condition of AndNext chain is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 2); + + /* two conditions of AndNext chain are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 4, 3); + + /* whole AndNext chain is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 4, 4); +} + +static void test_andnext_resetif() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000) == 0 && never(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_R:0xH0003=20"); + + /* tally a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* final condition of AndNext chain is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* two conditions of AndNext chain are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* middle condition not true */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* third condition not true */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); +} + +static void test_andnext_pauseif() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000) == 0 && unless(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_P:0xH0003=20"); + + /* tally a hit */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* final condition of AndNext chain is not enough */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* two conditions of AndNext chain are true, still not enough */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* whole AndNext chain is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 1); + + /* middle condition not true */ + ram[2] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + assert_hit_count(condset, 3, 0); /* pauseif goes to 0 when not true */ + + /* whole AndNext chain is true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 2); + assert_hit_count(condset, 3, 1); + + /* third condition not true */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 3); + assert_hit_count(condset, 3, 0); /* pauseif goes to 0 when not true */ + + /* whole AndNext chain is true */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 4); + assert_hit_count(condset, 3, 1); +} + +static void test_andnext_addsource() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* once(byte(0x0001) + byte(0x0002) == 20 && byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_N:0xH0002=20_0xH0003=20.1."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition true */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* conditions 2 and 3 true, but AddSource in condition 1 makes condition 2 not true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* AddSource condition true via sum, whole set is true */ + ram[1] = ram[2] = 10; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); +} + +static void test_andnext_addhits() { + uint8_t ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(5, (byte(0) == 1 && byte(0x0001) > prev(byte(0x0001))) || byte(0) == 2 || 0 == 1) */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH00=1_C:0xH01>d0xH01_N:0=1_0=1.2."); + + /* initialize delta */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* second part of AndNext is true, but first is still false */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* both parts of AndNext are true */ + ram[0] = 1; + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); + + /* And Next true again, hit count should match target */ + ram[1] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + assert_hit_count(condset, 3, 0); +} + +static void test_andnext_between_addhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext has higher priority than AddHits + * + * AddHits byte(0x0001) == 20 (2) + * AndNext byte(0x0002) == 20 (2) <-- hit count only applies to line 2, AddHits on line 1 modifies line 3 + * byte(0x0003) == 20 (4) + * + * The AndNext on line 2 will combine with line 3, not line 1, so the overall interpretation is: + * + * repeated(4, repeated(2, byte(0x0001) == 20) || (byte(0x0002) == 20 && byte(0x0003) == 20))) + */ + assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=20.2._N:0xH0002=20.2._0xH0003=20.4."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition is not enough to trigger */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* second condition is true, but only has one hit, so won't increment third */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* first condition true, but not second, only first will increment */ + /* hits from first condition should not cause second condition to act true */ + ram[2] = 0; + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* all three conditions are true. the first has already hit its target hit count, the + * second and third will increment. the total of the first and third is only 3, so no trigger */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 1); + + /* third clause will tally again and set will be true */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); +} + +static void test_andnext_with_hits_chain() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext has higher priority than AddHits + * + * AndNext byte(0x0001) == 20 (1) + * AndNext byte(0x0002) == 20 (1) + * byte(0x0003) == 20 (1) + * + * Line 1 must be true before line 2 can be true, which has to be true before line 3 + * + * a = once(byte(0x0001) == 20) + * b = once(a && byte(0x0002) == 20) + * c = once(b && byte(0x0003) == 20) + */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=20.1._N:0xH0002=20.1._0xH0003=20.1."); + + /* nothing true */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition is not enough to trigger */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* second condition is true, cut can't tally until the first is true */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* first condition is true, but not second, only first will increment */ + ram[2] = 0; + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* final condition cannot tally without the previous items in the chain */ + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* only second condition true. first is historically true, so second can tally */ + ram[3] = ram[1] = 0; + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* only final condition true, first two historically true, so can tally */ + ram[3] = 20; + ram[2] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* nothing true, but all historically true, overall still true */ + ram[3] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); +} + +static void test_andnext_changes_to() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0001) ~> 18 */ + assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=18_d0xH0001!=18"); + + /* value already 18, initial delta value is 0, so its considered changed */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* value already 18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value no longer 18 */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value changes to 18 */ + ram[1] = 18; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* value already 18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_ornext() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* repeated(5, byte(0x0001) == 20 || byte(0x0002) == 20 || byte(0x0003) == 20) */ + assert_parse_condset(&condset, &memrefs, buffer, "O:0xH0001=20_O:0xH0002=20_0xH0003=20.6."); + + /* first condition is true, which chains to make the second and third conditions true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 1); + + /* first and second are true, all but third should update, but only 1 hit each */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 2); + + /* all three true, only increment each once */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 3); + + /* only middle is true, first won't be incremented */ + ram[1] = ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 4); + + /* only last is true, only it will be incremented */ + ram[2] = 30; + ram[3] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 5); + + /* none are true */ + ram[3] = 30; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 4); + assert_hit_count(condset, 2, 5); + + /* first is true, hit target met, set is true */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 5); + assert_hit_count(condset, 2, 6); + + /* hit target met */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 5); + assert_hit_count(condset, 1, 6); + assert_hit_count(condset, 2, 6); +} + +static void test_andnext_ornext_interaction() { + uint8_t ram[] = {0, 0, 0, 0, 0}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* AndNext and OrNext are evaluated at each step: (((1 || 2) && 3) || 4) */ + assert_parse_condset(&condset, &memrefs, buffer, "O:0xH0001=1_N:0xH0002=1_O:0xH0003=1_0xH0004=1"); + + ram[3] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((0 || 0) && 0) || 0) = 0 */ + ram[4] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 0) || 1) = 1 */ + ram[3] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 1) || 1) = 1 */ + ram[4] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((0 || 0) && 1) || 0) = 0 */ + ram[2] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 1) || 0) = 1 */ + ram[1] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 0) = 1 */ + ram[2] = 0; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 0) = 1 */ + ram[3] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((1 || 0) && 0) || 0) = 0 */ + ram[4] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 0) || 1) = 1 */ + ram[3] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 1) = 1 */ +} + +static void test_addaddress_direct_pointer() { + uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000 + byte(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=22"); + + /* initially, byte(0x0000 + 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value, still correct */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_direct_pointer_delta() { + uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* delta(byte(0x0000 + byte(0xh0000))) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_d0xH0000=22"); + + /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + /* byte(0x0000 + 2); value=52, prev=22, prior=22 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 2); value=52, prev=52, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value, still correct */ + /* byte(0x0000 + 1); value=22, prev=22, prior=52 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + /* byte(0x0000 + 1); value=11, prev=22, prior=22 */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 1); value=11, prev=11, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to secondary value, which is correct */ + /* byte(0x0000 + 2); value=22, prev=11, prior=11 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=11 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_direct_pointer_prior() { + uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* delta(byte(0x0000 + byte(0xh0000))) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_p0xH0000=22"); + + /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to new value */ + /* byte(0x0000 + 2); value=52, prev=22, prior=22 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 2); value=52, prev=52, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* new pointed-at value is correct */ + /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to original value, still correct */ + /* byte(0x0000 + 1); value=22, prev=22, prior=52 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* original value no longer correct */ + /* byte(0x0000 + 1); value=11, prev=22, prior=22 */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 1); value=11, prev=11, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to secondary value, which is correct */ + /* byte(0x0000 + 2); value=22, prev=11, prior=11 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=11 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=32, prev=22, prior=22 */ + ram[2] = 32; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_direct_pointer_change() { + uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000 + byte(0xh0000)) == 20 && delta(byte(0x0000 + byte(0xh0000))) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=20_I:0xH0000=0_d0xH0000=22"); + + /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 1); value=20, prev=22, prior=22 */ + ram[1] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0x0000 + 1); value=20, prev=20, prior=22 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to new value */ + /* byte(0x0000 + 2); value=52, prev=20, prior=20 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=52, prev=52, prior=20 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to original value. looks correct! */ + /* byte(0x0000 + 1); value=20, prev=22, prior=22 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to secondary value, which is not correct */ + /* byte(0x0000 + 2); value=22, prev=20, prior=20 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=22, prev=22, prior=20 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0x0000 + 2); value=20, prev=22, prior=22 */ + ram[2] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer() { + uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002 + byte(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xH0002=22"); + + /* initially, byte(0x0001 + 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* non-offset value is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer_negative() { + uint8_t ram[] = {0x02, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0xh0000) - 1) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xHFFFFFFFF=22"); + + /* initially, byte(0x0002 - 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* non-offset value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to invalid address */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* point to already correct value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer_out_of_range() { + uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56, 0x16}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram) - 1; /* purposely hide ram[5] */ + + /* byte(0x0002 + byte(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xH0002=22"); + + /* pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* way out of bounds */ + ram[0] = 100; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* boundary condition - ram[5] value is correct, but should be unreachable */ + /* NOTE: address validation must be handled by the registered 'peek' callback */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_indirect_pointer_multiple() { + uint8_t ram[] = {0x01, 0x02, 0x03, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* the expectation is that the AddAddress lines will share rc_memref_value_t's, but the following lines + will generate their own rc_memref_value_t's for indirection. none of that is actually verified. */ + assert_parse_condset(&condset, &memrefs, buffer, + "I:0xH0000=0_0xH0002=22_I:0xH0000=0_0xH0003=23_I:0xH0001=0_0xH0003=24"); + /* $(0002 + $0000) == 22 && $(0003 + $0000) == 23 && $(0003 + $0001) == 24 */ + /* $0003 (0x34) == 22 && $0004 (0xAB) == 23 && $0005 (0x56) == 24 */ + + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 5, 0); + + /* first condition is true */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 3, 0); + assert_hit_count(condset, 5, 0); + + /* second condition is true */ + ram[4] = 23; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 3, 1); + assert_hit_count(condset, 5, 0); + + /* third condition is true */ + ram[5] = 24; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 3, 2); + assert_hit_count(condset, 5, 1); +} + +static void test_addaddress_indirect_pointer_with_delta() +{ + uint8_t ram[] = { 0x01, 0x02, 0x03, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0)*2+2) == 22 && prev(byte(byte(0)*2+2) == 20 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xH0002=22_I:0xH0000*2_d0xH0002=20"); + + /* byte(0) = 1, byte(1*2+2 [4]) = 0xAB, delta = 0 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 3, 0); + + /* byte(0) = 1, byte(1*2+2 [4]) = 22, delta = 0xAB */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 3, 0); + + /* byte(0) = 1, byte(1*2+2 [4]) = 20, delta = 22 */ + ram[4] = 20; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 3, 0); + + /* byte(0) = 0, byte(0*2+2 [2]) = 2, delta = 20 */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 3, 1); + + /* byte(0) = 0, byte(0*2+2 [2]) = 20, delta = 2 */ + ram[2] = 20; + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 3, 1); + + /* byte(0) = 1, byte(1*2+2 [4]) = 22, delta = 20 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 3, 2); +} + +static void test_addaddress_indirect_pointer_with_subsource() +{ + uint8_t ram[] = { 0x01, 0x02, 0x03, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0)*2+2) == 22 - prev(byte(byte(0)*2+2) == 20 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_B:d0xH0002_I:0xH0000*2_0xH0002=20"); + + /* byte(0) = 1, byte(1*2+2 [4]) = 0xAB, delta = 0, diff = 0xAB */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0) = 1, byte(1*2+2 [4]) = 0x22, delta = 0xAB, diff = 0x77 */ + ram[4] = 0x22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0) = 1, byte(1*2+2 [4]) = 0x36, delta = 0x22, diff = 0x14 */ + ram[4] = 0x36; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0) = 0, byte(0*2+2 [2]) = 0x03, delta = 0x36, diff = 0xCD */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* byte(0) = 0, byte(0*2+2 [2]) = 0x17, delta = 0x03, diff = 0x14 */ + ram[2] = 0x17; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* byte(0) = 1, byte(1*2+2 [4]) = 0x2B, delta = 0x17, diff = 0x14 */ + ram[0] = 1; + ram[4] = 0x2B; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer_with_subsource_float() +{ + uint8_t ram[] = { 0x02, 0x05, 0x00, 0x00, 0x42, 0x70, 0x00, 0x00, 0x41, 0xc4, 0x00, 0x00 }; + /* ^ ------ 60.0 ------ ^ ^ ------ 24.5 ------ ^ */ + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* float(byte(0) + 2) - float(byte(1) + 3) == 15.0 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0001_B:fB0003_I:0xH0000_fB0002=f15.0"); + + /* float(4) = 60.0, float(8) = 24.5, 60.0 - 24.5 = 35.5 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* float(4) = 60.0, float(8) = 45.0, 60.0 - 45.0 = 15.0 */ + ram[8] = 0x42; + ram[9] = 0x34; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* float(8) = 45.0, float(4) = 60.0, 45.0 - 60.0 = -15.0 */ + ram[0] = 0x06; + ram[1] = 0x01; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* float(8) = 24.5, float(4) = 60.0, 24.5 - 60.0 = -35.5 */ + ram[8] = 0x41; + ram[9] = 0xc4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* float(8) = 24.5, float(4) = 9.5, 24.5 - 9.5 = 15.0 */ + ram[4] = 0x41; + ram[5] = 0x18; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_pointer_from_memory() { + uint8_t ram[] = { 0x01, 0x01, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0x0000) + byte(0x0001)) == 22 --- byte(0x0000) = pointer, byte(0x0001) = index */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000+0xH0001_0xH0000=22"); + + /* initially, byte(1 + 1) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[1] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_indirect_constant() { + uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002 + 1) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:1_0xH0002=22"); + + /* initially, byte(0x0003) == 22, false */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* memory at constant is correct */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* memory at address is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* memory at offset address is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_pointer_data_size_differs_from_pointer_size() { + uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002 + word(0xh0000)) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0x 0002=22"); + + /* 8-bit pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 16-bit pointed-at value is correct */ + ram[4] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to new value */ + ram[0] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is only partially correct */ + ram[3] = 0; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_double_indirection() { + uint8_t ram[] = {0x01, 0x02, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0000 + byte(0x0000 + byte(0x0000))) == 22 | $($($0000))) == 22*/ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_I:0xH0000=0_0xH0000=22"); + + /* value is correct: $0000=1, $0001=2, $0002 = 22 */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* second pointer in chain causes final pointer to point at address 3 */ + ram[1] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new pointed-at value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* first pointer points at address 2, which is 22, so out-of-bounds */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* second pointer points at address 3, which is correct */ + ram[2] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* first pointer is out of range, so returns 0 for the second pointer, $0 contains the correct value */ + ram[0] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_double_indirection_with_delta() { + uint8_t ram[] = { 0, 2, 4 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* prev(byte(0x0000 + byte(0x0000 + byte(0x0000)))) == 22 | prev($($($0000)))) == 22*/ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_I:0xH0000=0_d0xH0000=4"); + + /* NOTE: indirectly calculated memrefs keep their own delta, not the delta of the newly pointed to + * value. using the intermediate deltas to calculate an address for the current frame will + * generate incorrect values. Only the final item in the chain should have the delta. */ + + /* 1st frame: A = Mem[0] = 0 (delta[0] = 0), B = Mem[A] = Mem[0] = 0 (delta B = 0), C = Mem[B] = Mem[0] = 0 (delta C = 0) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 2nd frame: A = Mem[0] = 1 (delta[0] = 0), B = Mem[A] = Mem[1] = 2 (delta B = 0), C = Mem[B] = Mem[2] = 4 (delta C = 0) */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 3nd frame: A = Mem[0] = 1 (delta[0] = 1), B = Mem[A] = Mem[1] = 2 (delta B = 2), C = Mem[B] = Mem[2] = 4 (delta C = 4) */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_double_indirection_with_delta_incorrect() { + uint8_t ram[] = { 0, 2, 4 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* prev(byte(0x0000 + prev(byte(0x0000 + prev(byte(0x0000)))))) == 22 | prev($($($0000)))) == 22*/ + assert_parse_condset(&condset, &memrefs, buffer, "I:d0xH0000=0_I:d0xH0000=0_d0xH0000=4"); + + /* putting prevs on each step of the chain results in using old pointers to get to data that may not exist yet, + * but this validates that incorrect behavior */ + + /* 1st frame: Mem[0] = 0 (delta[0] = 0), B = Mem[deltaA] = Mem[0] = 0 (delta B = 0), C = Mem[deltaB] = Mem[0] = 0 (delta C = 0) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 2nd frame: Mem[0] = 1 (delta[0] = 0), B = Mem[deltaA] = Mem[0] = 1 (delta B = 0), C = Mem[deltaB] = Mem[0] = 1 (delta C = 0) */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 3rd frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 1), C = Mem[deltaB] = Mem[1] = 2 (delta C = 1) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 4th frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 2), C = Mem[deltaB] = Mem[2] = 4 (delta C = 2) */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* 5th frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 2), C = Mem[deltaB] = Mem[2] = 4 (delta C = 4) */ + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_adjust_both_sides() { + uint8_t ram[] = {0x02, 0x11, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0) > delta $($0) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000>d0xH0000"); + + /* initial delta will be 0, so 2 will be greater */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* delta should be the same as current */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value increased */ + ram[2]++; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* value decreased */ + ram[2]--; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* this is a small hiccup in the AddAddress behavior. when the pointer changes, we + * can't reasonably know the previous value, so delta will be 0 for the first frame. + * 52 is greater than 0 (even though it didn't change), so set will be true. */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_adjust_both_sides_different_bases() { + uint8_t ram[] = {0x02, 0x11, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0) == $($0 + 1) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=0xH0001"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* values are the same */ + ram[2] = ram[3]; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust pointer */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* values are the same */ + ram[1] = ram[2]; + assert_evaluate_condset(condset, memrefs, &memory, 1); +} + +static void test_addaddress_scaled() { + uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0 * 2) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xH0000=22"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value is correct */ + ram[2] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust pointer */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new value is correct */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[2] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_scaled_negative() { + uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x01}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($4 * -1 + 2) */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0004*4294967295_0xH0002=22"); /* 4294967295 = 0xFFFFFFFF = -1 */ + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value is correct: $(1 * -1 + 2) = $(1) */ + ram[1] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust pointer: $(2 * -1 + 2) = $(0) */ + ram[4] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new value is correct */ + ram[0] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value */ + ram[4] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[1] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_double_read() { + uint8_t ram[] = {0x01, 0x02, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* $($0 + $1) == 22 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000+0xH0001_0xH0000=22"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value is correct */ + ram[3] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* adjust first address */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* new value is correct */ + ram[4] = 22; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* point to original value */ + ram[1] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* original value no longer correct */ + ram[3] = 11; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_addaddress_shared_size() { + uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* bit2(byte(0) * 2) == 1 */ + assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xO0000=1"); + + /* bit2( [0x34] ) = 1 */ + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* bit2( [0x01] ) = 0 */ + ram[2] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* adjust pointer. bit2( [0x56] ) = 1 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 1); + + /* new value is correct */ + ram[4] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); +} + +static void test_prior_sequence() { + uint8_t ram[] = {0x00}; + memory_t memory; + rc_condset_t* condset; + rc_memrefs_t memrefs; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* prior(bit0(0))==1 && prior(bit1(0))==1 && prior(bit2(0))==1 */ + assert_parse_condset(&condset, &memrefs, buffer, "p0xM0000=1_p0xN0000=1_p0xO0000=1"); + assert_evaluate_condset(condset, memrefs, &memory, 0); + + /* value ~> 1 [0001], all priors still 0 */ + ram[0] = 1; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 0); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* value ~> 2 [0010], prior(bit0(0)) = 1, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 2; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* value ~> 3 [0011], prior(bit0(0)) = 0, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 3; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 1); + assert_hit_count(condset, 1, 0); + assert_hit_count(condset, 2, 0); + + /* value ~> 4 [0100], prior(bit0(0)) = 1, prior(bit1(0)) = 1, prior(bit2(0)) = 0 */ + ram[0] = 4; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 1); + assert_hit_count(condset, 2, 0); + + /* value ~> 5 [0101], prior(bit0(0)) = 0, prior(bit1(0)) = 1, prior(bit2(0)) = 0 */ + ram[0] = 5; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 2); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* value ~> 6 [0110], prior(bit0(0)) = 1, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 6; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* value ~> 7 [0111], prior(bit0(0)) = 0, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ + ram[0] = 7; + assert_evaluate_condset(condset, memrefs, &memory, 0); + assert_hit_count(condset, 0, 3); + assert_hit_count(condset, 1, 2); + assert_hit_count(condset, 2, 0); + + /* value ~> 8 [1000], prior(bit0(0)) = 1, prior(bit1(0)) = 1, prior(bit2(0)) = 1 */ + ram[0] = 8; + assert_evaluate_condset(condset, memrefs, &memory, 1); + assert_hit_count(condset, 0, 4); + assert_hit_count(condset, 1, 3); + assert_hit_count(condset, 2, 1); +} + +static void test_get_real_operand1(void) +{ + rc_condset_t* condset; + rc_condition_t* condition; + const rc_operand_t* operand; + rc_memrefs_t memrefs; + char buffer[2048]; + + assert_parse_condset(&condset, &memrefs, buffer, "A:0xH1234*100_A:0xH1235*10_M:0xH1236>850"); + + condition = condset->conditions; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 0x1234); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 0x1235); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 1); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 0x1236); + ASSERT_NUM_EQUALS(operand->is_combining, 0); +} + +static void test_get_real_operand1_recall(void) +{ + rc_condset_t* condset; + rc_condition_t* condition; + const rc_operand_t* operand; + rc_memrefs_t memrefs; + char buffer[2048]; + + assert_parse_condset(&condset, &memrefs, buffer, "A:1_K:0xH0001_I:{recall}_I:0xH0002_0xH0003=4"); + + condition = condset->conditions; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_CONST); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_32_BITS); + ASSERT_NUM_EQUALS(operand->value.num, 1); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 1); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 1); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_RECALL); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_32_BITS); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 2); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 3); + ASSERT_NUM_EQUALS(operand->is_combining, 0); +} + +static void test_get_real_operand1_indirect_static() +{ + rc_condset_t* condset; + rc_condition_t* condition; + const rc_operand_t* operand; + rc_memrefs_t memrefs; + char buffer[2048]; + + assert_parse_condset(&condset, &memrefs, buffer, "I:8_I:0xH0010_0xH0020=2"); + + condition = condset->conditions; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_CONST); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_32_BITS); + ASSERT_NUM_EQUALS(operand->value.num, 8); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 0x0010); + ASSERT_NUM_EQUALS(operand->is_combining, 0); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); + operand = rc_condition_get_real_operand1(condition); + ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(operand->value.memref->address, 0x0020); + ASSERT_NUM_EQUALS(operand->is_combining, 0); +} + +static void test_ignore_parse_errors(const char* memaddr, uint8_t is_value, int32_t expected_error) +{ + const char* memaddr_test; + + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); + preparse.parse.is_value = is_value; + + memaddr_test = memaddr; + rc_parse_condset(&memaddr_test, &preparse.parse); + ASSERT_NUM_EQUALS(preparse.parse.offset, expected_error); + rc_destroy_preparse_state(&preparse); + + rc_init_preparse_state(&preparse); + preparse.parse.is_value = is_value; + preparse.parse.ignore_non_parse_errors = 1; + + memaddr_test = memaddr; + rc_parse_condset(&memaddr_test, &preparse.parse); + ASSERT_NUM_GREATER(preparse.parse.offset, 0); + rc_destroy_preparse_state(&preparse); +} + +void test_condset(void) { + TEST_SUITE_BEGIN(); + + /* hit counts */ + TEST(test_hitcount_increment_when_true); + TEST(test_hitcount_does_not_increment_when_false); + TEST(test_hitcount_target); + + /* two conditions */ + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001=18_0xH0002=52", 1, 1, 1); + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001=18_0xH0002!=52", 0, 1, 0); + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001>18_0xH0002=52", 0, 0, 1); + TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001<18_0xH0002>52", 0, 0, 0); + + /* three conditions */ + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002=52_0xL0004=6", 1, 1, 1, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002=52_0xL0004>6", 0, 1, 1, 0); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002<52_0xL0004=6", 0, 1, 0, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002<52_0xL0004>6", 0, 1, 0, 0); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002=52_0xL0004=6", 0, 0, 1, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002=52_0xL0004>6", 0, 0, 1, 0); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002<52_0xL0004=6", 0, 0, 0, 1); + TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002<52_0xL0004>6", 0, 0, 0, 0); + + /* pauseif */ + TEST(test_pauseif); + TEST(test_pauseif_hitcount_one); + TEST(test_pauseif_hitcount_two); + TEST(test_pauseif_hitcount_with_reset); + TEST(test_pauseif_resetnextif); + TEST(test_pauseif_does_not_increment_hits); + TEST(test_pauseif_delta_updated); + TEST(test_pauseif_indirect_delta_updated); + TEST(test_pauseif_short_circuit); + + /* resetif */ + TEST(test_resetif); + TEST(test_resetif_cond_with_hittarget); + TEST(test_resetif_hitcount); + TEST(test_resetif_hitcount_one); + TEST(test_resetif_hitcount_addhits); + + TEST(test_pauseif_resetif_hitcounts); + + /* resetnextif */ + TEST(test_resetnextif); + TEST(test_resetnextif_non_hitcount_condition); + TEST(test_resetnextif_addhits); + TEST(test_resetnextif_addhits_chain); + TEST(test_resetnextif_addhits_chain_total); + TEST(test_resetnextif_using_andnext); + TEST(test_resetnextif_andnext); + TEST(test_resetnextif_andnext_chain); + TEST(test_resetnextif_andnext_hitchain); + TEST(test_resetnextif_addaddress); + TEST(test_resetnextif_addaddress_andnext_chain); + TEST(test_resetnextif_chain); + TEST(test_resetnextif_chain_andnext); + TEST(test_resetnextif_chain_with_hits); + TEST(test_resetnextif_pause_lock); + TEST(test_resetnextif_unfinished); + + /* addsource/subsource */ + TEST(test_addsource); + TEST(test_addsource_overflow); + TEST(test_subsource); + TEST(test_subsource_legacy_garbage); + TEST(test_subsource_overflow); + TEST(test_addsource_subsource); + TEST(test_addsource_multiply); + TEST(test_subsource_multiply); + TEST(test_addsource_multiply_fraction); + TEST(test_addsource_multiply_address); + TEST(test_addsource_divide); + TEST(test_addsource_divide_address); + TEST(test_addsource_divide_self); + TEST(test_subsource_divide); + TEST(test_addsource_compare_percentage); + TEST(test_addsource_mask); + TEST(test_addsource_xor); + TEST(test_addsource_float_first); + TEST(test_addsource_float_second); + TEST(test_subsource_mask); + TEST(test_subsource_overflow_comparison_equal); + TEST(test_subsource_overflow_comparison_greater); + TEST(test_subsource_overflow_comparison_greater_or_equal); + TEST(test_subsource_overflow_comparison_lesser); + TEST(test_subsource_overflow_comparison_lesser_or_equal); + TEST(test_subsource_float); + TEST(test_addsource_bits_change); + TEST(test_addsource_bcd); + TEST(test_addsource_invert); + TEST(test_subsource_mem_delta); + TEST(test_subsource_mem_prior); + TEST(test_addsource_unfinished); + TEST(test_addsource_float_division); + TEST(test_addsource_recall_float_division); + TEST(test_addsource_long_chain); + + /* addhits/subhits */ + TEST(test_addhits); + TEST(test_addhits_no_target); + TEST(test_addhits_with_addsource); + TEST(test_addhits_multiple); + TEST(test_subhits); + TEST(test_subhits_below_zero); + TEST(test_addhits_unfinished); + TEST(test_addhits_float_coercion) + + /* andnext */ + TEST(test_andnext); + TEST(test_andnext_boundaries); + TEST(test_andnext_resetif); + TEST(test_andnext_pauseif); + TEST(test_andnext_addsource); + TEST(test_andnext_addhits); + TEST(test_andnext_between_addhits); + TEST(test_andnext_with_hits_chain); + TEST(test_andnext_changes_to); + + /* ornext */ + TEST(test_ornext); + TEST(test_andnext_ornext_interaction); + + /* addaddress */ + TEST(test_addaddress_direct_pointer); + TEST(test_addaddress_direct_pointer_delta); + TEST(test_addaddress_direct_pointer_prior); + TEST(test_addaddress_direct_pointer_change); + TEST(test_addaddress_indirect_pointer); + TEST(test_addaddress_indirect_pointer_negative); + TEST(test_addaddress_indirect_pointer_out_of_range); + TEST(test_addaddress_indirect_pointer_multiple); + TEST(test_addaddress_indirect_pointer_with_delta); + TEST(test_addaddress_indirect_pointer_with_subsource); + TEST(test_addaddress_indirect_pointer_with_subsource_float); + TEST(test_addaddress_indirect_pointer_from_memory); + TEST(test_addaddress_indirect_constant); + TEST(test_addaddress_pointer_data_size_differs_from_pointer_size); + TEST(test_addaddress_double_indirection); + TEST(test_addaddress_double_indirection_with_delta); + TEST(test_addaddress_double_indirection_with_delta_incorrect); + TEST(test_addaddress_adjust_both_sides); + TEST(test_addaddress_adjust_both_sides_different_bases); + TEST(test_addaddress_scaled); + TEST(test_addaddress_scaled_negative); + TEST(test_addaddress_double_read); + TEST(test_addaddress_shared_size); + + /* prior */ + TEST(test_prior_sequence); + + /* get_real_operand1 */ + TEST(test_get_real_operand1); + TEST(test_get_real_operand1_recall); + TEST(test_get_real_operand1_indirect_static); + + /* ignore parse errors */ + TEST_PARAMS3(test_ignore_parse_errors, "M:0x1234=5_M:0x1234=6", 0, RC_MULTIPLE_MEASURED); + TEST_PARAMS3(test_ignore_parse_errors, "M:0x1234", 0, RC_INVALID_OPERATOR); /* right side required for Measured non-value */ + TEST_PARAMS3(test_ignore_parse_errors, "M:0x1234=0x2345", 0, RC_INVALID_MEASURED_TARGET); + TEST_PARAMS3(test_ignore_parse_errors, "0x1234=6", 1, RC_INVALID_VALUE_FLAG); + TEST_PARAMS3(test_ignore_parse_errors, "T:0x1234=6", 1, RC_INVALID_VALUE_FLAG); + TEST_PARAMS3(test_ignore_parse_errors, "R:0x1234", 0, RC_INVALID_OPERATOR); + TEST_PARAMS3(test_ignore_parse_errors, "K:0x1234<6", 0, RC_INVALID_OPERATOR); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_consoleinfo.c b/src/rcheevos/test/rcheevos/test_consoleinfo.c new file mode 100644 index 0000000000..f761016fdb --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_consoleinfo.c @@ -0,0 +1,203 @@ +#include "rc_consoles.h" + +#include "../test_framework.h" + +static void test_name(uint32_t console_id, const char* expected_name) +{ + ASSERT_STR_EQUALS(rc_console_name(console_id), expected_name); +} + +static void test_memory(uint32_t console_id, uint32_t expected_total_memory, uint32_t expected_searchable_memory) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(console_id); + uint32_t total_memory = 0; + uint32_t searchable_memory = 0; + uint32_t max_address = 0; + uint32_t i; + ASSERT_PTR_NOT_NULL(regions); + + if (expected_total_memory == 0) + { + ASSERT_NUM_EQUALS(regions->num_regions, 0); + return; + } + + ASSERT_NUM_GREATER(regions->num_regions, 0); + for (i = 0; i < regions->num_regions; ++i) { + uint32_t region_size = (regions->region[i].end_address - regions->region[i].start_address + 1); + total_memory += region_size; + + if (regions->region[i].type != RC_MEMORY_TYPE_UNUSED) + searchable_memory += region_size; + + if (regions->region[i].end_address > max_address) + max_address = regions->region[i].end_address; + + ASSERT_PTR_NOT_NULL(regions->region[i].description); + } + + ASSERT_NUM_EQUALS(total_memory, expected_total_memory); + ASSERT_NUM_EQUALS(searchable_memory, expected_searchable_memory); + ASSERT_NUM_EQUALS(max_address, expected_total_memory - 1); +} + +void test_consoleinfo(void) { + TEST_SUITE_BEGIN(); + + /* use raw numbers instead of constants to ensure constants don't change */ + TEST_PARAMS2(test_name, 0, "Unknown"); + TEST_PARAMS2(test_name, 1, "Sega Genesis"); + TEST_PARAMS2(test_name, 2, "Nintendo 64"); + TEST_PARAMS2(test_name, 3, "Super Nintendo Entertainment System"); + TEST_PARAMS2(test_name, 4, "GameBoy"); + TEST_PARAMS2(test_name, 5, "GameBoy Advance"); + TEST_PARAMS2(test_name, 6, "GameBoy Color"); + TEST_PARAMS2(test_name, 7, "Nintendo Entertainment System"); + TEST_PARAMS2(test_name, 8, "PC Engine"); + TEST_PARAMS2(test_name, 9, "Sega CD"); + TEST_PARAMS2(test_name, 10, "Sega 32X"); + TEST_PARAMS2(test_name, 11, "Master System"); + TEST_PARAMS2(test_name, 12, "PlayStation"); + TEST_PARAMS2(test_name, 13, "Atari Lynx"); + TEST_PARAMS2(test_name, 14, "Neo Geo Pocket"); + TEST_PARAMS2(test_name, 15, "Game Gear"); + TEST_PARAMS2(test_name, 16, "GameCube"); + TEST_PARAMS2(test_name, 17, "Atari Jaguar"); + TEST_PARAMS2(test_name, 18, "Nintendo DS"); + TEST_PARAMS2(test_name, 19, "Wii"); + TEST_PARAMS2(test_name, 20, "Wii-U"); + TEST_PARAMS2(test_name, 21, "PlayStation 2"); + TEST_PARAMS2(test_name, 22, "XBOX"); + TEST_PARAMS2(test_name, 23, "Magnavox Odyssey 2"); + TEST_PARAMS2(test_name, 24, "Pokemon Mini"); + TEST_PARAMS2(test_name, 25, "Atari 2600"); + TEST_PARAMS2(test_name, 26, "MS-DOS"); + TEST_PARAMS2(test_name, 27, "Arcade"); + TEST_PARAMS2(test_name, 28, "Virtual Boy"); + TEST_PARAMS2(test_name, 29, "MSX"); + TEST_PARAMS2(test_name, 30, "Commodore 64"); + TEST_PARAMS2(test_name, 31, "ZX-81"); + TEST_PARAMS2(test_name, 32, "Oric"); + TEST_PARAMS2(test_name, 33, "SG-1000"); + TEST_PARAMS2(test_name, 34, "VIC-20"); + TEST_PARAMS2(test_name, 35, "Amiga"); + TEST_PARAMS2(test_name, 36, "Atari ST"); + TEST_PARAMS2(test_name, 37, "Amstrad CPC"); + TEST_PARAMS2(test_name, 38, "Apple II"); + TEST_PARAMS2(test_name, 39, "Sega Saturn"); + TEST_PARAMS2(test_name, 40, "Dreamcast"); + TEST_PARAMS2(test_name, 41, "PlayStation Portable"); + TEST_PARAMS2(test_name, 42, "CD-I"); + TEST_PARAMS2(test_name, 43, "3DO"); + TEST_PARAMS2(test_name, 44, "ColecoVision"); + TEST_PARAMS2(test_name, 45, "Intellivision"); + TEST_PARAMS2(test_name, 46, "Vectrex"); + TEST_PARAMS2(test_name, 47, "PC-8000/8800"); + TEST_PARAMS2(test_name, 48, "PC-9800"); + TEST_PARAMS2(test_name, 49, "PC-FX"); + TEST_PARAMS2(test_name, 50, "Atari 5200"); + TEST_PARAMS2(test_name, 51, "Atari 7800"); + TEST_PARAMS2(test_name, 52, "X68K"); + TEST_PARAMS2(test_name, 53, "WonderSwan"); + TEST_PARAMS2(test_name, 54, "CassetteVision"); + TEST_PARAMS2(test_name, 55, "Super CassetteVision"); + TEST_PARAMS2(test_name, 56, "Neo Geo CD"); + TEST_PARAMS2(test_name, 57, "Fairchild Channel F"); + TEST_PARAMS2(test_name, 58, "FM Towns"); + TEST_PARAMS2(test_name, 59, "ZX Spectrum"); + TEST_PARAMS2(test_name, 60, "Game & Watch"); + TEST_PARAMS2(test_name, 61, "Nokia N-Gage"); + TEST_PARAMS2(test_name, 62, "Nintendo 3DS"); + TEST_PARAMS2(test_name, 63, "Watara Supervision"); + TEST_PARAMS2(test_name, 64, "Sharp X1"); + TEST_PARAMS2(test_name, 65, "TIC-80"); + TEST_PARAMS2(test_name, 66, "Thomson TO8"); + TEST_PARAMS2(test_name, 67, "PC-6000"); + TEST_PARAMS2(test_name, 68, "Sega Pico"); + TEST_PARAMS2(test_name, 69, "Mega Duck"); + TEST_PARAMS2(test_name, 70, "Zeebo"); + TEST_PARAMS2(test_name, 71, "Arduboy"); + TEST_PARAMS2(test_name, 72, "WASM-4"); + TEST_PARAMS2(test_name, 73, "Arcadia 2001"); + TEST_PARAMS2(test_name, 74, "Interton VC 4000"); + TEST_PARAMS2(test_name, 75, "Elektor TV Games Computer"); + TEST_PARAMS2(test_name, 76, "PC Engine CD"); + TEST_PARAMS2(test_name, 77, "Atari Jaguar CD"); + TEST_PARAMS2(test_name, 78, "Nintendo DSi"); + TEST_PARAMS2(test_name, 79, "TI-83"); + TEST_PARAMS2(test_name, 80, "Uzebox"); + TEST_PARAMS2(test_name, 81, "Famicom Disk System"); + TEST_PARAMS2(test_name, 82, "Unknown"); + + TEST_PARAMS2(test_name, 100, "Hubs"); + TEST_PARAMS2(test_name, 101, "Events"); + TEST_PARAMS2(test_name, 102, "Standalone"); + + /* memory maps */ + TEST_PARAMS3(test_memory, RC_CONSOLE_UNKNOWN, 0x000000, 0x000000); + TEST_PARAMS3(test_memory, RC_CONSOLE_3DO, 0x200000, 0x200000); + TEST_PARAMS3(test_memory, RC_CONSOLE_AMIGA, 0x100000, 0x100000); + TEST_PARAMS3(test_memory, RC_CONSOLE_AMSTRAD_PC, 0x090000, 0x090000); + TEST_PARAMS3(test_memory, RC_CONSOLE_APPLE_II, 0x020000, 0x020000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ARCADE, 0x000000, 0x000000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ARCADIA_2001, 0x000300, 0x000300); + TEST_PARAMS3(test_memory, RC_CONSOLE_ARDUBOY, 0x000F00, 0x000F00); + TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_2600, 0x000080, 0x000080); + TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_7800, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_JAGUAR, 0x200000, 0x200000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_JAGUAR_CD, 0x200000, 0x200000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_LYNX, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_COLECOVISION, 0x008400, 0x008400); + TEST_PARAMS3(test_memory, RC_CONSOLE_COMMODORE_64, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_DREAMCAST, 0x01000000, 0x01000000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, 0x001800, 0x001700); + TEST_PARAMS3(test_memory, RC_CONSOLE_FAIRCHILD_CHANNEL_F, 0x010C40, 0x010C40); + TEST_PARAMS3(test_memory, RC_CONSOLE_FAMICOM_DISK_SYSTEM, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_GAMEBOY, 0x034000, 0x02DFA0); + TEST_PARAMS3(test_memory, RC_CONSOLE_GAMEBOY_COLOR, 0x034000, 0x033FA0); + TEST_PARAMS3(test_memory, RC_CONSOLE_GAMEBOY_ADVANCE, 0x058000, 0x058000); + TEST_PARAMS3(test_memory, RC_CONSOLE_GAMECUBE, 0x01800000, 0x01800000); + TEST_PARAMS3(test_memory, RC_CONSOLE_GAME_GEAR, 0x00A000, 0x00A000); + TEST_PARAMS3(test_memory, RC_CONSOLE_INTELLIVISION, 0x040080, 0x03BC00); + TEST_PARAMS3(test_memory, RC_CONSOLE_INTERTON_VC_4000, 0x000600, 0x000600); + TEST_PARAMS3(test_memory, RC_CONSOLE_MAGNAVOX_ODYSSEY2, 0x000140, 0x000140); + TEST_PARAMS3(test_memory, RC_CONSOLE_MASTER_SYSTEM, 0x00A000, 0x00A000); + TEST_PARAMS3(test_memory, RC_CONSOLE_MEGA_DRIVE, 0x020000, 0x020000); + TEST_PARAMS3(test_memory, RC_CONSOLE_MEGADUCK, 0x010000, 0x00FFA0); + TEST_PARAMS3(test_memory, RC_CONSOLE_MSX, 0x080000, 0x080000); + TEST_PARAMS3(test_memory, RC_CONSOLE_MS_DOS, 0x4200000, 0x4140000); + TEST_PARAMS3(test_memory, RC_CONSOLE_NEOGEO_POCKET, 0x004000, 0x004000); + TEST_PARAMS3(test_memory, RC_CONSOLE_NEO_GEO_CD, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO_64, 0x800000, 0x800000); + TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO_DS, 0x01004000, 0x00404000); + TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO_DSI, 0x01004000, 0x01004000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ORIC, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_PC8800, 0x011000, 0x011000); + TEST_PARAMS3(test_memory, RC_CONSOLE_PC_ENGINE, 0x02000, 0x02000); + TEST_PARAMS3(test_memory, RC_CONSOLE_PC_ENGINE_CD, 0x42800, 0x42800); + TEST_PARAMS3(test_memory, RC_CONSOLE_PCFX, 0x210000, 0x210000); + TEST_PARAMS3(test_memory, RC_CONSOLE_PLAYSTATION, 0x200400, 0x200400); + TEST_PARAMS3(test_memory, RC_CONSOLE_PLAYSTATION_2, 0x02004000, 0x02004000); + TEST_PARAMS3(test_memory, RC_CONSOLE_PSP, 0x02000000, 0x02000000); + TEST_PARAMS3(test_memory, RC_CONSOLE_POKEMON_MINI, 0x002000, 0x002000); + TEST_PARAMS3(test_memory, RC_CONSOLE_SATURN, 0x200000, 0x200000); + TEST_PARAMS3(test_memory, RC_CONSOLE_SEGA_32X, 0x060000, 0x060000); + TEST_PARAMS3(test_memory, RC_CONSOLE_SEGA_CD, 0x0B0000, 0x0B0000); + TEST_PARAMS3(test_memory, RC_CONSOLE_SG1000, 0x006000, 0x006000); + TEST_PARAMS3(test_memory, RC_CONSOLE_SUPER_CASSETTEVISION, 0x010000, 0x00B000); + TEST_PARAMS3(test_memory, RC_CONSOLE_SUPER_NINTENDO, 0x0A0800, 0x0A0800); + TEST_PARAMS3(test_memory, RC_CONSOLE_SUPERVISION, 0x006000, 0x006000); + TEST_PARAMS3(test_memory, RC_CONSOLE_THOMSONTO8, 0x080000, 0x080000); + TEST_PARAMS3(test_memory, RC_CONSOLE_TI83, 0x08000, 0x08000); + TEST_PARAMS3(test_memory, RC_CONSOLE_TIC80, 0x018000, 0x018000); + TEST_PARAMS3(test_memory, RC_CONSOLE_UZEBOX, 0x001000, 0x001000); + TEST_PARAMS3(test_memory, RC_CONSOLE_WASM4, 0x010000, 0x010000); + TEST_PARAMS3(test_memory, RC_CONSOLE_WII, 0x14000000, 0x05800000); + TEST_PARAMS3(test_memory, RC_CONSOLE_WONDERSWAN, 0x090000, 0x090000); + TEST_PARAMS3(test_memory, RC_CONSOLE_VECTREX, 0x000400, 0x000400); + TEST_PARAMS3(test_memory, RC_CONSOLE_VIRTUAL_BOY, 0x020000, 0x020000); + TEST_PARAMS3(test_memory, RC_CONSOLE_ZX_SPECTRUM, 0x020000, 0x020000); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_format.c b/src/rcheevos/test/rcheevos/test_format.c new file mode 100644 index 0000000000..adf27efe72 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_format.c @@ -0,0 +1,112 @@ +#include "rc_internal.h" + +#include "../test_framework.h" + +static void test_format_value(int format, int value, const char* expected) { + char buffer[64]; + int result; + + result = rc_format_value(buffer, sizeof(buffer), value, format); + ASSERT_STR_EQUALS(buffer, expected); + ASSERT_NUM_EQUALS(result, strlen(expected)); +} + +static void test_parse_format(const char* format, int expected) { + ASSERT_NUM_EQUALS(rc_parse_format(format), expected); +} + +void test_format(void) { + TEST_SUITE_BEGIN(); + + /* rc_format_value */ + TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, 12345, "12,345"); + TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, -12345, "-12,345"); + TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, 0xFFFFFFFF, "-1"); + TEST_PARAMS3(test_format_value, RC_FORMAT_UNSIGNED_VALUE, 0xFFFFFFFF, "4,294,967,295"); + TEST_PARAMS3(test_format_value, RC_FORMAT_UNFORMATTED, 0xFFFFFFFF, "4294967295"); /* UNFORMATTED does not add commas */ + TEST_PARAMS3(test_format_value, RC_FORMAT_SCORE, 12345, "012345"); /* SCORE does not add commas */ + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 45, "0:45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 345, "5:45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 12345, "3h25:45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 345, "0:03.45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 12345, "2:03.45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 1234567, "3h25:45.67"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 45, "0h00"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 345, "0h05"); + TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 12345, "3h25"); + TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 45, "0h45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 345, "5h45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 12345, "205h45"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 345, "0:05.75"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 12345, "3:25.75"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 1234567, "5h42:56.11"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, 0, "0.0"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, 1, "0.1"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, 1234, "123.4"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, -1234, "-123.4"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, 0, "0.00"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, 1, "0.01"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, 1234, "12.34"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, -1234, "-12.34"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, 0, "0.000"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, 1, "0.001"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, 1234, "1.234"); + TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, -1234, "-1.234"); + TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, 0, "0"); + TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, 1, "10"); + TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, 1234, "12,340"); + TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, -1234, "-12,340"); + TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, 0, "0"); + TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, 1, "100"); + TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, 1234, "123,400"); + TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, -1234, "-123,400"); + TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, 0, "0"); + TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, 1, "1,000"); + TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, 1234, "1,234,000"); + TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, -1234, "-1,234,000"); + + /* because of the internal conversion to centiseconds, anything above MAX_INT / 10 could overflow */ + TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 0x19999999, "1,988h24:38.81"); + + /* rc_parse_format */ + TEST_PARAMS2(test_parse_format, "VALUE", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "SECS", RC_FORMAT_SECONDS); + TEST_PARAMS2(test_parse_format, "TIMESECS", RC_FORMAT_SECONDS); + TEST_PARAMS2(test_parse_format, "TIME", RC_FORMAT_FRAMES); + TEST_PARAMS2(test_parse_format, "MINUTES", RC_FORMAT_MINUTES); + TEST_PARAMS2(test_parse_format, "SECS_AS_MINS", RC_FORMAT_SECONDS_AS_MINUTES); + TEST_PARAMS2(test_parse_format, "FRAMES", RC_FORMAT_FRAMES); + TEST_PARAMS2(test_parse_format, "SCORE", RC_FORMAT_SCORE); + TEST_PARAMS2(test_parse_format, "POINTS", RC_FORMAT_SCORE); + TEST_PARAMS2(test_parse_format, "MILLISECS", RC_FORMAT_CENTISECS); + TEST_PARAMS2(test_parse_format, "TENS", RC_FORMAT_TENS); + TEST_PARAMS2(test_parse_format, "HUNDREDS", RC_FORMAT_HUNDREDS); + TEST_PARAMS2(test_parse_format, "THOUSANDS", RC_FORMAT_THOUSANDS); + TEST_PARAMS2(test_parse_format, "UNSIGNED", RC_FORMAT_UNSIGNED_VALUE); + TEST_PARAMS2(test_parse_format, "OTHER", RC_FORMAT_SCORE); + TEST_PARAMS2(test_parse_format, "INVALID", RC_FORMAT_VALUE); + + TEST_PARAMS2(test_parse_format, "FLOAT", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FLOAT0", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FLOAT1", RC_FORMAT_FLOAT1); + TEST_PARAMS2(test_parse_format, "FLOAT2", RC_FORMAT_FLOAT2); + TEST_PARAMS2(test_parse_format, "FLOAT3", RC_FORMAT_FLOAT3); + TEST_PARAMS2(test_parse_format, "FLOAT4", RC_FORMAT_FLOAT4); + TEST_PARAMS2(test_parse_format, "FLOAT5", RC_FORMAT_FLOAT5); + TEST_PARAMS2(test_parse_format, "FLOAT6", RC_FORMAT_FLOAT6); + TEST_PARAMS2(test_parse_format, "FLOAT7", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FLOAT10", RC_FORMAT_VALUE); + + TEST_PARAMS2(test_parse_format, "FIXED", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FIXED0", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FIXED1", RC_FORMAT_FIXED1); + TEST_PARAMS2(test_parse_format, "FIXED2", RC_FORMAT_FIXED2); + TEST_PARAMS2(test_parse_format, "FIXED3", RC_FORMAT_FIXED3); + TEST_PARAMS2(test_parse_format, "FIXED4", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FIXED5", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FIXED6", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FIXED7", RC_FORMAT_VALUE); + TEST_PARAMS2(test_parse_format, "FIXED10", RC_FORMAT_VALUE); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_lboard.c b/src/rcheevos/test/rcheevos/test_lboard.c new file mode 100644 index 0000000000..fe568c3c8a --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_lboard.c @@ -0,0 +1,746 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_parse_lboard(rc_lboard_t** lboard, void* buffer, const char* memaddr) +{ + int size; + unsigned* overflow; + + size = rc_lboard_size(memaddr); + ASSERT_NUM_GREATER(size, 0); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + *lboard = rc_parse_lboard(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(*lboard); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_parse_lboard(lboard, buffer, memaddr) ASSERT_HELPER(_assert_parse_lboard(lboard, buffer, memaddr), "assert_parse_lboard") + +static int evaluate_lboard(rc_lboard_t* lboard, memory_t* memory, int* value) { + return rc_evaluate_lboard(lboard, value, peek, memory, NULL); +} + +static void test_simple_leaderboard() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02"); + ASSERT_NUM_EQUALS(lboard->state, RC_LBOARD_STATE_WAITING); + + /* submit is true, but leaderboard has not started */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); /* value is only calculated in STARTED and TRIGGERED states */ + + /* cancel is true - still not started */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start is true - will activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x34); + + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x34); + + /* cancel is true - will deactivate */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* submit is true, but leaderboard is not active */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start is true - will activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x34); + + /* submit is true - will submit */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x34); +} + +static void test_start_and_cancel_same_frame() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=18::SUB:0xH00=3::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start and cancel are both true - should not start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* cancel no longer true - should start */ + ram[1] = 0x13; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* start and cancel are both true - should cancel */ + ram[1] = 0x12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + + /* cancel no longer true, but start still is - shouldn't activate */ + ram[1] = 0x13; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_INACTIVE); + + /* start no longer true - can activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* start true - should start */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_and_submit_same_frame() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start and submit are both true - should trigger */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x34); + + /* disable submit - leaderboard should not start */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_INACTIVE); + + /* disable start - leaderboard can activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* enable start - leaderboard should start */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_and_submit_same_frame_hitcount() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0=1::SUB:1=1::VAL:M:0xH02=52"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start and submit are both true - should trigger. value logic is true, submit one hit */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 1); + + /* disable start - leaderboard can activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* reactivate leaderboard - it should immediately submit again. value logic is still true, submit one hit */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 1); + + /* disable start - leaderboard can activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* reactivate leaderboard - it should immediately submit again. value logic is not true, submit zero hits */ + ram[2] = 55; + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0); +} + +static void test_start_and_conditions() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0_0xH01=0::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* only first start condition true - should not start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* only second start condition true - should not start */ + ram[0] = 1; + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* both conditions true - should start */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_or_conditions() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:S0xH00=1S0xH01=1::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* neither start condition true - should not start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* only second start condition true - should start */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* reset lboard state */ + ram[1] = 0; + lboard->state = RC_LBOARD_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + + /* only first start condition true - should start */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); +} + +static void test_start_resets_value() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH01=0::CAN:0xH01=10::SUB:0xH01=18::VAL:M:0xH02!=d0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* not started */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start condition true - should start */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* tally a couple hits */ + ram[2]++; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + ram[2]++; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 2); + + /* canceled, hitcount kept, but ignored */ + ram[1] = 10; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* must wait one frame to switch back to active */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted, hitcount should be reset */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* tally a hit */ + ram[2]++; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); +} + +static void test_cancel_or_conditions() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:S0xH01=12S0xH02=12::SUB:0xH00=3::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start condition true - should start */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* second cancel condition true - should cancel */ + ram[2] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + + /* reset lboard state */ + ram[2] = 0; + lboard->state = RC_LBOARD_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* first cancel condition true - should cancel */ + ram[1] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); +} + +static void test_submit_and_conditions() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:0xH01=18_0xH03=18::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* only first submit condition is true - should not submit */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* only second submit condition true - should not submit */ + ram[1] = 0; + ram[3] = 18; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* both conditions true - should submit */ + ram[1] = 18; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); +} + +static void test_submit_or_conditions() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:S0xH01=12S0xH03=12::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* neither start condition true - should not submit */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* only second submit condition true - should submit */ + ram[1] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + + /* reset lboard state */ + ram[1] = 0; + lboard->state = RC_LBOARD_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + + /* only first submit condition true - should submit */ + ram[3] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); +} + +static void test_progress() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* start true - should start - value from PRO */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x56); + + /* submit true - should trigger - value from VAL */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x34); +} + +static void test_value_from_hitcount() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02!=d0xH02"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* not started, value should not be tallied */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + ram[2] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* started, value will not be tallied as it hasn't changed */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* value changed, expect tally */ + ram[2] = 11; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + /* not changed, no tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + /* changed, tally */ + ram[2] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 2); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ram[2] = 13; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted, tally should be reset */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* value changed, expect tally */ + ram[2] = 11; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); +} + +static void test_value_from_addhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:C:0xH03=1_M:0xH02=1"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, nothing to tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* second value tallied*/ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); + + /* both values tallied */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 3); + + /* only first value tallied */ + ram[2] = 12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 4); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted, tally should be reset, but first is still true, so it'll be tallied */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 1); +} + +static void test_value_from_float() { + /* bytes 5-8 are the float value for pi */ + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:fF0005"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, nothing to tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 3); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 3); +} + +static void test_value_from_float_scaled() { + /* bytes 5-8 are the float value for pi */ + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:fF0005*100"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, nothing to tally */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 314); + + /* canceled, expect no value */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* waiting to start, expect no value */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* restarted */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 314); +} + +static void test_value_from_division() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02/2"); + ASSERT_NUM_EQUALS(lboard->state, RC_LBOARD_STATE_WAITING); + + /* submit is true, but leaderboard has not started */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); /* value is only calculated in STARTED and TRIGGERED states */ + + /* cancel is true - still not started */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start is true - will activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x1a); + + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x1a); + + /* cancel is true - will deactivate */ + ram[0] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(value, 0); + + /* submit is true, but leaderboard is not active */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(value, 0); + + /* start is true - will activate */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x1a); + + /* submit is true - will submit */ + ram[0] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(value, 0x1a); +} + +static void test_maximum_value_from_conditions() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_lboard_t* lboard; + char buffer[1024]; + int value; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:Q:0xH01=1_M:0x 02$Q:0xH01=2_M:0x 03"); + lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* started, neither value is active */ + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); + + /* first value is active */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0xAB34); + + /* second value is active */ + ram[1] = 2; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x56AB); + + /* value updated */ + ram[3] = 0x12; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0x5612); + + /* neither value is active */ + ram[1] = 3; + ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(value, 0); +} + +static void test_measured_value_and_condition() +{ + rc_lboard_t* lboard; + char buffer[1024]; + + /* a Measured is irrelevant in the STA/CAN/SUB conditions, but if present, allow them to be unique */ + assert_parse_lboard(&lboard, buffer, "STA:M:0xH00=0::CAN:M:0xH00=2::SUB:M:0xH00=3::VAL:M:0xH04"); +} + +static void test_unparsable_lboard(const char* memaddr, int expected_error) { + ASSERT_NUM_EQUALS(rc_lboard_size(memaddr), expected_error); +} + +static void test_unparsable_strings() { + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::GARBAGE", RC_INVALID_LBOARD_FIELD); + TEST_PARAMS2(test_unparsable_lboard, "CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02", RC_MISSING_START); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::SUB:0xH00=3::PRO:0xH04::VAL:0xH02", RC_MISSING_CANCEL); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::PRO:0xH04::VAL:0xH02", RC_MISSING_SUBMIT); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04", RC_MISSING_VALUE); + TEST_PARAMS2(test_unparsable_lboard, "STA:::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_MISSING_START); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:::SUB:0xH00=3::VAL:0xH02", RC_MISSING_CANCEL); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:::VAL:0xH02", RC_MISSING_SUBMIT); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:", RC_MISSING_VALUE); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::STA:0=0", RC_DUPLICATED_START); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::CAN:0=0", RC_DUPLICATED_CANCEL); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::SUB:0=0", RC_DUPLICATED_SUBMIT); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::VAL:0", RC_DUPLICATED_VALUE); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::PRO:0", RC_DUPLICATED_PROGRESS); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH01=1_M:0xH01=2", RC_MULTIPLE_MEASURED); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH01=1_T:0xH01=2", RC_INVALID_VALUE_FLAG); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1_0xH01=2", RC_INVALID_VALUE_FLAG); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1", RC_MISSING_VALUE_MEASURED); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1$M:0xH03", RC_MISSING_VALUE_MEASURED); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02SM:0xH03", RC_INVALID_VALUE_FLAG); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=A::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_MEMORY_OPERAND); + + /* "STA:0xH00=1" is valid, but that leaves the read pointer pointing at the "A", which is not "::", so a generic + * invalid field error is returned. */ + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1A::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_LBOARD_FIELD); + + /* Missing '_' causes "0xH00=10" to be valid, but that leaves the read pointer pointing at the "x", which is not + * "::", so a generic invalid field error is returned. */ + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=10xH01=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_LBOARD_FIELD); + + /* Garbage following value field (legacy format conversion will return invalid comparison) */ + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02=1=2", RC_INVALID_COMPARISON); + TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02=1=2", RC_INVALID_LBOARD_FIELD); +} + +void test_lboard(void) { + TEST_SUITE_BEGIN(); + + TEST(test_simple_leaderboard); + TEST(test_start_and_cancel_same_frame); + TEST(test_start_and_submit_same_frame); + TEST(test_start_and_submit_same_frame_hitcount); + + TEST(test_start_and_conditions); + TEST(test_start_or_conditions); + + TEST(test_start_resets_value); + + TEST(test_cancel_or_conditions); + + TEST(test_submit_and_conditions); + TEST(test_submit_or_conditions); + + TEST(test_progress); + + TEST(test_value_from_hitcount); + TEST(test_value_from_addhits); + TEST(test_value_from_float); + TEST(test_value_from_float_scaled); + TEST(test_value_from_division); + TEST(test_maximum_value_from_conditions); + TEST(test_measured_value_and_condition); + + test_unparsable_strings(); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_memref.c b/src/rcheevos/test/rcheevos/test_memref.c new file mode 100644 index 0000000000..3749ab3bf9 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_memref.c @@ -0,0 +1,520 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +#include +#include /* pow */ + +static void test_mask(char size, uint32_t expected) +{ + ASSERT_NUM_EQUALS(rc_memref_mask(size), expected); +} + +static void test_shared_masks(void) +{ + TEST_PARAMS2(test_mask, RC_MEMSIZE_8_BITS, 0x000000ff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_0, 0x00000001); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_1, 0x00000002); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_2, 0x00000004); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_3, 0x00000008); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_4, 0x00000010); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_5, 0x00000020); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_6, 0x00000040); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_7, 0x00000080); + TEST_PARAMS2(test_mask, RC_MEMSIZE_LOW, 0x0000000f); + TEST_PARAMS2(test_mask, RC_MEMSIZE_HIGH, 0x000000f0); + TEST_PARAMS2(test_mask, RC_MEMSIZE_BITCOUNT, 0x000000ff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_16_BITS, 0x0000ffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_16_BITS_BE, 0x0000ffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_24_BITS, 0x00ffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_24_BITS_BE, 0x00ffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_32_BITS, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_32_BITS_BE, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_FLOAT, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_FLOAT_BE, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_DOUBLE32, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_DOUBLE32_BE, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_MBF32, 0xffffffff); + TEST_PARAMS2(test_mask, RC_MEMSIZE_VARIABLE, 0xffffffff); +} + +static void test_shared_size(char size, char expected) +{ + ASSERT_NUM_EQUALS(rc_memref_shared_size(size), expected); +} + +static void test_shared_sizes(void) +{ + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_8_BITS, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_0, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_1, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_2, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_3, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_4, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_5, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_6, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_7, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_LOW, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_HIGH, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BITCOUNT, RC_MEMSIZE_8_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_16_BITS, RC_MEMSIZE_16_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_16_BITS_BE, RC_MEMSIZE_16_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_24_BITS, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_24_BITS_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_32_BITS, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_32_BITS_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_FLOAT, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_FLOAT_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_DOUBLE32, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_DOUBLE32_BE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_MBF32, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_MBF32_LE, RC_MEMSIZE_32_BITS); + TEST_PARAMS2(test_shared_size, RC_MEMSIZE_VARIABLE, RC_MEMSIZE_32_BITS); +} + +static void test_transform(uint32_t value, uint8_t size, uint32_t expected) +{ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + ASSERT_NUM_EQUALS(typed_value.value.u32, expected); +} + +static void test_transform_float(uint32_t value, uint8_t size, double expected) +{ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + ASSERT_FLOAT_EQUALS(typed_value.value.f32, expected); +} + +static double rc_round(double n) +{ + return floor(n + 0.5); /* no round() in c89 */ +} + +static void test_transform_double32(uint32_t value, uint8_t size, double expected) +{ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + + /* a 20-bit mantissa only has 6 digits of precision. round to 6 digits, then do a float comparison. */ + if (fabs(expected) != 0.0) { + const double digits = floor(log10(fabs(expected))) + 1; + const double expected_pow = pow(10, 6 - digits); + expected = rc_round(expected * expected_pow) / expected_pow; + typed_value.value.f32 = (float)(rc_round(typed_value.value.f32 * expected_pow) / expected_pow); + } + + ASSERT_FLOAT_EQUALS(typed_value.value.f32, expected); +} + +static void test_transform_float_inf(uint32_t value, uint8_t size) +{ + /* C89 does not provide defines for NAN and INFINITY, nor does it provide isnan() or isinf() functions */ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + + if (typed_value.value.f32 < FLT_MAX) { + /* infinity will be greater than max float value */ + ASSERT_FAIL("result of transform is not infinity") + } +} + +static void test_transform_float_nan(uint32_t value, uint8_t size) +{ + /* C89 does not provide defines for NAN and INFINITY, nor does it provide isnan() or isinf() functions */ + rc_typed_value_t typed_value; + typed_value.type = RC_VALUE_TYPE_UNSIGNED; + typed_value.value.u32 = value; + rc_transform_memref_value(&typed_value, size); + + if (typed_value.value.f32 == typed_value.value.f32) { + /* NaN cannot be compared, will fail equality check with itself */ + ASSERT_FAIL("result of transform is not NaN") + } +} + +static void test_transforms(void) +{ + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_8_BITS, 0x00000078); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_16_BITS, 0x00005678); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_24_BITS, 0x00345678); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_32_BITS, 0x12345678); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_LOW, 0x00000008); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_HIGH, 0x00000007); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_BITCOUNT, 0x00000004); /* only counts bits in lowest byte */ + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_16_BITS_BE, 0x00007856); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_24_BITS_BE, 0x00785634); + TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_32_BITS_BE, 0x78563412); + + TEST_PARAMS3(test_transform, 0x00000001, RC_MEMSIZE_BIT_0, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000002, RC_MEMSIZE_BIT_1, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000004, RC_MEMSIZE_BIT_2, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000008, RC_MEMSIZE_BIT_3, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000010, RC_MEMSIZE_BIT_4, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000020, RC_MEMSIZE_BIT_5, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000040, RC_MEMSIZE_BIT_6, 0x00000001); + TEST_PARAMS3(test_transform, 0x00000080, RC_MEMSIZE_BIT_7, 0x00000001); + + TEST_PARAMS3(test_transform, 0x000000FE, RC_MEMSIZE_BIT_0, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000FD, RC_MEMSIZE_BIT_1, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000FB, RC_MEMSIZE_BIT_2, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000F7, RC_MEMSIZE_BIT_3, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000EF, RC_MEMSIZE_BIT_4, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000DF, RC_MEMSIZE_BIT_5, 0x00000000); + TEST_PARAMS3(test_transform, 0x000000BF, RC_MEMSIZE_BIT_6, 0x00000000); + TEST_PARAMS3(test_transform, 0x0000007F, RC_MEMSIZE_BIT_7, 0x00000000); + + TEST_PARAMS3(test_transform_float, 0x3F800000, RC_MEMSIZE_FLOAT, 1.0); + TEST_PARAMS3(test_transform_float, 0x41460000, RC_MEMSIZE_FLOAT, 12.375); + TEST_PARAMS3(test_transform_float, 0x42883EFA, RC_MEMSIZE_FLOAT, 68.123); + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_FLOAT, 0.0); + TEST_PARAMS3(test_transform_float, 0x80000000, RC_MEMSIZE_FLOAT, -0.0); + TEST_PARAMS3(test_transform_float, 0xC0000000, RC_MEMSIZE_FLOAT, -2.0); + TEST_PARAMS3(test_transform_float, 0x40490FDB, RC_MEMSIZE_FLOAT, 3.14159274101257324); + TEST_PARAMS3(test_transform_float, 0x3EAAAAAB, RC_MEMSIZE_FLOAT, 0.333333334326744076); + TEST_PARAMS3(test_transform_float, 0x429A4492, RC_MEMSIZE_FLOAT, 77.133926); + TEST_PARAMS3(test_transform_float, 0x4350370A, RC_MEMSIZE_FLOAT, 208.214996); + TEST_PARAMS3(test_transform_float, 0x45AE36E9, RC_MEMSIZE_FLOAT, 5574.863770); + TEST_PARAMS3(test_transform_float, 0x58635FA9, RC_MEMSIZE_FLOAT, 1000000000000000.0); + TEST_PARAMS3(test_transform_float, 0x24E69595, RC_MEMSIZE_FLOAT, 0.0000000000000001); + TEST_PARAMS3(test_transform_float, 0x000042B4, RC_MEMSIZE_FLOAT, 2.39286e-41); + TEST_PARAMS2(test_transform_float_inf, 0x7F800000, RC_MEMSIZE_FLOAT); + TEST_PARAMS2(test_transform_float_nan, 0x7FFFFFFF, RC_MEMSIZE_FLOAT); + + TEST_PARAMS3(test_transform_double32, 0x3FF00000, RC_MEMSIZE_DOUBLE32, 1.0); + TEST_PARAMS3(test_transform_double32, 0x4028C000, RC_MEMSIZE_DOUBLE32, 12.375); + TEST_PARAMS3(test_transform_double32, 0x405107DF, RC_MEMSIZE_DOUBLE32, 68.123); + TEST_PARAMS3(test_transform_double32, 0x00000000, RC_MEMSIZE_DOUBLE32, 0.0); + TEST_PARAMS3(test_transform_double32, 0x80000000, RC_MEMSIZE_DOUBLE32, -0.0); + TEST_PARAMS3(test_transform_double32, 0xC0000000, RC_MEMSIZE_DOUBLE32, -2.0); + TEST_PARAMS3(test_transform_double32, 0x400921FB, RC_MEMSIZE_DOUBLE32, 3.14159274101257324); + TEST_PARAMS3(test_transform_double32, 0x3FD55555, RC_MEMSIZE_DOUBLE32, 0.333333334326744076); + TEST_PARAMS3(test_transform_double32, 0x40534892, RC_MEMSIZE_DOUBLE32, 77.133926); + TEST_PARAMS3(test_transform_double32, 0x406A06E1, RC_MEMSIZE_DOUBLE32, 208.214996); + TEST_PARAMS3(test_transform_double32, 0x40B5C6DD, RC_MEMSIZE_DOUBLE32, 5574.863770); + TEST_PARAMS3(test_transform_double32, 0x430C6BF5, RC_MEMSIZE_DOUBLE32, 1000000000000000.0); + TEST_PARAMS3(test_transform_double32, 0x3C9CD2B2, RC_MEMSIZE_DOUBLE32, 0.0000000000000001); + TEST_PARAMS3(test_transform_double32, 0x3780AD01, RC_MEMSIZE_DOUBLE32, 2.39286e-41); + TEST_PARAMS3(test_transform_double32, 0x3FF3C0CA, RC_MEMSIZE_DOUBLE32, 1.234568); + TEST_PARAMS2(test_transform_float_inf, 0x7FF00000, RC_MEMSIZE_DOUBLE32); + TEST_PARAMS2(test_transform_float_nan, 0x7FFFFFFF, RC_MEMSIZE_DOUBLE32); + + TEST_PARAMS3(test_transform_double32, 0x000000C0, RC_MEMSIZE_DOUBLE32_BE, -2.0); + TEST_PARAMS3(test_transform_double32, 0x00003840, RC_MEMSIZE_DOUBLE32_BE, 24.0); + TEST_PARAMS3(test_transform_double32, 0xCAC0F33F, RC_MEMSIZE_DOUBLE32_BE, 1.234568); + TEST_PARAMS3(test_transform_double32, 0xFB210940, RC_MEMSIZE_DOUBLE32_BE, 3.14159274101257324); + + TEST_PARAMS3(test_transform_float, 0x0000803F, RC_MEMSIZE_FLOAT_BE, 1.0); + TEST_PARAMS3(test_transform_float, 0x00004641, RC_MEMSIZE_FLOAT_BE, 12.375); + TEST_PARAMS3(test_transform_float, 0xFA3E8842, RC_MEMSIZE_FLOAT_BE, 68.123); + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_FLOAT_BE, 0.0); + TEST_PARAMS3(test_transform_float, 0x00000080, RC_MEMSIZE_FLOAT_BE, -0.0); + TEST_PARAMS3(test_transform_float, 0x000000C0, RC_MEMSIZE_FLOAT_BE, -2.0); + TEST_PARAMS3(test_transform_float, 0xDB0F4940, RC_MEMSIZE_FLOAT_BE, 3.14159274101257324); + TEST_PARAMS3(test_transform_float, 0xABAAAA3E, RC_MEMSIZE_FLOAT_BE, 0.333333334326744076); + TEST_PARAMS3(test_transform_float, 0x92449A42, RC_MEMSIZE_FLOAT_BE, 77.133926); + TEST_PARAMS3(test_transform_float, 0x0A375043, RC_MEMSIZE_FLOAT_BE, 208.214996); + TEST_PARAMS3(test_transform_float, 0xE936AE45, RC_MEMSIZE_FLOAT_BE, 5574.863770); + TEST_PARAMS3(test_transform_float, 0xA95F6358, RC_MEMSIZE_FLOAT_BE, 1000000000000000.0); + TEST_PARAMS3(test_transform_float, 0x9595E624, RC_MEMSIZE_FLOAT_BE, 0.0000000000000001); + TEST_PARAMS3(test_transform_float, 0xB4420000, RC_MEMSIZE_FLOAT_BE, 2.39286e-41); + TEST_PARAMS2(test_transform_float_inf, 0x0000807F, RC_MEMSIZE_FLOAT_BE); + TEST_PARAMS2(test_transform_float_nan, 0xFFFFFF7F, RC_MEMSIZE_FLOAT_BE); + + /* MBF values are stored big endian (at least on Apple II), so will be byteswapped + * when passed to rc_transform_memref_value. MBF doesn't support infinity or NaN. */ + TEST_PARAMS3(test_transform_float, 0x00000081, RC_MEMSIZE_MBF32, 1.0); /* 81 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00002084, RC_MEMSIZE_MBF32, 10.0); /* 84 20 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00004687, RC_MEMSIZE_MBF32, 99.0); /* 87 46 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_MBF32, 0.0); /* 00 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00000080, RC_MEMSIZE_MBF32, 0.5); /* 80 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x00008082, RC_MEMSIZE_MBF32, -2.0); /* 82 80 00 00 */ + TEST_PARAMS3(test_transform_float, 0xF3043581, RC_MEMSIZE_MBF32, 1.41421354); /* 81 34 04 F3 */ + TEST_PARAMS3(test_transform_float, 0xDA0F4982, RC_MEMSIZE_MBF32, 3.14159256); /* 82 49 0F DA */ + TEST_PARAMS3(test_transform_float, 0xDB0F4983, RC_MEMSIZE_MBF32, 6.28318548); /* 83 49 0F DB */ + + /* Some flavors of BASIC (notably Locomotive BASIC on the Amstrad CPC) use the native endian-ness of + * the system for their MBF values, so we support both MBF32 (big endian) and MBF32_LE (little endian). + * Also note that Amstrad BASIC and Apple II BASIC both use MBF40, but since MBF40 just adds 8 extra bits + * of significance as the end of the MBF32 value, we can discard those as we convert to a 32-bit float. */ + TEST_PARAMS3(test_transform_float, 0x81000000, RC_MEMSIZE_MBF32_LE, 1.0); /* 00 00 00 81 */ + TEST_PARAMS3(test_transform_float, 0x84200000, RC_MEMSIZE_MBF32_LE, 10.0); /* 00 00 20 84 */ + TEST_PARAMS3(test_transform_float, 0x87460000, RC_MEMSIZE_MBF32_LE, 99.0); /* 00 00 46 87 */ + TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_MBF32_LE, 0.0); /* 00 00 00 00 */ + TEST_PARAMS3(test_transform_float, 0x80000000, RC_MEMSIZE_MBF32_LE, 0.5); /* 00 00 00 80 */ + TEST_PARAMS3(test_transform_float, 0x82800000, RC_MEMSIZE_MBF32_LE, -2.0); /* 00 00 80 82 */ + TEST_PARAMS3(test_transform_float, 0x813504F3, RC_MEMSIZE_MBF32_LE, 1.41421354); /* F3 04 34 81 */ + TEST_PARAMS3(test_transform_float, 0x82490FDA, RC_MEMSIZE_MBF32_LE, 3.14159256); /* DA 0F 49 82 */ + TEST_PARAMS3(test_transform_float, 0x83490FDB, RC_MEMSIZE_MBF32_LE, 6.28318548); /* DB 0F 49 83 */ +} + +static int get_memref_count(rc_parse_state_t* parse) { + return rc_memrefs_count_memrefs(parse->memrefs) + rc_memrefs_count_modified_memrefs(parse->memrefs); +} + +static void test_allocate_shared_address() { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 1); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* differing size will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 2); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW); /* differing size will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 3); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* differing size will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 4); + + rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* differing address will not match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* match */ + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + rc_destroy_parse_state(&parse); +} + +static void test_allocate_shared_address2() { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_memref_t* memref1; + rc_memref_t* memref2; + rc_memref_t* memref3; + rc_memref_t* memref4; + rc_memref_t* memref5; + rc_memref_t* memrefX; + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + memref1 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(memref1->address, 1); + ASSERT_NUM_EQUALS(memref1->value.size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(memref1->value.memref_type, RC_MEMREF_TYPE_MEMREF); + ASSERT_NUM_EQUALS(memref1->value.type, RC_VALUE_TYPE_UNSIGNED); + ASSERT_NUM_EQUALS(memref1->value.value, 0); + ASSERT_NUM_EQUALS(memref1->value.changed, 0); + ASSERT_NUM_EQUALS(memref1->value.prior, 0); + + memref2 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* differing size will not match */ + memref3 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW); /* differing size will not match */ + memref4 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* differing size will not match */ + memref5 = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* differing address will not match */ + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref1); + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref2); + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref3); + + memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref4); + + memrefX = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* match */ + ASSERT_PTR_EQUALS(memrefX, memref5); + + rc_destroy_parse_state(&parse); +} + +static void test_allocate_shared_indirect_address() { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_memref_t* parent_memref1, *parent_memref2; + rc_operand_t parent1, parent2, delta1, intermediate2; + rc_modified_memref_t* child1, *child2, *child3, *child4; + rc_operand_t offset0, offset4; + rc_operand_set_const(&offset0, 0); + rc_operand_set_const(&offset4, 4); + + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + parent1.value.memref = parent_memref1 = rc_alloc_memref(&parse, 88, RC_MEMSIZE_16_BITS); + parent1.type = RC_OPERAND_ADDRESS; + parent1.size = RC_MEMSIZE_16_BITS; + parent2.value.memref = parent_memref2 = rc_alloc_memref(&parse, 99, RC_MEMSIZE_16_BITS); + parent2.type = RC_OPERAND_ADDRESS; + parent2.size = RC_MEMSIZE_16_BITS; + delta1.value.memref = parent_memref1; + delta1.type = RC_OPERAND_DELTA; + ASSERT_NUM_EQUALS(get_memref_count(&parse), 2); + + child1 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 3); + + /* differing size will not match */ + child2 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_16_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 4); + + /* differing parent will not match */ + child3 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent2, RC_OPERATOR_INDIRECT_READ, &offset0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); + + /* differing parent type will not match */ + child4 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &delta1, RC_OPERATOR_INDIRECT_READ, &offset0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 6); + + /* differing offset will not match */ + child4 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset4); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); + + /* exact match to first */ + ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset0), child1); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); + + /* exact match to differing parent */ + ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent2, RC_OPERATOR_INDIRECT_READ, &offset0), child3); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); + + /* exact match to differing offset */ + ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset4), child4); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); + + /* intermediate parent */ + intermediate2.value.memref = &child2->memref; + intermediate2.type = RC_OPERAND_ADDRESS; + intermediate2.size = RC_MEMSIZE_32_BITS; + intermediate2.memref_access_type = RC_OPERAND_ADDRESS; + child4 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &intermediate2, RC_OPERATOR_INDIRECT_READ, &offset0); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 8); + + ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &intermediate2, RC_OPERATOR_INDIRECT_READ, &offset0), child4); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 8); + + rc_destroy_parse_state(&parse); +} + +static void test_sizing_mode_grow_buffer() { + int i; + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + /* memrefs are allocated 16 at a time */ + for (i = 0; i < 100; i++) { + rc_alloc_memref(&parse, i, RC_MEMSIZE_8_BITS); + } + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + /* 100 have been allocated, make sure we can still access items at various addresses without allocating more */ + rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 25, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 50, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 75, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_alloc_memref(&parse, 99, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); + + rc_destroy_parse_state(&parse); +} + +static void test_update_memref_values() { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_memref_t* memref1; + rc_memref_t* memref2; + + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + memory.ram = ram; + memory.size = sizeof(ram); + + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + memref1 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); + memref2 = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); + + rc_update_memref_values(&memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 0x12); + ASSERT_NUM_EQUALS(memref1->value.changed, 1); + ASSERT_NUM_EQUALS(memref1->value.prior, 0); + ASSERT_NUM_EQUALS(memref2->value.value, 0x34); + ASSERT_NUM_EQUALS(memref2->value.changed, 1); + ASSERT_NUM_EQUALS(memref2->value.prior, 0); + + ram[1] = 3; + rc_update_memref_values(&memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 3); + ASSERT_NUM_EQUALS(memref1->value.changed, 1); + ASSERT_NUM_EQUALS(memref1->value.prior, 0x12); + ASSERT_NUM_EQUALS(memref2->value.value, 0x34); + ASSERT_NUM_EQUALS(memref2->value.changed, 0); + ASSERT_NUM_EQUALS(memref2->value.prior, 0); + + ram[1] = 5; + rc_update_memref_values(&memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 5); + ASSERT_NUM_EQUALS(memref1->value.changed, 1); + ASSERT_NUM_EQUALS(memref1->value.prior, 3); + ASSERT_NUM_EQUALS(memref2->value.value, 0x34); + ASSERT_NUM_EQUALS(memref2->value.changed, 0); + ASSERT_NUM_EQUALS(memref2->value.prior, 0); + + ram[2] = 7; + rc_update_memref_values(&memrefs, peek, &memory); + + ASSERT_NUM_EQUALS(memref1->value.value, 5); + ASSERT_NUM_EQUALS(memref1->value.changed, 0); + ASSERT_NUM_EQUALS(memref1->value.prior, 3); + ASSERT_NUM_EQUALS(memref2->value.value, 7); + ASSERT_NUM_EQUALS(memref2->value.changed, 1); + ASSERT_NUM_EQUALS(memref2->value.prior, 0x34); + + rc_destroy_parse_state(&parse); +} + +void test_memref(void) { + TEST_SUITE_BEGIN(); + + test_shared_masks(); + test_shared_sizes(); + test_transforms(); + + TEST(test_allocate_shared_address); + TEST(test_allocate_shared_address2); + TEST(test_allocate_shared_indirect_address); + + TEST(test_sizing_mode_grow_buffer); + TEST(test_update_memref_values); + + /* rc_parse_memref is thoroughly tested by rc_parse_operand tests */ + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_operand.c b/src/rcheevos/test/rcheevos/test_operand.c new file mode 100644 index 0000000000..fbf7c42cbc --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_operand.c @@ -0,0 +1,692 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_parse_operand(rc_operand_t* self, char* buffer, const char** memaddr) { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + int ret; + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + ret = rc_parse_operand(self, memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_GREATER_EQUALS(ret, 0); + ASSERT_NUM_EQUALS(**memaddr, 0); +} +#define assert_parse_operand(operand, buffer, memaddr_out) ASSERT_HELPER(_assert_parse_operand(operand, buffer, memaddr_out), "assert_parse_operand") + +static void _assert_operand(rc_operand_t* self, uint8_t expected_type, uint8_t expected_size, uint32_t expected_address) { + ASSERT_NUM_EQUALS(expected_type, self->type); + switch (expected_type) { + case RC_OPERAND_ADDRESS: + case RC_OPERAND_DELTA: + case RC_OPERAND_PRIOR: + ASSERT_NUM_EQUALS(expected_size, self->size); + ASSERT_UNUM_EQUALS(expected_address, self->value.memref->address); + break; + + case RC_OPERAND_CONST: + ASSERT_UNUM_EQUALS(expected_address, self->value.num); + break; + } +} +#define assert_operand(operand, expected_type, expected_size, expected_address) ASSERT_HELPER(_assert_operand(operand, expected_type, expected_size, expected_address), "assert_operand") + +static void test_parse_operand(const char* memaddr, uint8_t expected_type, uint8_t expected_size, uint32_t expected_value) { + char buffer[256]; + rc_operand_t self; + assert_parse_operand(&self, buffer, &memaddr); + assert_operand(&self, expected_type, expected_size, expected_value); +} + +static void test_parse_operand_fp(const char* memaddr, uint8_t expected_type, double expected_value) { + char buffer[256]; + rc_operand_t self; + assert_parse_operand(&self, buffer, &memaddr); + + ASSERT_NUM_EQUALS(expected_type, self.type); + switch (expected_type) { + case RC_OPERAND_CONST: + ASSERT_DBL_EQUALS(expected_value, self.value.num); + break; + case RC_OPERAND_FP: + ASSERT_DBL_EQUALS(expected_value, self.value.dbl); + break; + } +} + +static void test_parse_error_operand(const char* memaddr, int valid_chars, int expected_error) { + rc_operand_t self; + rc_parse_state_t parse; + int ret; + const char* begin = memaddr; + rc_memrefs_t memrefs; + + rc_init_parse_state(&parse, 0); + rc_init_parse_state_memrefs(&parse, &memrefs); + ret = rc_parse_operand(&self, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_EQUALS(expected_error, ret); + ASSERT_NUM_EQUALS(memaddr - begin, valid_chars); +} + +static uint32_t evaluate_operand(rc_operand_t* op, memory_t* memory, rc_memrefs_t* memrefs) +{ + rc_eval_state_t eval_state; + rc_typed_value_t value; + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + rc_update_memref_values(memrefs, peek, memory); + rc_evaluate_operand(&value, op, &eval_state); + return value.value.u32; +} + +static void test_evaluate_operand(const char* memaddr, memory_t* memory, uint32_t expected_value) { + rc_operand_t self; + rc_parse_state_t parse; + rc_memrefs_t memrefs; + char buffer[512]; + uint32_t value; + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&self, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + value = evaluate_operand(&self, memory, &memrefs); + ASSERT_NUM_EQUALS(value, expected_value); +} + +static float evaluate_operand_float(rc_operand_t* op, memory_t* memory, rc_memrefs_t* memrefs) { + rc_eval_state_t eval_state; + rc_typed_value_t value; + + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = memory; + + rc_update_memref_values(memrefs, peek, memory); + rc_evaluate_operand(&value, op, &eval_state); + return value.value.f32; +} + +static void test_evaluate_operand_float(const char* memaddr, memory_t* memory, double expected_value) { + rc_operand_t self; + rc_parse_state_t parse; + rc_memrefs_t memrefs; + char buffer[512]; + float value; + + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&self, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + value = evaluate_operand_float(&self, memory, &memrefs); + ASSERT_FLOAT_EQUALS(value, expected_value); +} +static void test_parse_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "0xH1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xH1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0x 1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0x1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xW1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xX1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xL1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xU1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xM1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xN1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xO1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xP1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xQ1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xR1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xS1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xT1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xK1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xI1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xJ1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xG1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fF1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fB1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fM1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fL1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* sizes (ignore case) */ + TEST_PARAMS4(test_parse_operand, "0Xh1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xx1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xl1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xu1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xm1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xn1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xo1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xp1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xq1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xr1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xs1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xt1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xk1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xi1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xj1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "0xg1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "ff1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fb1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fm1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "fl1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* addresses */ + TEST_PARAMS4(test_parse_operand, "0xH0000", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x0000U); + TEST_PARAMS4(test_parse_operand, "0xH12345678", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x12345678U); + TEST_PARAMS4(test_parse_operand, "0xHABCD", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "0xhabcd", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "fFABCD", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0xABCDU); + + /* doubled up prefix */ + TEST_PARAMS3(test_parse_error_operand, "0x0xH1234", 0, RC_INVALID_MEMORY_OPERAND); +} + +static void test_parse_delta_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "d0xH1234", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0x 1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0x1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xW1234", RC_OPERAND_DELTA, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xX1234", RC_OPERAND_DELTA, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xL1234", RC_OPERAND_DELTA, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xU1234", RC_OPERAND_DELTA, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xM1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xN1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xO1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xP1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xQ1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xR1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xS1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xT1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xK1234", RC_OPERAND_DELTA, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xI1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xJ1234", RC_OPERAND_DELTA, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "d0xG1234", RC_OPERAND_DELTA, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfF1234", RC_OPERAND_DELTA, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfB1234", RC_OPERAND_DELTA, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfM1234", RC_OPERAND_DELTA, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "dfL1234", RC_OPERAND_DELTA, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* ignores case */ + TEST_PARAMS4(test_parse_operand, "D0Xh1234", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x1234U); + + /* addresses */ + TEST_PARAMS4(test_parse_operand, "d0xH0000", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x0000U); + TEST_PARAMS4(test_parse_operand, "d0xH12345678", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x12345678U); + TEST_PARAMS4(test_parse_operand, "d0xHABCD", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "d0xhabcd", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0xABCDU); +} + +static void test_parse_prior_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "p0xH1234", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0x 1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0x1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xW1234", RC_OPERAND_PRIOR, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xX1234", RC_OPERAND_PRIOR, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xL1234", RC_OPERAND_PRIOR, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xU1234", RC_OPERAND_PRIOR, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xM1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xN1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xO1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xP1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xQ1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xR1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xS1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xT1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_7, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xK1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BITCOUNT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xI1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xJ1234", RC_OPERAND_PRIOR, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "p0xG1234", RC_OPERAND_PRIOR, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfF1234", RC_OPERAND_PRIOR, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfB1234", RC_OPERAND_PRIOR, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfM1234", RC_OPERAND_PRIOR, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "pfL1234", RC_OPERAND_PRIOR, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* ignores case */ + TEST_PARAMS4(test_parse_operand, "P0Xh1234", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x1234U); + + /* addresses */ + TEST_PARAMS4(test_parse_operand, "p0xH0000", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x0000U); + TEST_PARAMS4(test_parse_operand, "p0xH12345678", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x12345678U); + TEST_PARAMS4(test_parse_operand, "p0xHABCD", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "p0xhabcd", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0xABCDU); +} + +static void test_parse_bcd_memory_references() { + /* sizes */ + TEST_PARAMS4(test_parse_operand, "b0xH1234", RC_OPERAND_BCD, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0x 1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0x1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xW1234", RC_OPERAND_BCD, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xX1234", RC_OPERAND_BCD, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xI1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xJ1234", RC_OPERAND_BCD, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xG1234", RC_OPERAND_BCD, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfF1234", RC_OPERAND_BCD, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfB1234", RC_OPERAND_BCD, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfM1234", RC_OPERAND_BCD, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "bfL1234", RC_OPERAND_BCD, RC_MEMSIZE_MBF32_LE, 0x1234U); + + /* sizes less than 8-bit technically don't need a BCD conversion */ + TEST_PARAMS4(test_parse_operand, "b0xL1234", RC_OPERAND_BCD, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xU1234", RC_OPERAND_BCD, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xM1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xN1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xO1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xP1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xQ1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xR1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xS1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "b0xT1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_7, 0x1234U); +} + +static void test_parse_inverted_memory_references() { + TEST_PARAMS4(test_parse_operand, "~0xH1234", RC_OPERAND_INVERTED, RC_MEMSIZE_8_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0x 1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0x1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xW1234", RC_OPERAND_INVERTED, RC_MEMSIZE_24_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xX1234", RC_OPERAND_INVERTED, RC_MEMSIZE_32_BITS, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xI1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xJ1234", RC_OPERAND_INVERTED, RC_MEMSIZE_24_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xG1234", RC_OPERAND_INVERTED, RC_MEMSIZE_32_BITS_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fF1234", RC_OPERAND_INVERTED, RC_MEMSIZE_FLOAT, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fB1234", RC_OPERAND_INVERTED, RC_MEMSIZE_FLOAT_BE, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fM1234", RC_OPERAND_INVERTED, RC_MEMSIZE_MBF32, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~fL1234", RC_OPERAND_INVERTED, RC_MEMSIZE_MBF32_LE, 0x1234U); + + TEST_PARAMS4(test_parse_operand, "~0xL1234", RC_OPERAND_INVERTED, RC_MEMSIZE_LOW, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xU1234", RC_OPERAND_INVERTED, RC_MEMSIZE_HIGH, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xM1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_0, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xN1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_1, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xO1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_2, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xP1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_3, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xQ1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_4, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xR1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_5, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xS1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_6, 0x1234U); + TEST_PARAMS4(test_parse_operand, "~0xT1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_7, 0x1234U); +} + +static void test_parse_unsigned_values() { + /* unsigned integers - no prefix */ + /* values don't actually have size, default is RC_MEMSIZE_8_BITS */ + TEST_PARAMS4(test_parse_operand, "123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123U); + TEST_PARAMS4(test_parse_operand, "123456", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123456U); + TEST_PARAMS4(test_parse_operand, "0", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U); + TEST_PARAMS4(test_parse_operand, "0000000000", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U); + TEST_PARAMS4(test_parse_operand, "0123456", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123456U); + TEST_PARAMS4(test_parse_operand, "4294967295", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); + + /* more than 32-bits (error), will be constrained to 32-bits */ + TEST_PARAMS4(test_parse_operand, "4294967296", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); +} + +static void test_parse_signed_values() { + /* signed integers - 'V' prefix */ + TEST_PARAMS4(test_parse_operand, "v100", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 100); + TEST_PARAMS4(test_parse_operand, "V100", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 100); + TEST_PARAMS4(test_parse_operand, "V+1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1); + TEST_PARAMS4(test_parse_operand, "V-1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xFFFFFFFFU); + TEST_PARAMS4(test_parse_operand, "V-2", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xFFFFFFFEU); + TEST_PARAMS4(test_parse_operand, "V9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x7FFFFFFFU); + TEST_PARAMS4(test_parse_operand, "V-9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x80000001U); + + /* no prefix, sign */ + TEST_PARAMS4(test_parse_operand, "-1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); + TEST_PARAMS4(test_parse_operand, "+1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1); + TEST_PARAMS4(test_parse_operand, "+9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x7FFFFFFFU); + TEST_PARAMS4(test_parse_operand, "-9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x80000001U); + + /* prefix, no value */ + TEST_PARAMS3(test_parse_error_operand, "v", 0, RC_INVALID_CONST_OPERAND); + + /* signed integer prefix, hex value */ + TEST_PARAMS3(test_parse_error_operand, "vabcd", 0, RC_INVALID_CONST_OPERAND); +} + +static void test_parse_hex_values() { + /* hex - 'H' prefix, not '0x'! */ + TEST_PARAMS4(test_parse_operand, "H123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x123U); + TEST_PARAMS4(test_parse_operand, "HABCD", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "h123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x123U); + TEST_PARAMS4(test_parse_operand, "habcd", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xABCDU); + TEST_PARAMS4(test_parse_operand, "HFFFFFFFF", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); + + /* hex without prefix */ + TEST_PARAMS3(test_parse_error_operand, "ABCD", 0, RC_INVALID_MEMORY_OPERAND); + + /* '0x' is an address */ + TEST_PARAMS4(test_parse_operand, "0x123", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x123U); +} + +static void test_parse_float_values() { + /* floating point - 'F' prefix */ + TEST_PARAMS3(test_parse_operand_fp, "f0.5", RC_OPERAND_FP, 0.5); + TEST_PARAMS3(test_parse_operand_fp, "F0.5", RC_OPERAND_FP, 0.5); + TEST_PARAMS3(test_parse_operand_fp, "f+0.5", RC_OPERAND_FP, 0.5); + TEST_PARAMS3(test_parse_operand_fp, "f-0.5", RC_OPERAND_FP, -0.5); + TEST_PARAMS3(test_parse_operand_fp, "f1.0", RC_OPERAND_FP, 1.0); + TEST_PARAMS3(test_parse_operand_fp, "f1.000000", RC_OPERAND_FP, 1.0); + TEST_PARAMS3(test_parse_operand_fp, "f1.000001", RC_OPERAND_FP, 1.000001); + TEST_PARAMS3(test_parse_operand_fp, "f1", RC_OPERAND_CONST, 1.0); + TEST_PARAMS3(test_parse_operand_fp, "f0.666666", RC_OPERAND_FP, 0.666666); + TEST_PARAMS3(test_parse_operand_fp, "f0.001", RC_OPERAND_FP, 0.001); + TEST_PARAMS3(test_parse_operand_fp, "f0.100", RC_OPERAND_FP, 0.1); + TEST_PARAMS3(test_parse_operand_fp, "f.12345", RC_OPERAND_FP, 0.12345); + + /* prefix, no value */ + TEST_PARAMS3(test_parse_error_operand, "f", 0, RC_INVALID_FP_OPERAND); + + /* float prefix, hex value */ + TEST_PARAMS3(test_parse_error_operand, "fabcd", 0, RC_INVALID_FP_OPERAND); + + /* float prefix, hex value, no period, parser will stop after valid numbers */ + TEST_PARAMS3(test_parse_error_operand, "f1d", 2, RC_OK); + + /* non-numeric decimal part */ + TEST_PARAMS3(test_parse_error_operand, "f1.d", 0, RC_INVALID_FP_OPERAND); + TEST_PARAMS3(test_parse_error_operand, "f1..0", 0, RC_INVALID_FP_OPERAND); + + /* non-C locale - parser will stop at comma */ + TEST_PARAMS3(test_parse_error_operand, "f1,23", 2, RC_OK); + TEST_PARAMS3(test_parse_error_operand, "f-1,23", 3, RC_OK); + + /* no prefix - parser will stop at period */ + TEST_PARAMS3(test_parse_error_operand, "0.5", 1, RC_OK); + TEST_PARAMS3(test_parse_error_operand, "-0.5", 2, RC_OK); +} + +static void test_evaluate_memory_references() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + memory.ram = ram; + memory.size = sizeof(ram); + + /* value */ + TEST_PARAMS3(test_evaluate_operand, "0", &memory, 0x00U); + + /* eight-bit */ + TEST_PARAMS3(test_evaluate_operand, "0xh0", &memory, 0x00U); + TEST_PARAMS3(test_evaluate_operand, "0xh1", &memory, 0x12U); + TEST_PARAMS3(test_evaluate_operand, "0xh4", &memory, 0x56U); + TEST_PARAMS3(test_evaluate_operand, "0xh5", &memory, 0x00U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xh4", &memory, 0xA9U); + + /* sixteen-bit */ + TEST_PARAMS3(test_evaluate_operand, "0x 0", &memory, 0x1200U); + TEST_PARAMS3(test_evaluate_operand, "0x 3", &memory, 0x56ABU); + TEST_PARAMS3(test_evaluate_operand, "0x 4", &memory, 0x0056U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0x 3", &memory, 0xA954U); + + /* twenty-four-bit */ + TEST_PARAMS3(test_evaluate_operand, "0xw0", &memory, 0x341200U); + TEST_PARAMS3(test_evaluate_operand, "0xw2", &memory, 0x56AB34U); + TEST_PARAMS3(test_evaluate_operand, "0xw3", &memory, 0x0056ABU); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xw2", &memory, 0xA954CBU); + + /* thirty-two-bit */ + TEST_PARAMS3(test_evaluate_operand, "0xx0", &memory, 0xAB341200U); + TEST_PARAMS3(test_evaluate_operand, "0xx1", &memory, 0x56AB3412U); + TEST_PARAMS3(test_evaluate_operand, "0xx3", &memory, 0x000056ABU); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xx1", &memory, 0xA954CBEDU); + TEST_PARAMS3(test_evaluate_operand, "~0xx3", &memory, 0xFFFFA954U); /* out of range */ + + /* sixteen-bit big endian*/ + TEST_PARAMS3(test_evaluate_operand, "0xi0", &memory, 0x0012U); + TEST_PARAMS3(test_evaluate_operand, "0xi3", &memory, 0xAB56U); + TEST_PARAMS3(test_evaluate_operand, "0xi4", &memory, 0x5600U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xi3", &memory, 0x54A9U); + + /* twenty-four-bit big endian */ + TEST_PARAMS3(test_evaluate_operand, "0xj0", &memory, 0x001234U); + TEST_PARAMS3(test_evaluate_operand, "0xj1", &memory, 0x1234ABU); + TEST_PARAMS3(test_evaluate_operand, "0xj3", &memory, 0xAB5600U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xj1", &memory, 0xEDCB54U); + TEST_PARAMS3(test_evaluate_operand, "~0xj3", &memory, 0x54A9FFU); /* out of range */ + + /* thirty-two-bit big endian */ + TEST_PARAMS3(test_evaluate_operand, "0xg0", &memory, 0x001234ABU); + TEST_PARAMS3(test_evaluate_operand, "0xg1", &memory, 0x1234AB56U); + TEST_PARAMS3(test_evaluate_operand, "0xg3", &memory, 0xAB560000U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xg1", &memory, 0xEDCB54A9U); + TEST_PARAMS3(test_evaluate_operand, "~0xg3", &memory, 0x54A9FFFFU); /* out of range */ + + /* nibbles */ + TEST_PARAMS3(test_evaluate_operand, "0xu0", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xu1", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xu4", &memory, 0x5U); + TEST_PARAMS3(test_evaluate_operand, "0xu5", &memory, 0x0U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xu4", &memory, 0xAU); + + TEST_PARAMS3(test_evaluate_operand, "0xl0", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xl1", &memory, 0x2U); + TEST_PARAMS3(test_evaluate_operand, "0xl4", &memory, 0x6U); + TEST_PARAMS3(test_evaluate_operand, "0xl5", &memory, 0x0U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xl4", &memory, 0x9U); + + /* bits */ + TEST_PARAMS3(test_evaluate_operand, "0xm0", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xm3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xn3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xo3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xp3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xq3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xr3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xs3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "0xt3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "0xm5", &memory, 0x0U); /* out of range */ + TEST_PARAMS3(test_evaluate_operand, "~0xm0", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xm3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xn3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xo3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xp3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xq3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xr3", &memory, 0x0U); + TEST_PARAMS3(test_evaluate_operand, "~0xs3", &memory, 0x1U); + TEST_PARAMS3(test_evaluate_operand, "~0xt3", &memory, 0x0U); + + /* bit count */ + TEST_PARAMS3(test_evaluate_operand, "0xk00", &memory, 0U); /* 0 bits in 0x00 */ + TEST_PARAMS3(test_evaluate_operand, "0xk01", &memory, 2U); /* 2 bits in 0x12 */ + TEST_PARAMS3(test_evaluate_operand, "0xk02", &memory, 3U); /* 3 bits in 0x34 */ + TEST_PARAMS3(test_evaluate_operand, "0xk03", &memory, 5U); /* 5 bits in 0xAB */ + TEST_PARAMS3(test_evaluate_operand, "0xk04", &memory, 4U); /* 4 bits in 0x56 */ + + /* BCD */ + TEST_PARAMS3(test_evaluate_operand, "b0xh3", &memory, 111U); /* 0xAB not technically valid in BCD */ + + ram[3] = 0x56; /* 0xAB not valid in BCD */ + ram[4] = 0x78; + TEST_PARAMS3(test_evaluate_operand, "b0xh0", &memory, 00U); + TEST_PARAMS3(test_evaluate_operand, "b0xh1", &memory, 12U); + TEST_PARAMS3(test_evaluate_operand, "b0x 1", &memory, 3412U); + TEST_PARAMS3(test_evaluate_operand, "b0xw1", &memory, 563412U); + TEST_PARAMS3(test_evaluate_operand, "b0xx1", &memory, 78563412U); + TEST_PARAMS3(test_evaluate_operand, "b0xi1", &memory, 1234U); + TEST_PARAMS3(test_evaluate_operand, "b0xj1", &memory, 123456U); + TEST_PARAMS3(test_evaluate_operand, "b0xg1", &memory, 12345678U); +} + +static void test_evaluate_delta_memory_reference() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_operand_t op; + const char* memaddr; + rc_parse_state_t parse; + char buffer[256]; + rc_memrefs_t memrefs; + + memory.ram = ram; + memory.size = sizeof(ram); + + memaddr = "d0xh1"; + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&op, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x00); /* first call gets uninitialized value */ + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12); /* second gets current value */ + + /* RC_OPERAND_DELTA is always one frame behind */ + ram[1] = 0x13; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); + + ram[1] = 0x14; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x13U); + + ram[1] = 0x15; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x14U); + + ram[1] = 0x16; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); + + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x16U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x16U); +} + +void test_evaluate_prior_memory_reference() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_operand_t op; + const char* memaddr; + rc_parse_state_t parse; + char buffer[256]; + rc_memrefs_t memrefs; + + memory.ram = ram; + memory.size = sizeof(ram); + + memaddr = "p0xh1"; + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&op, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + /* RC_OPERAND_PRIOR only updates when the memory value changes */ + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x00); /* first call gets uninitialized value */ + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x00); /* value only changes when memory changes */ + + ram[1] = 0x13; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); + + ram[1] = 0x14; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x13U); + + ram[1] = 0x15; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x14U); + + ram[1] = 0x16; + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); + ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); +} + +static void test_evaluate_memory_references_float() { + uint8_t ram[] = {0x00, 0x00, 0x80, 0x3F, 0x81, 0x00, 0x00, 0x00, 0x00, 0x81}; + memory_t memory; + memory.ram = ram; + memory.size = sizeof(ram); + + TEST_PARAMS3(test_evaluate_operand_float, "fF0", &memory, 1.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fM4", &memory, 1.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fL6", &memory, 1.0); /* MBF32_LE float */ + + /* BCD and inversion are not supported for floats - behaves as if the prefix wasn't present */ + TEST_PARAMS3(test_evaluate_operand_float, "bfF0", &memory, 1.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfM4", &memory, 1.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfL6", &memory, 1.0); /* MBF32_LE float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fF0", &memory, 1.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fM4", &memory, 1.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fL6", &memory, 1.0); /* MBF32_LE float */ + + ram[2] = 0x00; ram[3] = 0x40; /* set IEE754 float to 2.0 */ + ram[4] = 0x83; ram[5] = 0x40; /* set MBF32 float to 6.0 */ + ram[9] = 0x83; ram[8] = 0x00; /* set MBF32_LE float to 4.0 */ + + TEST_PARAMS3(test_evaluate_operand_float, "fF0", &memory, 2.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fM4", &memory, 6.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "fL6", &memory, 4.0); /* MBF32_LE float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfF0", &memory, 2.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfM4", &memory, 6.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "bfL6", &memory, 4.0); /* MBF32_LE float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fF0", &memory, 2.0); /* IEE754 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fM4", &memory, 6.0); /* MBF32 float */ + TEST_PARAMS3(test_evaluate_operand_float, "~fL6", &memory, 4.0); /* MBF32_LE float */ +} + +static void test_evaluate_delta_memory_reference_float() { + uint8_t ram[] = {0x00, 0x00, 0x80, 0x3F}; + memory_t memory; + rc_operand_t op; + const char* memaddr; + rc_parse_state_t parse; + char buffer[256]; + rc_memrefs_t memrefs; + + memory.ram = ram; + memory.size = sizeof(ram); + + memaddr = "dff0"; + rc_init_parse_state(&parse, buffer); + rc_init_parse_state_memrefs(&parse, &memrefs); + rc_parse_operand(&op, &memaddr, &parse); + rc_destroy_parse_state(&parse); + + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 0.0); /* first call gets uninitialized value */ + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 1.0); /* second gets current value */ + + /* RC_OPERAND_DELTA is always one frame behind */ + ram[3] = 0x40; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 1.0); + + ram[3] = 0x41; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 4.0); + + ram[3] = 0x42; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 16.0); + + ram[3] = 0x43; + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 64.0); + + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 256.0); + ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 256.0); +} + +void test_operand(void) { + TEST_SUITE_BEGIN(); + + test_parse_memory_references(); + test_parse_delta_memory_references(); + test_parse_prior_memory_references(); + test_parse_bcd_memory_references(); + test_parse_inverted_memory_references(); + + test_parse_unsigned_values(); + test_parse_signed_values(); + test_parse_hex_values(); + test_parse_float_values(); + + test_evaluate_memory_references(); + TEST(test_evaluate_delta_memory_reference); + TEST(test_evaluate_prior_memory_reference); + + test_evaluate_memory_references_float(); + TEST(test_evaluate_delta_memory_reference_float); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_rc_validate.c b/src/rcheevos/test/rcheevos/test_rc_validate.c new file mode 100644 index 0000000000..53101749a7 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_rc_validate.c @@ -0,0 +1,500 @@ +#include "rc_validate.h" + +#include "rc_consoles.h" + +#include "../rc_compat.h" +#include "../test_framework.h" + +#include +#include + +int validate_trigger(const char* trigger, char result[], const size_t result_size, uint32_t max_address) { + char* buffer; + rc_trigger_t* compiled; + int success = 0; + + int ret = rc_trigger_size(trigger); + if (ret < 0) { + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_trigger(buffer, trigger, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(uint32_t*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else if (rc_validate_trigger(compiled, result, result_size, max_address)) { + success = 1; + } + + free(buffer); + return success; +} + +static void test_validate_trigger_max_address(const char* trigger, const char* expected_error, uint32_t max_address) { + char buffer[512]; + int valid = validate_trigger(trigger, buffer, sizeof(buffer), max_address); + + if (*expected_error) { + ASSERT_STR_EQUALS(buffer, expected_error); + ASSERT_NUM_EQUALS(valid, 0); + } + else { + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_NUM_EQUALS(valid, 1); + } +} + +static void test_validate_trigger(const char* trigger, const char* expected_error) { + test_validate_trigger_max_address(trigger, expected_error, 0xFFFFFFFF); +} + +static void test_validate_trigger_64k(const char* trigger, const char* expected_error) { + test_validate_trigger_max_address(trigger, expected_error, 0xFFFF); +} + +static void test_validate_trigger_128k(const char* trigger, const char* expected_error) { + test_validate_trigger_max_address(trigger, expected_error, 0x1FFFF); +} + +int validate_trigger_for_console(const char* trigger, char result[], const size_t result_size, uint32_t console_id) { + char* buffer; + rc_trigger_t* compiled; + int success = 0; + + int ret = rc_trigger_size(trigger); + if (ret < 0) { + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_trigger(buffer, trigger, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { + success = 1; + } + + free(buffer); + return success; +} + +static void test_validate_trigger_console(const char* trigger, const char* expected_error, uint32_t console_id) { + char buffer[512]; + int valid = validate_trigger_for_console(trigger, buffer, sizeof(buffer), console_id); + + if (*expected_error) { + ASSERT_STR_EQUALS(buffer, expected_error); + ASSERT_NUM_EQUALS(valid, 0); + } + else { + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_NUM_EQUALS(valid, 1); + } +} + +static void test_combining_conditions_at_end_of_definition() { + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_A:0xH2345=2", "Condition 2: AddSource condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_B:0xH2345=2", "Condition 2: SubSource condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_C:0xH2345=2", "Condition 2: AddHits condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_D:0xH2345=2", "Condition 2: SubHits condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_N:0xH2345=2", "Condition 2: AndNext condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_O:0xH2345=2", "Condition 2: OrNext condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_Z:0xH2345=2", "Condition 2: ResetNextIf condition type expects another condition to follow"); + + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_A:0xH2345=2S0x3456=1", "Core Condition 2: AddSource condition type expects another condition to follow"); + TEST_PARAMS2(test_validate_trigger, "0x3456=1S0xH1234=1_A:0xH2345=2", "Alt1 Condition 2: AddSource condition type expects another condition to follow"); + + /* combining conditions not at end of definition */ + TEST_PARAMS2(test_validate_trigger, "A:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "B:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "N:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "O:0xH1234=1_0xH2345=2", ""); + TEST_PARAMS2(test_validate_trigger, "Z:0xH1234=1_0xH2345=2", ""); +} + +static void test_addhits_chain_without_target() { + TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_0xH2345=2", "Condition 2: Final condition in AddHits chain must have a hit target"); + TEST_PARAMS2(test_validate_trigger, "D:0xH1234=1_0xH2345=2", "Condition 2: Final condition in AddHits chain must have a hit target"); + TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_0xH2345=2.1.", ""); + TEST_PARAMS2(test_validate_trigger, "D:0xH1234=1_0xH2345=2.1.", ""); + + /* ResetIf at the end of a hit chain does not require a hit target. + * It's meant to reset things if some subset of conditions have been true. */ + TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_C:0xH2345=2_R:0=1.1.", ""); + TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_C:0xH2345=2_R:0=1", ""); +} + +static void test_range_comparisons() { + TEST_PARAMS2(test_validate_trigger, "0xH1234>1", ""); + + TEST_PARAMS2(test_validate_trigger, "0xH1234=255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234!=255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234>255", "Condition 1: Comparison is never true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234>=255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234<255", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234<=255", "Condition 1: Comparison is always true (max 255)"); + + /* while a BCD value shouldn't exceed 99, it can reach 165: 0xFF => 15*10+15 */ + TEST_PARAMS2(test_validate_trigger, "b0xH1234<165", ""); + TEST_PARAMS2(test_validate_trigger, "b0xH1234<=165", "Condition 1: Comparison is always true (max 165)"); + + TEST_PARAMS2(test_validate_trigger, "R:0xH1234<255_0xH0000=1.1.", ""); + TEST_PARAMS2(test_validate_trigger, "R:0xH1234<=255_0xH0000=1.1.", "Condition 1: Comparison is always true (max 255)"); + + TEST_PARAMS2(test_validate_trigger, "0xH1234=256", "Condition 1: Comparison is never true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234!=256", "Condition 1: Comparison is always true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234>256", "Condition 1: Comparison is never true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234>=256", "Condition 1: Comparison is never true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234<256", "Condition 1: Comparison is always true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234<=256", "Condition 1: Comparison is always true (max 255)"); + + TEST_PARAMS2(test_validate_trigger, "0x 1234>=65535", ""); + TEST_PARAMS2(test_validate_trigger, "0x 1234>=65536", "Condition 1: Comparison is never true (max 65535)"); + + TEST_PARAMS2(test_validate_trigger, "b0x 1234>=16665", ""); + TEST_PARAMS2(test_validate_trigger, "b0x 1234>=16666", "Condition 1: Comparison is never true (max 16665)"); + + TEST_PARAMS2(test_validate_trigger, "0xW1234>=16777215", ""); + TEST_PARAMS2(test_validate_trigger, "0xW1234>=16777216", "Condition 1: Comparison is never true (max 16777215)"); + + TEST_PARAMS2(test_validate_trigger, "b0xW1234>=1666665", ""); + TEST_PARAMS2(test_validate_trigger, "b0xW1234>=1666666", "Condition 1: Comparison is never true (max 1666665)"); + + TEST_PARAMS2(test_validate_trigger, "0xX1234>=4294967295", ""); + TEST_PARAMS2(test_validate_trigger, "0xX1234>4294967295", "Condition 1: Comparison is never true (max 4294967295)"); + + TEST_PARAMS2(test_validate_trigger, "b0xX1234>=166666665", ""); + TEST_PARAMS2(test_validate_trigger, "b0xX1234>=166666666", "Condition 1: Comparison is never true (max 166666665)"); + + TEST_PARAMS2(test_validate_trigger, "0xT1234>=1", ""); + TEST_PARAMS2(test_validate_trigger, "0xT1234>1", "Condition 1: Comparison is never true (max 1)"); + + /* max for AddSource is the sum of all parts (255+255=510) */ + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0<255", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0<=255", "Condition 2: Comparison is always true (max 255)"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235<510", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235<=510", "Condition 2: Comparison is always true (max 510)"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234*10_0xH1235>=2805", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234*10_0xH1235>2805", "Condition 2: Comparison is never true (max 2805)"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234/10_0xH1235>=280", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234/10_0xH1235>280", "Condition 2: Comparison is never true (max 280)"); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234&10_0xH1235>=265", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234&10_0xH1235>265", "Condition 2: Comparison is never true (max 265)"); + TEST_PARAMS2(test_validate_trigger, "A:b0xH1234*100_b0xH1235>=16665", ""); + TEST_PARAMS2(test_validate_trigger, "A:b0xH1234*100_b0xH1235>16665", "Condition 2: Comparison is never true (max 16665)"); + TEST_PARAMS2(test_validate_trigger, "A:0xX1234_0x 1245=123456", ""); /* sum of 32-bit and 16-bit is still 32-bit */ + + /* max for SubSource is always 0xFFFFFFFF */ + TEST_PARAMS2(test_validate_trigger, "B:0xH1234_0xH1235<510", ""); + TEST_PARAMS2(test_validate_trigger, "B:0xH1234_0xH1235<=510", ""); + + /* A - da + 255 > 255 */ + TEST_PARAMS2(test_validate_trigger, "A:255_B:d0xH1234_0xH1234>255", ""); + + /* division by self results in a 0 or 1 */ + TEST_PARAMS2(test_validate_trigger, "B:0xH1241/0xH1241_B:0xH124c/0xH124c_M:2=2", ""); + TEST_PARAMS2(test_validate_trigger, "B:0xH1241/0xH1241_B:0xH124c/0xH124c_M:2>2", "Condition 3: Comparison is never true (max 2)"); +} + +void test_size_comparisons() { + TEST_PARAMS2(test_validate_trigger, "0xH1234>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234>0x 1235", "Condition 1: Comparing different memory sizes"); + + /* AddSource chain may compare different sizes without warning as the chain changes the + * size of the final result. */ + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235=0xH2345", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235=0x 2345", ""); +} + +void test_address_range() { + /* basic checks for each side */ + TEST_PARAMS2(test_validate_trigger_64k, "0xH1234>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger_64k, "0xH12345>0xH1235", "Condition 1: Address 12345 out of range (max FFFF)"); + TEST_PARAMS2(test_validate_trigger_64k, "0xH1234>0xH12345", "Condition 1: Address 12345 out of range (max FFFF)"); + TEST_PARAMS2(test_validate_trigger_64k, "0xH12345>0xH12345", "Condition 1: Address 12345 out of range (max FFFF)"); + TEST_PARAMS2(test_validate_trigger_64k, "0xX1234>h12345", ""); + TEST_PARAMS2(test_validate_trigger_64k, "K:0xX1234&1073741823_K:0xX2345+{recall}_0=1", ""); + + /* support for multiple memory blocks and edge addresses */ + TEST_PARAMS2(test_validate_trigger_128k, "0xH1234>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH12345>0xH1235", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH0000>5", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH1FFFF>5", ""); + TEST_PARAMS2(test_validate_trigger_128k, "0xH20000>5", "Condition 1: Address 20000 out of range (max 1FFFF)"); + + /* AddAddress can use really big values for negative offsets, don't flag them. */ + TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xHFFFFFF00>5", ""); + TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xH1234>5_0xHFFFFFF00>5", "Condition 3: Address FFFFFF00 out of range (max 1FFFF)"); + TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xHFFFFFF00*2>5", ""); + + /* console-specific warnings */ + TEST_PARAMS3(test_validate_trigger_console, "0xH0123>23", "", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH07FF>23", "", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH0800>23", "Condition 1: Mirror RAM may not be exposed by emulator (address 0800)", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH1FFF>23", "Condition 1: Mirror RAM may not be exposed by emulator (address 1FFF)", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH2000>23", "", RC_CONSOLE_NINTENDO); + TEST_PARAMS3(test_validate_trigger_console, "0xH0123>0xH1000", "Condition 1: Mirror RAM may not be exposed by emulator (address 1000)", RC_CONSOLE_NINTENDO); + + TEST_PARAMS3(test_validate_trigger_console, "0xHC123>23", "", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHDFFF>23", "", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHE000>23", "Condition 1: Echo RAM may not be exposed by emulator (address E000)", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHFDFF>23", "Condition 1: Echo RAM may not be exposed by emulator (address FDFF)", RC_CONSOLE_GAMEBOY); + TEST_PARAMS3(test_validate_trigger_console, "0xHFE00>23", "", RC_CONSOLE_GAMEBOY); + + TEST_PARAMS3(test_validate_trigger_console, "0xHC123>23", "", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHDFFF>23", "", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHE000>23", "Condition 1: Echo RAM may not be exposed by emulator (address E000)", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHFDFF>23", "Condition 1: Echo RAM may not be exposed by emulator (address FDFF)", RC_CONSOLE_GAMEBOY_COLOR); + TEST_PARAMS3(test_validate_trigger_console, "0xHFE00>23", "", RC_CONSOLE_GAMEBOY_COLOR); + + TEST_PARAMS3(test_validate_trigger_console, "0xH9E20=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address 9E20)", RC_CONSOLE_PLAYSTATION); + TEST_PARAMS3(test_validate_trigger_console, "0xHB8BE=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address B8BE)", RC_CONSOLE_PLAYSTATION); + TEST_PARAMS3(test_validate_trigger_console, "0xHFFFF=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address FFFF)", RC_CONSOLE_PLAYSTATION); + TEST_PARAMS3(test_validate_trigger_console, "0xH10000=68", "", RC_CONSOLE_PLAYSTATION); +} + +void test_delta_pointers() { + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_0xH0000=1", ""); + TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:p0xX1234_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_d0xH0000=1", ""); + TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_I:d0xH0010_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_I:0xH0010_0xH0000=1", "Condition 1: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_I:d0xH0010_0xH0000=1", "Condition 2: Using pointer from previous frame"); + TEST_PARAMS2(test_validate_trigger, "I:0xX1234_I:0xH0010_0xH0000=1", ""); +} + +void test_nonsized_pointers() { + TEST_PARAMS2(test_validate_trigger, "I:Ff1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:Fb1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:Fm1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:Fh1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:0xH1234*f1.5_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:b0xH1234_0xH0000=1", "Condition 1: Using transformed value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:~0xH1234_0xH0000=1", "Condition 1: Using transformed value in AddAddress calculation"); + TEST_PARAMS2(test_validate_trigger, "I:~0xH1234*1_0xH0000=1", ""); /* don't report scaled values - assume indexed array */ + TEST_PARAMS2(test_validate_trigger, "I:~0xM1234*4096_0xH0000=1", ""); /* don't report scaled values - assume indexed array */ + TEST_PARAMS2(test_validate_trigger, "I:b0x 1234*8_0xH0000=1", ""); /* don't report scaled values - assume indexed array */ +} + +void test_float_comparisons() { + TEST_PARAMS2(test_validate_trigger, "fF1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fM1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fF1234=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xX1234=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xX1234=f2.3", "Condition 1: Comparison is never true (integer can never be 2.3)"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "0xX1234!=f2.3", "Condition 1: Comparison is always true (integer can never be 2.3)"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "0xX1234f2.3", ""); /* will be converted to > 2 */ + TEST_PARAMS2(test_validate_trigger, "0xX1234>=f2.3", ""); /* will be converted to >= 3 */ + TEST_PARAMS2(test_validate_trigger, "0xX1234=f2.0", ""); /* float can be converted to int without loss of data*/ + TEST_PARAMS2(test_validate_trigger, "0xH1234=f2.3", "Condition 1: Comparison is never true (integer can never be 2.3)"); + TEST_PARAMS2(test_validate_trigger, "0xH1234=f300.0", "Condition 1: Comparison is never true (max 255)"); /* value out of range */ + TEST_PARAMS2(test_validate_trigger, "f2.3=fF1234", ""); + TEST_PARAMS2(test_validate_trigger, "f2.3=0xX1234", "Condition 1: Comparison is never true (integer can never be 2.3)"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "f2.0=0xX1234", ""); + TEST_PARAMS2(test_validate_trigger, "A:Ff2345_fF1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xX2345_fF1234=f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "A:Ff2345_0x1234=f2.3", "Condition 2: Comparison is never true (integer can never be 2.3)"); /* non integral comparison */ + TEST_PARAMS2(test_validate_trigger, "fM1234>f2.3", ""); + TEST_PARAMS2(test_validate_trigger, "fM1234>f-2.3", ""); + TEST_PARAMS2(test_validate_trigger, "I:0xX2345_fM1234>f1.0", ""); + TEST_PARAMS2(test_validate_trigger, "I:0xX2345_fM1234>f-1.0", ""); + TEST_PARAMS2(test_validate_trigger, "fF1234>=f0.0", ""); /* explicit float comparison can be negative */ + TEST_PARAMS2(test_validate_trigger, "fM1234>=f0.0", ""); + TEST_PARAMS2(test_validate_trigger, "fB1234>=f0.0", ""); + TEST_PARAMS2(test_validate_trigger, "fF1234>=0", ""); /* implicit float comparison can be negative */ + TEST_PARAMS2(test_validate_trigger, "fM1234>=0", ""); + TEST_PARAMS2(test_validate_trigger, "fB1234>=0", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234>=f0.1", ""); /* 0 can be less than 0.1 */ + TEST_PARAMS2(test_validate_trigger, "0xH1234>=f255.1", "Condition 1: Comparison is never true (max 255)"); /* 255 cannot be >= 255.1 */ + TEST_PARAMS2(test_validate_trigger, "f0.1<=0xH1234", ""); /* 0 can be less than 0.1 */ + TEST_PARAMS2(test_validate_trigger, "f255.1<=0xH1234", "Condition 1: Comparison is never true (max 255)"); /* 255 cannot be >= 255.1 */ +} + +void test_dependent_conditions() { + TEST_PARAMS2(test_validate_trigger, "M:0xH1234=30_Q:0xH2345!=0", ""); + TEST_PARAMS2(test_validate_trigger, "G:0xH1234=30_Q:0xH2345!=0", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234=30_Q:0xH2345!=0", "Condition 2: MeasuredIf without Measured"); + TEST_PARAMS2(test_validate_trigger, "M:0xH1234=30SQ:0xH2345!=0", "Alt1 Condition 1: MeasuredIf without Measured"); +} + +void test_conflicting_conditions() { + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_0xH0000=2", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000>5", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000=5", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0001=5", ""); /* ignore differing address */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0x 0000=5", ""); /* ignore differing size */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_d0xH0000=5", ""); /* ignore differing type */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000=5.1.", ""); /* ignore anything with a hit target */ + TEST_PARAMS2(test_validate_trigger, "O:0xH0000<5_0xH0000=5", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "A:0xH0000<5_0xH0000=5", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "N:0xH0000<5_R:0xH0001=8_T:0xH0000=0_0xH0002=1.1.", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "T:0xH0000=8_N:d0xH0000=0_R:0xH0000=8_0xH0001=1.1.", ""); /* ignore combining conditions - individually, third conditions is conflicting (second allowed because of delta) */ + TEST_PARAMS2(test_validate_trigger, "0xH0001=58.1._N:0xH0001!=58_N:0xH0001!=4_R:0xH0001!=18", ""); /* ignore combining conditions */ + TEST_PARAMS2(test_validate_trigger, "0xH0000<=5_0xH0000>=5", ""); + TEST_PARAMS2(test_validate_trigger, "0xH0000>1_0xH0000<3", ""); + TEST_PARAMS2(test_validate_trigger, "1=1S0xH0000=1S0xH0000=2", ""); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=2", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1S0xH0000=1_0xH0001=1.1.", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1SR:0xH0000=1_0xH0001=1.1.", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_0xH0000=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1S0xH0000=1", ""); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SP:0xH0000!=1", "Alt1 Condition 1: Conflicts with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_P:0xH0000!=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000=1_0xH0001=1.1.", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1SP:0xH0000=5", ""); + TEST_PARAMS2(test_validate_trigger, "M:0xH0000=5_Q:0xH0000=255", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_Q:0xH0000=2_M:0xH0001=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_0xH0000=2_M:0xH0001=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_Q:0xH0000=2_M:0xH0001=1", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_0xH0000<5_A:0xX0004_0xH0000>5", "Condition 4: Conflicts with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "A:d0xX0004_d0xH0000<5_A:0xX0004_0xH0000>5", ""); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_d0xH0000<5_A:0xX0004_0xH0000>5", ""); + + /* PauseIf prevents hits from incrementing. ResetIf clears all hits. If both exist and are conflicting, the group + * will only ever be paused or reset, and therefore will never be true */ + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000!=1_0xH0001=1.1.", "Condition 2: Conflicts with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=1_P:0xH0000=1_0xH0001=1.1.", "Condition 2: Conflicts with Condition 1"); + /* if the PauseIf is less restrictive than the ResetIf, it's just a guard. ignore it*/ + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000!=6_0xH0001=1.1.", ""); + /* PauseIf in alternate group does not affect the ResetIf*/ + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SR:0xH0000!=1_0xH0001=1.1.", ""); + + /* cannot determine OrNext conflicts */ + TEST_PARAMS2(test_validate_trigger, "O:0xH0000=1_0xH0001=1_O:0xH0000=2_0xH0001=2", ""); + TEST_PARAMS2(test_validate_trigger, "O:0xH0000=1_0xH0001=1_O:0xH0000=1_0xH0001=2", ""); + + /* cannot determine AddSource conflicts */ + TEST_PARAMS2(test_validate_trigger, "d0xH1234>0_A:0xH2345_0>d0xH1234", ""); + + /* AndNext conflicts are limited to matching the last condition after exactly matching the others */ + TEST_PARAMS2(test_validate_trigger, "N:0xH0000=1_0xH0001=1_N:0xH0000=2_0xH0001=2", ""); + TEST_PARAMS2(test_validate_trigger, "N:0xH0000=1_0xH0001=1_N:0xH0000=2_0xH0001=1", ""); /* technically conflicting, but hard to detect */ + TEST_PARAMS2(test_validate_trigger, "N:0xH0000=1_0xH0001=1_N:0xH0000=1_0xH0001=2", "Condition 4: Conflicts with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=0_N:0xH0000!=0_0xH0000=2", ""); +} + +void test_redundant_conditions() { + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_0xH0000=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<3_0xH0000<5", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000<3", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=1", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_0xH0000!=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0x 0001=50.1._0x 0000>=300_R:0x 0000<2", ""); /* ResetIf not redundant if hit target available */ + TEST_PARAMS2(test_validate_trigger, "0x 0001=50.1._R:0x 0000>=300_R:0x 0000<2", ""); + TEST_PARAMS2(test_validate_trigger, "0x 0001=50.1._R:0x 0000<300_R:0x 0000<2", "Condition 2: Redundant with Condition 3"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_P:0xH0000!=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000!=1S0xH0000!=1", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=1", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SP:0xH0000=1", ""); /* same pauseif can appear in different groups */ + TEST_PARAMS2(test_validate_trigger, "0xH0000=4.1._0xH0000=5_P:0xH0000<4", ""); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=5_Q:0xH0000!=255_M:0xH0001=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "M:0xH0000=5_Q:0xH0000!=255", ""); /* measuredif not redundant measured */ + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_0xH0000=1_M:0xH0001=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_Q:0xH0000=1_M:0xH0001=1", "Condition 2: Redundant with Condition 1"); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", "Condition 4: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_0xH0000=1_M:0xH0001=1", "Condition 4: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", "Condition 2: Redundant with Condition 4"); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0005_Q:0xH0000=1_M:0xH0001=1", ""); /* different chains */ + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0x 0004_Q:0xH0000=1_M:0xH0001=1", ""); /* different sizes */ + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", "Condition 4: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_A:0xX0008_Q:0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", ""); /* longer first chain */ + TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_A:0xX0008_Q:0xH0000=1_M:0xH0001=1", ""); /* longer second chain */ + TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_M:0xH0001=1SQ:0xH0000=1_M:0xH0002=1", ""); /* same measuredif can appear in different groups */ + TEST_PARAMS2(test_validate_trigger, "T:0xH0000!=0_T:0xH0000=6", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000!=0_T:0xH0000=6", ""); /* trigger more restrictive than non-trigger */ + TEST_PARAMS2(test_validate_trigger, "T:0xH0000!=0_0xH0000=6", "Condition 1: Redundant with Condition 2"); /* trigger less restrictive than non-trigger */ + TEST_PARAMS2(test_validate_trigger, "0xH0000=6_T:0xH0000=6", "Condition 2: Redundant with Condition 1"); /* trigger same as non-trigger */ + TEST_PARAMS2(test_validate_trigger, "0xH0000=1_Q:0xH0000=1_M:0xH0001=1", "Condition 1: Redundant with Condition 2"); + TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000!=0S0xH0001=2", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000!=0S0xH0000=1S0xH0001=2", ""); /* more restrictive alt 1 is not redundant with core */ + TEST_PARAMS2(test_validate_trigger, "0xH0000!=0S0xH0000!=0S0xH0001=2", "Alt1 Condition 1: Redundant with Core Condition 1"); + TEST_PARAMS2(test_validate_trigger, "0xH0000<10_N:0xH0001=2_0xH0000<20", ""); /* AndNext should prevent condition 3 being compared directly to condition 1 */ + TEST_PARAMS2(test_validate_trigger, "0xH0001=7.1._R:0xH0000=0_T:0xH0000=1", ""); +} + +void test_resetif_hittargets() { + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0", "Condition 1: No captured hits to reset"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0_0xH0001=1.1.", ""); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0_M:0xH0001=1.1.", ""); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0.1._0xH0001=1.1.", "Condition 1: Hit target of 1 is redundant on ResetIf"); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0.2._0xH0001=1.1.", ""); + TEST_PARAMS2(test_validate_trigger, "I:0xG1234&536870911_R:0xG0000=4294967294_0xH2222=1.1.", ""); + TEST_PARAMS2(test_validate_trigger, "N:0xH0001=4.1._T:0xH0001=5_R:0xH0001<2", ""); + TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0S0xH0001=1.1.S0xH0002=1", ""); +} + +void test_variable_operand_errors() { + TEST_PARAMS2(test_validate_trigger, "K:4_M:{thingy}", "Unknown variable name"); /* variable that does not exist */ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{th$ingy}", "Invalid variable name"); /* separator in name */ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{th*ingy}", "Invalid variable name"); /* invalid character in name */ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{2things}", "Invalid variable name"); /* variable name begins with number*/ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{recall_P:0xH01=18", "Invalid variable name"); /* missing closing curly brace */ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{thisvariablenameistoolong}_P:0xH01=18", "Invalid variable name"); /*too long */ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{}_P:0xH01=18", "Invalid variable name"); /* no name */ + TEST_PARAMS2(test_validate_trigger, "K:4_M:{recall}=4", ""); /* recognized as recall operand */ +} + +void test_remember_recall_errors() { + TEST_PARAMS2(test_validate_trigger, "{recall}=5", "Condition 1: Recall used before Remember"); /* No value ever remembered */ + TEST_PARAMS2(test_validate_trigger, "{recall}=5_K:0xH1234&1023_K:{recall}*8_{recall}=100", "Condition 1: Recall used before Remember"); /* First remember is after first recall. */ + TEST_PARAMS2(test_validate_trigger, "K:0xH1234&1023_K:{recall}*8_{recall}=100", ""); /* Recall used after Remember */ + TEST_PARAMS2(test_validate_trigger, "{recall}=5_K:0xH1234*2_P:{recall}>6", ""); /* Remember sets recall in pause - no warning */ + TEST_PARAMS2(test_validate_trigger, "K:0xH1234*2_{recall}=5_P:{recall}>6", "Condition 3: Recall used before Remember"); /* Pause happens before remembered value. */ +} + +void test_error_priorities() { + TEST_PARAMS2(test_validate_trigger, "R:0xH1234=1_0xH2345=500", "Condition 2: Comparison is never true (max 255)"); /* impossible condition more important than unnecessary reset */ + TEST_PARAMS2(test_validate_trigger, "0xH1234>5_0xH1234!=0_A:0xH2345", "Condition 3: AddSource condition type expects another condition to follow"); /* impotent condition more important than redundant */ + TEST_PARAMS2(test_validate_trigger, "0xH1234!=d0x 1234_I:d0x2345_0=6", "Condition 2: Using pointer from previous frame"); /* pointer math more important that potential logic errors */ + TEST_PARAMS2(test_validate_trigger, "R:0xH1234=1.1.S0xH2345=500", "Alt1 Condition 1: Comparison is never true (max 255)"); /* impossible condition in alt more important than redundant condition in core */ +} + +void test_rc_validate(void) { + TEST_SUITE_BEGIN(); + + /* positive baseline test cases */ + TEST_PARAMS2(test_validate_trigger, "", ""); + TEST_PARAMS2(test_validate_trigger, "0xH1234=1_0xH2345=2S0xH3456=1S0xH3456=2", ""); + TEST_PARAMS2(test_validate_trigger, "S0xH3456=1S0xH3456=2", ""); /* empty core */ + + test_combining_conditions_at_end_of_definition(); + test_addhits_chain_without_target(); + test_range_comparisons(); + test_size_comparisons(); + test_address_range(); + test_delta_pointers(); + test_nonsized_pointers(); + test_float_comparisons(); + test_dependent_conditions(); + test_conflicting_conditions(); + test_redundant_conditions(); + test_resetif_hittargets(); + test_variable_operand_errors(); + test_remember_recall_errors(); + test_error_priorities(); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_richpresence.c b/src/rcheevos/test/rcheevos/test_richpresence.c new file mode 100644 index 0000000000..941dbfee79 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_richpresence.c @@ -0,0 +1,1542 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +#include "../src/rc_compat.h" + +static void _assert_parse_richpresence(rc_richpresence_t** richpresence, void* buffer, size_t buffer_size, const char* script) { + int size; + unsigned* overflow; + *richpresence = NULL; + + size = rc_richpresence_size(script); + ASSERT_NUM_GREATER(size, 0); + ASSERT_NUM_LESS_EQUALS(size + 4, buffer_size); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + *richpresence = rc_parse_richpresence(buffer, script, NULL, 0); + ASSERT_PTR_NOT_NULL(*richpresence); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_parse_richpresence(richpresence_out, buffer, script) ASSERT_HELPER(_assert_parse_richpresence(richpresence_out, buffer, sizeof(buffer), script), "assert_parse_richpresence") + +static void _assert_richpresence_output(rc_richpresence_t* richpresence, memory_t* memory, const char* expected_display_string) { + char output[256]; + int result; + + result = rc_evaluate_richpresence(richpresence, output, sizeof(output), peek, memory, NULL); + ASSERT_STR_EQUALS(output, expected_display_string); + ASSERT_NUM_EQUALS(result, strlen(expected_display_string)); +} +#define assert_richpresence_output(richpresence, memory, expected_display_string) ASSERT_HELPER(_assert_richpresence_output(richpresence, memory, expected_display_string), "assert_richpresence_output") + +static void test_empty_script() { + int lines; + int result = rc_richpresence_size_lines("", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 1); +} + +static void test_simple_richpresence(const char* script, const char* expected_display_string) { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected_display_string); +} + +static void assert_buffer_boundary(rc_richpresence_t* richpresence, memory_t* memory, int buffersize, int expected_result, const char* expected_display_string) { + char output[256]; + int result; + unsigned* overflow = (unsigned*)(&output[buffersize]); + *overflow = 0xCDCDCDCD; + + result = rc_evaluate_richpresence(richpresence, output, buffersize, peek, memory, NULL); + ASSERT_NUM_EQUALS(result, expected_result); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + ASSERT_STR_EQUALS(output, expected_display_string); +} + +static void test_buffer_boundary() { + uint8_t ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[2048]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* static strings */ + assert_parse_richpresence(&richpresence, buffer, "Display:\nABCDEFGH"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "ABCDEF"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "ABCDEFG"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "ABCDEFGH"); /* all 8 chars written */ + + /* number formatting */ + assert_parse_richpresence(&richpresence, buffer, "Format:V\nFormatType=VALUE\n\nDisplay:\n@V(0xX0000)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 10, "16,777,2"); /* only 8 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 10, "16,777,21"); /* only 8 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 11, 10, "16,777,216"); /* all 10 chars written */ + + /* lookup */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:L\n1=ABCDEFGH\n\nDisplay:\n@L(0xH0003)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "ABCDEF"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "ABCDEFG"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "ABCDEFGH"); /* all 8 chars written */ + + /* unknown macro - "[Unknown macro]L(0xH0003)" = 25 chars */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n@L(0xH0003)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 25, "[Unkno"); /* only 6 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 25, 25, "[Unknown macro]L(0xH0003"); /* only 24 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 26, 25, "[Unknown macro]L(0xH0003)"); /* all 25 chars written */ + + /* multipart */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:L\n0=\n1=A\n4=ABCD\n8=ABCDEFGH\n\nFormat:V\nFormatType=VALUE\n\nDisplay:\n@L(0xH0000)--@L(0xH0001)--@V(0xH0002)"); + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 5, "----0"); /* initial value fits */ + ram[1] = 4; + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 9, "--ABCD-"); /* only 7 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 9, "--ABCD--"); /* only 8 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 9, "--ABCD--0"); /* all 9 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 5, 9, "--AB"); /* only 7 chars written */ + ram[2] = 123; + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 11, "--ABCD--1"); /* only 9 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 11, 11, "--ABCD--12"); /* only 10 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 12, 11, "--ABCD--123"); /* all 11 chars written */ + TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 2, 11, "-"); /* only 1 char written */ +} + +static void test_conditional_display_simple() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?Zero\n?0xH0000=1?One\nOther"); + assert_richpresence_output(richpresence, &memory, "Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_conditional_display_after_default() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\nOther\n?0xH0000=0?Zero\n?0xH0000=1?One"); + assert_richpresence_output(richpresence, &memory, "Other"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_conditional_display_no_default() { + int lines; + int result = rc_richpresence_size_lines("Display:\n?0xH0000=0?Zero", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 3); +} + +static void test_conditional_display_common_condition() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* condition for Second is a sub-clause of First */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0_0xH0001=18?First\n?0xH0000=0?Second\nThird"); + assert_richpresence_output(richpresence, &memory, "First"); + + /* secondary part of first condition is false, will match second condition */ + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "Second"); + + /* common condition is false, will use default */ + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Third"); + + /* ================================================================ */ + /* == reverse the conditions so the First is a sub-clause of Second */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?First\n?0xH0000=0_0xH0001=18?Second\nThird"); + + /* reset the memory so it matches the first test, First clause will be matched before even looking at Second */ + ram[0] = 0; + ram[1] = 18; + assert_richpresence_output(richpresence, &memory, "First"); + + /* secondary part of second condition is false, will still match first condition */ + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "First"); + + /* common condition is false, will use default */ + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Third"); +} + +static void test_conditional_display_duplicated_condition() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?First\n?0xH0000=0?Second\nThird"); + assert_richpresence_output(richpresence, &memory, "First"); + + /* cannot activate Second */ + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Third"); +} + +static void test_conditional_display_invalid_condition_logic() { + int lines; + int result = rc_richpresence_size_lines("Display:\n?BANANA?Zero\nDefault", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); +} + +static void test_conditional_display_shared_lookup() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0001=1?One @Points(0xL0002)\n?0xH0001=0?Zero @Points(0xL0002)\nDefault @Points(0xL0002)"); + assert_richpresence_output(richpresence, &memory, "Default 4"); + + ram[1] = 1; + ram[2] = 24; + assert_richpresence_output(richpresence, &memory, "One 8"); + + ram[1] = 0; + assert_richpresence_output(richpresence, &memory, "Zero 8"); +} + +static void test_conditional_display_whitespace_text() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0? \n?0xH0000=1?One\nOther"); + assert_richpresence_output(richpresence, &memory, " "); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_macro_value() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "13,330 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "13,332 Points"); +} + +static void test_macro_value_nibble() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* nibble first, see if byte overwrites */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xL0001)@Points(0xH0001) Points"); + assert_richpresence_output(richpresence, &memory, "218 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "420 Points"); + + /* put byte first, see if nibble overwrites */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH0001)@Points(0xL0001) Points"); + assert_richpresence_output(richpresence, &memory, "204 Points"); +} + +static void test_macro_value_bcd() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(b0xH0001) Points"); + assert_richpresence_output(richpresence, &memory, "12 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "14 Points"); +} + +static void test_macro_value_bitcount() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Bits\nFormatType=VALUE\n\nDisplay:\n@Bits(0xK0001) Bits"); + assert_richpresence_output(richpresence, &memory, "2 Bits"); + + ram[1] = 0x76; + assert_richpresence_output(richpresence, &memory, "5 Bits"); +} + +static void test_conditional_display_indirect() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?I:0xH0000_0xH0002=h01?True\nFalse\n"); + assert_richpresence_output(richpresence, &memory, "False"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "False"); + + ram[3] = 1; + assert_richpresence_output(richpresence, &memory, "True"); +} + +static void test_conditional_display_unnecessary_measured() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?M:0xH0000=0?Zero\n?0xH0000=1?One\nOther"); + assert_richpresence_output(richpresence, &memory, "Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Other"); +} + +static void test_conditional_display_unnecessary_measured_indirect() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n?I:0xH0000_M:0xH0002=h01?True\nFalse\n"); + assert_richpresence_output(richpresence, &memory, "False"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "False"); + + ram[3] = 1; + assert_richpresence_output(richpresence, &memory, "True"); +} + +static void test_conditional_display_invalid() { + int lines_read = 0; + ASSERT_NUM_EQUALS(rc_richpresence_size_lines("Display:\n?I:0x0x0000=1?True\nFalse\n", &lines_read), RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines_read, 2); + + ASSERT_NUM_EQUALS(rc_richpresence_size_lines("Display:\n?0x0000=1 0x0001=2?True\nFalse\n", &lines_read), RC_INVALID_OPERATOR); + ASSERT_NUM_EQUALS(lines_read, 2); +} + +static void test_macro_value_adjusted_negative() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001_V-10000) Points"); + assert_richpresence_output(richpresence, &memory, "3,330 Points"); + + ram[2] = 7; + assert_richpresence_output(richpresence, &memory, "-8,190 Points"); +} + +static void test_macro_value_from_formula() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH0001*100_0xH0002) Points"); + assert_richpresence_output(richpresence, &memory, "1,852 Points"); + + ram[1] = 32; + assert_richpresence_output(richpresence, &memory, "3,252 Points"); +} + +static void test_macro_value_from_hits() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Hits\nFormatType=VALUE\n\nDisplay:\n@Hits(M:0xH01=1) Hits"); + assert_richpresence_output(richpresence, &memory, "0 Hits"); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "1 Hits"); + assert_richpresence_output(richpresence, &memory, "2 Hits"); + assert_richpresence_output(richpresence, &memory, "3 Hits"); +} + +static void test_macro_value_from_indirect() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nPointing at @Value(I:0xH00_M:0xH01)"); + assert_richpresence_output(richpresence, &memory, "Pointing at 18"); + + /* pointed at data changes */ + ram[1] = 99; + assert_richpresence_output(richpresence, &memory, "Pointing at 99"); + + /* pointer changes */ + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Pointing at 52"); +} + +static void test_macro_value_divide_by_zero() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(0xH02/0xH00)"); + assert_richpresence_output(richpresence, &memory, "Result is 0"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Result is 52"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "Result is 26"); +} + +static void test_macro_value_divide_by_self() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* sneaky trick to turn any non-zero value into 1 */ + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(0xH02/0xH02)"); + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 1; + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 32; + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 255; + assert_richpresence_output(richpresence, &memory, "Result is 1"); + + ram[2] = 0; + assert_richpresence_output(richpresence, &memory, "Result is 0"); +} + +static void test_macro_value_remember_recall() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* sneaky trick to turn any non-zero value into 1 */ + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(A:0xH02*2_K:1_M:{recall}*3)"); + assert_richpresence_output(richpresence, &memory, "Result is 315"); + + ram[2] = 1; + assert_richpresence_output(richpresence, &memory, "Result is 9"); + + ram[2] = 0; + assert_richpresence_output(richpresence, &memory, "Result is 3"); +} + +static void test_macro_value_invalid() { + ASSERT_NUM_EQUALS(rc_richpresence_size("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x0x0001) Points"), RC_INVALID_MEMORY_OPERAND); +} + +static void test_macro_value_measured_if() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(Q:0xH000=0_M:0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "13,330 Points"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "0 Points"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "0 Points"); + + ram[0] = 0; + assert_richpresence_output(richpresence, &memory, "13,332 Points"); +} + +static void test_multiple_macros() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Number\nFormatType=VALUE\n\nDisplay:\n@Number(0x 0001) Points | @Number(A:0xH0002_A:0xH0003_M:0xH0004) Items"); + assert_richpresence_output(richpresence, &memory, "13,330 Points | 309 Items"); + + ram[2] = 0x05; + assert_richpresence_output(richpresence, &memory, "1,298 Points | 262 Items"); + + /* both should map to memrefs, so no helper values will be created */ + ASSERT_PTR_NULL(richpresence->values); +} + +static void test_macro_hundreds() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=HUNDREDS\n\nDisplay:\nResult is @Value(0xH00)"); + assert_richpresence_output(richpresence, &memory, "Result is 0"); + + ram[0] = 18; + assert_richpresence_output(richpresence, &memory, "Result is 1,800"); + + ram[0] = 255; + assert_richpresence_output(richpresence, &memory, "Result is 25,500"); + + ram[0] = 0; + assert_richpresence_output(richpresence, &memory, "Result is 0"); +} + +static void test_macro_frames() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Frames\nFormatType=FRAMES\n\nDisplay:\n@Frames(0x 0001)"); + assert_richpresence_output(richpresence, &memory, "3:42.16"); + + ram[1] = 20; + assert_richpresence_output(richpresence, &memory, "3:42.20"); +} + +static void test_macro_float(const char* format, uint32_t value, const char* expected) { + uint8_t ram[4]; + memory_t memory; + rc_richpresence_t* richpresence; + char script[128]; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + ram[0] = (value & 0xFF); + ram[1] = (value >> 8) & 0xFF; + ram[2] = (value >> 16) & 0xFF; + ram[3] = (value >> 24) & 0xFF; + + snprintf(script, sizeof(script), "Format:N\nFormatType=%s\n\nDisplay:\n@N(fF0000)", format); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_macro_lookup_simple() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_with_inline_comment() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n// Zero\n0=Zero\n// One\n1=One\n//2=Two\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_hex_keys() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0x00=Zero\n0x01=One\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_default() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n*=Star\n\nDisplay:\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At Star"); +} + +static void test_macro_lookup_crlf() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\r\n0=Zero\r\n1=One\r\n\r\nDisplay:\r\nAt @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_after_display() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\nAt @Location(0xH0000)\n\nLookup:Location\n0=Zero\n1=One"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_macro_lookup_from_formula() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000*0.5)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At One"); +} + +static void test_macro_lookup_from_indirect() { + uint8_t ram[] = { 0x00, 0x00, 0x01, 0x00, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(I:0xH0000=0_M:0xH0001)"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At Zero"); +} + +static void test_macro_lookup_repeated() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000), Near @Location(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At , Near "); +} + +static void test_macro_lookup_shared() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for multiple addresses */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000), Near @Location(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near "); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "At Zero, Near One"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near One"); +} + +static void test_macro_lookup_multiple() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple lookups can be used for same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nLookup:Location2\n0=zero\n1=one\n\nDisplay:\nAt @Location(0xH0000), Near @Location2(0xH0000)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near one"); + + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At , Near "); +} + +static void test_macro_lookup_and_value() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nFormat:Location2\nFormatType=VALUE\n\nDisplay:\nAt @Location(0xH0000), Near @Location2(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "At Zero, Near 18"); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "At Zero, Near 1"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One, Near 1"); +} + +static void test_macro_lookup_negative_value() { + uint8_t ram[] = { 0x00, 0x00, 0x00, 0x00, 0x00 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* lookup keys are signed 32-bit values. the -1 will become 0xFFFFFFFF */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Diff\n0=Zero\n1=One\n-1=Negative One\n\nDisplay:\nDiff=@Diff(B:0xH0000_M:0xH0001)"); + assert_richpresence_output(richpresence, &memory, "Diff=Zero"); + + ram[1] = 1; + assert_richpresence_output(richpresence, &memory, "Diff=One"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "Diff=Zero"); + + ram[1] = 0; + assert_richpresence_output(richpresence, &memory, "Diff=Negative One"); +} + +static void test_macro_lookup_value_with_whitespace() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0= Zero \n1= One \n\nDisplay:\nAt '@Location(0xH0000)' "); + assert_richpresence_output(richpresence, &memory, "At ' Zero ' "); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At ' One ' "); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At '' "); +} + +static void test_macro_lookup_mapping_repeated() { + uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:OddOrEven\n0=Even\n1=Odd\n2=Even\n3=Odd\n4=Even\n5=Odd\n\nDisplay:\nFirst:@OddOrEven(0xH0000), Second:@OddOrEven(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Even"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:Odd, Second:Even"); + + ram[0] = 2; + ram[1] = 3; + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Odd"); +} + +static void test_macro_lookup_mapping_repeated_csv() { + uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:OddOrEven\n0,2,4=Even\n1,3,5=Odd\n\nDisplay:\nFirst:@OddOrEven(0xH0000), Second:@OddOrEven(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Even"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:Odd, Second:Even"); + + ram[0] = 2; + ram[1] = 3; + assert_richpresence_output(richpresence, &memory, "First:Even, Second:Odd"); +} + +static void test_macro_lookup_mapping_merged() { + uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Place\n0=First\n1=First\n2=First\n3=Second\n4=Second\n5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 5; + ram[1] = 2; + assert_richpresence_output(richpresence, &memory, "First:Second, Second:First"); + + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->first, 0); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->last, 2); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->first, 3); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->last, 5); + ASSERT_PTR_NULL(richpresence->first_lookup->root->right->right); +} + +static void test_macro_lookup_mapping_range() { + uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* same lookup can be used for the same address */ + assert_parse_richpresence(&richpresence, buffer, "Lookup:Place\n0-2=First\n5,3-4=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)"); + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); + + ram[0] = 5; + ram[1] = 2; + assert_richpresence_output(richpresence, &memory, "First:Second, Second:First"); + + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->first, 0); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->last, 2); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->first, 3); + ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->last, 5); + ASSERT_PTR_NULL(richpresence->first_lookup->root->right->right); +} + +static void test_macro_lookup_mapping_range_overlap() { + int result; + int lines; + + /* 2 appears in the first range (2) and the second (2) */ + result = rc_richpresence_size_lines("Lookup:Place\n2=First\n2=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); + ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); + ASSERT_NUM_EQUALS(lines, 3); + + /* 2 appears in the first range (0-2) and the second (2-5) */ + result = rc_richpresence_size_lines("Lookup:Place\n0-2=First\n2-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); + ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); + ASSERT_NUM_EQUALS(lines, 3); + + /* 2 appears in the first range (0-2) and the second (2,4-5) */ + result = rc_richpresence_size_lines("Lookup:Place\n0-2=First\n2,4-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); + ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); + ASSERT_NUM_EQUALS(lines, 3); + + /* 1 and 2 appear in the first range (0-2) and the second (1,2,4-5) */ + result = rc_richpresence_size_lines("Lookup:Place\n0-2=First\n1,2,4-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); + ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); + ASSERT_NUM_EQUALS(lines, 3); + + /* 2 appears in the first range (2) and the second (1-5) */ + result = rc_richpresence_size_lines("Lookup:Place\n2=First\n1-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); + ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); + ASSERT_NUM_EQUALS(lines, 3); +} + +static void test_macro_lookup_invalid() { + int result; + int lines; + + /* lookup value starts with Ox instead of 0x */ + result = rc_richpresence_size_lines("Lookup:Location\nOx0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); + + /* lookup value contains invalid hex character */ + result = rc_richpresence_size_lines("Lookup:Location\n0xO=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); + + /* lookup value is not numeric */ + result = rc_richpresence_size_lines("Lookup:Location\nZero=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); + ASSERT_NUM_EQUALS(lines, 2); +} + +static void test_macro_escaped() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ensures @ can be used in the display string by escaping it */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n\\@Points(0x 0001) \\@@Points(0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "@Points(0x 0001) @13,330 Points"); +} + +static void test_macro_undefined() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001) Points"); + assert_richpresence_output(richpresence, &memory, "[Unknown macro]Points(0x 0001) Points"); +} + +static void test_macro_undefined_at_end_of_line() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* adding [Unknown macro] to the output effectively makes the script larger than it started. + * since we don't detect unknown macros in `rc_richpresence_size`, this was causing a + * write-past-end-of-buffer memory corruption error. this test recreated that error. */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001)"); + assert_richpresence_output(richpresence, &memory, "[Unknown macro]Points(0x 0001)"); +} + +static void test_macro_unterminated() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* valid macro with no closing parenthesis should just be dumped as-is */ + assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001"); + assert_richpresence_output(richpresence, &memory, "@Points(0x 0001"); + + /* adding [Unknown macro] to the output effectively makes the script larger than it started. + * since we don't detect unknown macros in `rc_richpresence_size`, this was causing a + * write-past-end-of-buffer memory corruption error. this test recreated that error. */ + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001"); + assert_richpresence_output(richpresence, &memory, "@Points(0x 0001"); +} + +static void test_macro_without_parameter() { + int result; + int lines; + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points Points", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(lines, 5); + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points() Points", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_macro_without_parameter_conditional_display() { + int result; + int lines; + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n?0x0h0001=1?@Points Points\nDefault", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_NUM_EQUALS(lines, 5); + + result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n?0x0h0001=1?@Points() Points\nDefault", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_macro_non_numeric_parameter() { + int lines; + int result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(Zero) Points", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_macro_mathematic_chain() { + int lines; + int result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH1234 + 5) Points", &lines); + ASSERT_NUM_EQUALS(result, RC_INVALID_OPERATOR); + ASSERT_NUM_EQUALS(lines, 5); +} + +static void test_builtin_macro(const char* macro, const char* expected) { + uint8_t ram[] = { 0x39, 0x30 }; + memory_t memory; + rc_richpresence_t* richpresence; + char script[128]; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + snprintf(script, sizeof(script), "Display:\n@%s(0x 0)", macro); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_builtin_macro_float(const char* macro, const char* expected) { + uint8_t ram[] = { 0x92, 0x44, 0x9A, 0x42 }; /* 77.133926 */ + memory_t memory; + rc_richpresence_t* richpresence; + char script[128]; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + snprintf(script, sizeof(script), "Display:\n@%s(fF0000)", macro); + + assert_parse_richpresence(&richpresence, buffer, script); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_builtin_macro_unsigned_large() { + uint8_t ram[] = { 0x85, 0xE2, 0x59, 0xC7 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@Unsigned(0xX0)"); + assert_richpresence_output(richpresence, &memory, "3,344,556,677"); +} + +static void test_builtin_macro_override() { + uint8_t ram[] = { 0x39, 0x30 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Number\nFormatType=SECS\n\nDisplay:\n@Number(0x 0)"); + assert_richpresence_output(richpresence, &memory, "3h25:45"); +} + +static void test_unformatted_legacy() { + uint8_t ram[] = { 0x39, 0x30 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Format:Unformatted\nFormatType=VALUE\n\nDisplay:\n@Unformatted(0x 0)"); + assert_richpresence_output(richpresence, &memory, "12345"); +} + +static void test_asciichar() { + uint8_t ram[] = { 'K', 'e', 'n', '\0', 'V', 'e', 'g', 'a', 1 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Lookup:Round\n0= (Round 1)\n1= (Round 2)\n\n" + "Display:\n@ASCIIChar(0xH0)@ASCIIChar(0xH1)@ASCIIChar(0xH2)@ASCIIChar(0xH3) vs @ASCIIChar(0xH4)@ASCIIChar(0xH5)@ASCIIChar(0xH6)@ASCIIChar(0xH7)@Round(0xH8)"); + assert_richpresence_output(richpresence, &memory, "Ken vs Vega (Round 2)"); + + ram[0] = 'R'; ram[1] = 'o'; ram[2] = 's'; ram[3] = 'e'; + ram[4] = 'K'; ram[5] = 'e'; ram[6] = 'n'; ram[7] = '\0'; + ram[8] = 0; + assert_richpresence_output(richpresence, &memory, "Rose vs Ken (Round 1)"); +} + +static void test_ascii8(unsigned char c1, unsigned char c2, unsigned char c3, unsigned char c4, + unsigned char c5, unsigned char c6, unsigned char c7, unsigned char c8, char* expected) { + uint8_t ram[9]; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + ram[0] = c1; ram[1] = c2; ram[2] = c3; ram[3] = c4; + ram[4] = c5; ram[5] = c6; ram[6] = c7; ram[7] = c8; + ram[8] = '~'; + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@ASCIIChar(0xH0)@ASCIIChar(0xH1)@ASCIIChar(0xH2)@ASCIIChar(0xH3)@ASCIIChar(0xH4)@ASCIIChar(0xH5)@ASCIIChar(0xH6)@ASCIIChar(0xH7)"); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_unicode4(unsigned short c1, unsigned short c2, unsigned short c3, unsigned short c4, char* expected) { + uint8_t ram[10]; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + ram[0] = c1 & 0xFF; ram[1] = (c1 >> 8) & 0xFF; + ram[2] = c2 & 0xFF; ram[3] = (c2 >> 8) & 0xFF; + ram[4] = c3 & 0xFF; ram[5] = (c3 >> 8) & 0xFF; + ram[6] = c4 & 0xFF; ram[7] = (c4 >> 8) & 0xFF; + ram[8] = '~'; ram[9] = '\0'; + + assert_parse_richpresence(&richpresence, buffer, "Display:\n@UnicodeChar(0x 0)@UnicodeChar(0x 2)@UnicodeChar(0x 4)@UnicodeChar(0x 6)"); + assert_richpresence_output(richpresence, &memory, expected); +} + +static void test_random_text_between_sections() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "Locations are fun!\nLookup:Location\n0=Zero\n1=One\n\nDisplay goes here\nDisplay:\nAt @Location(0xH0000)\n\nWritten by User3"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_comments() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "// Locations are fun!\nLookup:Location // lookup\n0=Zero // 0\n1=One // 1\n\n//Display goes here\nDisplay: // display\nAt @Location(0xH0000) // text\n\n//Written by User3"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_comments_between_lines() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_richpresence_t* richpresence; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_richpresence(&richpresence, buffer, "// Locations are fun!\nLookup:Location\n// lookup\n0=Zero\n// 0\n1=One\n// 1\n\n//Display goes here\nDisplay:\n// display\nAt @Location(0xH0000)\n// text\n\n//Written by User3"); + assert_richpresence_output(richpresence, &memory, "At Zero"); + + ram[0] = 1; + assert_richpresence_output(richpresence, &memory, "At One"); + + /* no entry - default to empty string */ + ram[0] = 2; + assert_richpresence_output(richpresence, &memory, "At "); +} + +static void test_display_string_comment_only() { + int lines; + int result = rc_richpresence_size_lines("Display:\n// This is a comment\n// And another\n// And some whitespace", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 5); /* end of file reached */ +} + +static void test_display_string_comment_with_blank_line() { + int lines; + int result = rc_richpresence_size_lines("Display:\n// This is a comment\n// And another\n\n// And some whitespace", &lines); + ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); + ASSERT_NUM_EQUALS(lines, 4); /* line 4 was blank */ +} + +void test_richpresence(void) { + TEST_SUITE_BEGIN(); + + TEST(test_empty_script); + + /* static display string */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nHello, world!", "Hello, world!"); + + /* static display string with trailing whitespace */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat ", "What "); + + /* static display string whitespace only*/ + TEST_PARAMS2(test_simple_richpresence, "Display:\n ", " "); + + /* static display string with comment (trailing whitespace will be trimmed) */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat // Where", "What"); + + /* static display string with escaped comment */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\// Where", "What // Where"); + + /* static display string with escaped backslash */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\\\ Where", "What \\ Where"); + + /* static display string with partially escaped comment */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\/// Where", "What /"); + + /* static display string with trailing backslash (backslash will be ignored) */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\", "What "); + + /* static display string with trailing text */ + TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat\n\nWhere", "What"); + + /* buffer boundary */ + test_buffer_boundary(); + + /* condition display */ + TEST(test_conditional_display_simple); + TEST(test_conditional_display_after_default); + TEST(test_conditional_display_no_default); + TEST(test_conditional_display_common_condition); + TEST(test_conditional_display_duplicated_condition); + TEST(test_conditional_display_invalid_condition_logic); + TEST(test_conditional_display_shared_lookup); + TEST(test_conditional_display_whitespace_text); + TEST(test_conditional_display_indirect); + TEST(test_conditional_display_unnecessary_measured); + TEST(test_conditional_display_unnecessary_measured_indirect); + TEST(test_conditional_display_invalid); + + /* value macros */ + TEST(test_macro_value); + TEST(test_macro_value_nibble); + TEST(test_macro_value_bcd); + TEST(test_macro_value_bitcount); + TEST(test_macro_value_adjusted_negative); + TEST(test_macro_value_from_formula); + TEST(test_macro_value_from_hits); + TEST(test_macro_value_from_indirect); + TEST(test_macro_value_divide_by_zero); + TEST(test_macro_value_divide_by_self); + TEST(test_macro_value_remember_recall); + TEST(test_macro_value_invalid); + TEST(test_macro_value_measured_if); + TEST(test_multiple_macros); + + /* hundreds macro */ + TEST(test_macro_hundreds); + + /* frames macros */ + TEST(test_macro_frames); + + /* float macros */ + TEST_PARAMS3(test_macro_float, "VALUE", 0x429A4492, "77"); /* 77.133926 */ + TEST_PARAMS3(test_macro_float, "FLOAT1", 0x429A4492, "77.1"); + TEST_PARAMS3(test_macro_float, "FLOAT2", 0x429A4492, "77.13"); + TEST_PARAMS3(test_macro_float, "FLOAT3", 0x429A4492, "77.134"); /* rounded up */ + TEST_PARAMS3(test_macro_float, "FLOAT4", 0x429A4492, "77.1339"); + TEST_PARAMS3(test_macro_float, "FLOAT5", 0x429A4492, "77.13393"); /* rounded up */ + TEST_PARAMS3(test_macro_float, "FLOAT6", 0x429A4492, "77.133926"); + TEST_PARAMS3(test_macro_float, "VALUE", 0xC0000000, "-2"); /* -2.0 */ + TEST_PARAMS3(test_macro_float, "FLOAT1", 0xC0000000, "-2.0"); + TEST_PARAMS3(test_macro_float, "FLOAT6", 0xC0000000, "-2.000000"); + TEST_PARAMS3(test_macro_float, "SECS", 0x429A4492, "1:17"); /* 77.133926 */ + + /* lookup macros */ + TEST(test_macro_lookup_simple); + TEST(test_macro_lookup_with_inline_comment); + TEST(test_macro_lookup_hex_keys); + TEST(test_macro_lookup_default); + TEST(test_macro_lookup_crlf); + TEST(test_macro_lookup_after_display); + TEST(test_macro_lookup_from_formula); + TEST(test_macro_lookup_from_indirect); + TEST(test_macro_lookup_repeated); + TEST(test_macro_lookup_shared); + TEST(test_macro_lookup_multiple); + TEST(test_macro_lookup_and_value); + TEST(test_macro_lookup_negative_value); + TEST(test_macro_lookup_value_with_whitespace); + TEST(test_macro_lookup_mapping_repeated); + TEST(test_macro_lookup_mapping_repeated_csv); + TEST(test_macro_lookup_mapping_merged); + TEST(test_macro_lookup_mapping_range); + TEST(test_macro_lookup_mapping_range_overlap); + TEST(test_macro_lookup_invalid); + + /* escaped macro */ + TEST(test_macro_escaped); + + /* macro errors */ + TEST(test_macro_undefined); + TEST(test_macro_undefined_at_end_of_line); + TEST(test_macro_unterminated); + TEST(test_macro_without_parameter); + TEST(test_macro_without_parameter_conditional_display); + TEST(test_macro_non_numeric_parameter); + TEST(test_macro_mathematic_chain); + + /* builtin macros */ + TEST_PARAMS2(test_builtin_macro, "Number", "12,345"); + TEST_PARAMS2(test_builtin_macro, "Score", "012345"); + TEST_PARAMS2(test_builtin_macro, "Centiseconds", "2:03.45"); + TEST_PARAMS2(test_builtin_macro, "Seconds", "3h25:45"); + TEST_PARAMS2(test_builtin_macro, "Minutes", "205h45"); + TEST_PARAMS2(test_builtin_macro, "SecondsAsMinutes", "3h25"); + TEST_PARAMS2(test_builtin_macro, "ASCIIChar", "?"); /* 0x3039 is not a single ASCII char */ + TEST_PARAMS2(test_builtin_macro, "UnicodeChar", "\xe3\x80\xb9"); + TEST_PARAMS2(test_builtin_macro_float, "Float1", "77.1"); + TEST_PARAMS2(test_builtin_macro_float, "Float2", "77.13"); + TEST_PARAMS2(test_builtin_macro_float, "Float3", "77.134"); + TEST_PARAMS2(test_builtin_macro_float, "Float4", "77.1339"); + TEST_PARAMS2(test_builtin_macro_float, "Float5", "77.13393"); + TEST_PARAMS2(test_builtin_macro_float, "Float6", "77.133926"); + TEST_PARAMS2(test_builtin_macro, "Fixed1", "1,234.5"); + TEST_PARAMS2(test_builtin_macro, "Fixed2", "123.45"); + TEST_PARAMS2(test_builtin_macro, "Fixed3", "12.345"); + TEST_PARAMS2(test_builtin_macro, "Unsigned", "12,345"); + TEST_PARAMS2(test_builtin_macro, "Unformatted", "12345"); + TEST(test_builtin_macro_unsigned_large); + TEST(test_builtin_macro_override); + TEST(test_unformatted_legacy); + + /* asciichar */ + TEST(test_asciichar); + TEST_PARAMS9(test_ascii8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ""); + TEST_PARAMS9(test_ascii8, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, "ABCDEFGH"); + TEST_PARAMS9(test_ascii8, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, "Test"); + TEST_PARAMS9(test_ascii8, 0x54, 0x65, 0x73, 0x74, 0x00, 0x46, 0x6F, 0x6F, "Test"); + TEST_PARAMS9(test_ascii8, 0x00, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, ""); + TEST_PARAMS9(test_ascii8, 0x31, 0x7E, 0x32, 0x7F, 0x33, 0x20, 0x34, 0x5E, "1~2?3 4^"); /* 7F out of range */ + TEST_PARAMS9(test_ascii8, 0x54, 0x61, 0x62, 0x09, 0x31, 0x0D, 0x0E, 0x00, "Tab?1??"); /* control characters */ + + /* unicodechar */ + TEST_PARAMS5(test_unicode4, 0x0000, 0x0000, 0x0000, 0x0000, ""); + TEST_PARAMS5(test_unicode4, 0x0054, 0x0065, 0x0073, 0x0074, "Test"); + TEST_PARAMS5(test_unicode4, 0x0000, 0x0065, 0x0073, 0x0074, ""); + TEST_PARAMS5(test_unicode4, 0x00A9, 0x0031, 0x0032, 0x0033, "\xc2\xa9\x31\x32\x33"); /* two-byte unicode char */ + TEST_PARAMS5(test_unicode4, 0x2260, 0x0020, 0x0040, 0x0040, "\xe2\x89\xa0 @@"); /* three-byte unicode char */ + TEST_PARAMS5(test_unicode4, 0xD83D, 0xDEB6, 0x005F, 0x007A, "\xef\xbf\xbd\xef\xbf\xbd_z"); /* four-byte unicode pair */ + TEST_PARAMS5(test_unicode4, 0x0009, 0x003E, 0x000D, 0x000A, "\xef\xbf\xbd>\xef\xbf\xbd\xef\xbf\xbd"); /* control characters */ + + /* comments */ + TEST(test_random_text_between_sections); /* before official comments extra text was ignored, so was occassionally used to comment */ + TEST(test_comments); + TEST(test_comments_between_lines); + TEST(test_display_string_comment_only); + TEST(test_display_string_comment_with_blank_line); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_runtime.c b/src/rcheevos/test/rcheevos/test_runtime.c new file mode 100644 index 0000000000..e5edb8f5c0 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_runtime.c @@ -0,0 +1,1667 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "mock_memory.h" + +#include "../test_framework.h" + +static rc_runtime_event_t events[16]; +static int event_count = 0; + +static void event_handler(const rc_runtime_event_t* e) +{ + memcpy(&events[event_count++], e, sizeof(rc_runtime_event_t)); +} + +static void _assert_event(uint8_t type, uint32_t id, int32_t value) +{ + int i; + + for (i = 0; i < event_count; ++i) { + if (events[i].id == id && events[i].type == type && events[i].value == value) + return; + } + + ASSERT_FAIL("expected event not found"); +} +#define assert_event(type, id, value) ASSERT_HELPER(_assert_event(type, id, value), "assert_event") + +static void _assert_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr) +{ + int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") + +static void _assert_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr) +{ + int result = rc_runtime_activate_lboard(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_lboard(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_lboard(runtime, id, memaddr), "assert_activate_lboard") + +static void _assert_activate_richpresence(rc_runtime_t* runtime, const char* script) +{ + int result = rc_runtime_activate_richpresence(runtime, script, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_richpresence(runtime, script) ASSERT_HELPER(_assert_activate_richpresence(runtime, script), "assert_activate_richpresence") + +static void assert_do_frame(rc_runtime_t* runtime, memory_t* memory) +{ + event_count = 0; + rc_runtime_do_frame(runtime, event_handler, peek, memory, NULL); +} + +static void _assert_richpresence_display_string(rc_runtime_t* runtime, memory_t* memory, const char* expected) +{ + char buffer[512]; + const int expected_len = (int)strlen(expected); + const int result = rc_runtime_get_richpresence(runtime, buffer, sizeof(buffer), peek, memory, NULL); + ASSERT_STR_EQUALS(buffer, expected); + ASSERT_NUM_EQUALS(result, expected_len); +} +#define assert_richpresence_display_string(runtime, memory, expected) ASSERT_HELPER(_assert_richpresence_display_string(runtime, memory, expected), "assert_richpresence_display_string") + +static void test_two_achievements_activate_and_trigger(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + + /* both achievements are true, should remain in waiting state */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both achievements are false, should activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* second achievement is true, should trigger */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + /* first achievement is true, should trigger. second is already triggered */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* reset second achievement, should go back to WAITING and stay there */ + rc_reset_trigger(runtime.triggers[1].trigger); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both achievements are false again. second should active, first should be ignored */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_deactivate_achievements(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + + /* both achievements are true, should remain in waiting state */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* deactivate the first. */ + rc_runtime_deactivate_achievement(&runtime, 1); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* both achievements are false, deactivated one should not activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* both achievements are true, deactivated one should not trigger */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + /* reactivate achievement. */ + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* reactivated achievement is waiting and should not trigger */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both achievements are false. first should activate, second should be ignored */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_achievement_measured(void) +{ + /* bytes 3-7 are the float value for 16*pi */ + uint8_t ram[] = { 0, 10, 10, 0xDB, 0x0F, 0x49, 0x41 }; + char buffer[32]; + memory_t memory; + rc_runtime_t runtime; + uint32_t value, target; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* use equality so we can test values greater than the target */ + assert_activate_achievement(&runtime, 1, "0xH0002==10"); + assert_activate_achievement(&runtime, 2, "M:0xH0002==10"); + assert_activate_achievement(&runtime, 3, "G:0xH0002==10"); + assert_activate_achievement(&runtime, 4, "M:fF0003==f12.56637"); + + /* achievements are true, should remain in waiting state with no measured value */ + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0/10"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0%"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 12); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0/12"); + ASSERT_FALSE(rc_runtime_get_achievement_measured(&runtime, 5, &value, &target)); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 5, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + + /* achievements are false, should activate */ + ram[2] = 9; + ram[6] = 0x40; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 9); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "9/10"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 9); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "90%"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 3); + ASSERT_NUM_EQUALS(target, 12); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "3/12"); /* captured measured value and target are integers, so 3.14/12.56 -> 3/12 */ + + /* value greater than target (i.e. "6 >= 5" should report maximum "5/5" or "100%" */ + ram[2] = 12; + ram[6] = 0x42; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 12); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "10/10"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 12); + ASSERT_NUM_EQUALS(target, 10); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "100%"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 50); + ASSERT_NUM_EQUALS(target, 12); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "12/12"); + + /* achievements are true, should trigger - triggered achievement is not measurable */ + ram[2] = 10; + ram[6] = 0x41; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + + rc_runtime_destroy(&runtime); +} + +static void test_achievement_measured_maxint(void) +{ + uint8_t ram[] = { 0xFF, 0xFF, 0xFF, 0xFF }; + char buffer[32]; + memory_t memory; + rc_runtime_t runtime; + uint32_t value, target; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 2, "M:0xX0000==hFFFFFFFF"); + assert_activate_achievement(&runtime, 3, "G:0xX0000==hFFFFFFFF"); + + /* achievements are true, should remain in waiting state */ + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0/4294967295"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "0%"); + + /* achievements are false (value fits in 31-bits), should activate */ + ram[1] = ram[3] = 0x7F; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0x7FFF7FFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "2147450879/4294967295"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0x7FFF7FFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "49%"); + + /* achievements are false (value requires 32-bits) */ + ram[1] = ram[3] = 0xFE; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0xFEFFFEFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "4278189823/4294967295"); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0xFEFFFEFF); + ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); + ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, "99%"); + + /* achievements are true, should trigger - triggered achievement is not measurable */ + ram[1] = ram[3] = 0xFF; + assert_do_frame(&runtime, &memory); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); + ASSERT_NUM_EQUALS(value, 0); + ASSERT_NUM_EQUALS(target, 0); + ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); + ASSERT_STR_EQUALS(buffer, ""); + + rc_runtime_destroy(&runtime); +} + +static void test_two_achievements_differing_resets_in_alts(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10S1=1SR:0xH0000!=0"); + assert_activate_achievement(&runtime, 2, "0xH0001=10S1=1SR:0xH0000!=1"); + + /* first achievement true (stays waiting), second not true because of reset */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* both achievements are false, should activate */ + ram[1] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* first should fire, second prevented by reset */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* second can fire, reset first which will be activated due to reset */ + ram[0] = 1; + rc_reset_trigger(runtime.triggers[0].trigger); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + /* both achievements are false again. second should active, first should be ignored */ + ram[0] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_shared_memref(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + rc_memref_t* memref1; + rc_memref_t* memref2; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0001=12"); + + memref1 = runtime.triggers[0].trigger->requirement->conditions->operand1.value.memref; + memref2 = runtime.triggers[1].trigger->requirement->conditions->operand1.value.memref; + ASSERT_PTR_EQUALS(memref1, memref2); + + /* first is true, should remain waiting. second should activate */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* deactivate second one. it doesn't have any unique memrefs, so can be free'd */ + rc_runtime_deactivate_achievement(&runtime, 2); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_PTR_NOT_NULL(runtime.triggers[0].trigger); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* second is true, but no longer in runtime. first should activate, expect nothing from second */ + ram[1] = 12; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* first is true and should trigger */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* reactivate achievement. old definition was free'd so should be recreated */ + assert_activate_achievement(&runtime, 2, "0xH0001=12"); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* reactivated achievement is waiting and false. should activate */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); + + /* deactivate first achievement. */ + rc_runtime_deactivate_achievement(&runtime, 1); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* second achievement is true. should trigger using memrefs from first */ + ram[1] = 12; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_replace_active_trigger(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 1, "0xH0002=10"); + + /* both are true, but first should have been overwritten by second */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both are false. only second should be getting processed, expect single event */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* first is true, but should not trigger */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second is true and should trigger */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* switch back to original definition. */ + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + + rc_runtime_destroy(&runtime); +} + +static rc_runtime_t* discarding_event_handler_runtime = NULL; +static void discarding_event_handler(const rc_runtime_event_t* e) +{ + event_handler(e); + rc_runtime_deactivate_achievement(discarding_event_handler_runtime, e->id); +} + +static void test_trigger_deactivation(void) +{ + uint8_t ram[] = { 0, 9, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* three identical achievements that should trigger when $1 changes from 9 to 10 */ + assert_activate_achievement(&runtime, 1, "0xH0001=10_d0xH0001=9"); + assert_activate_achievement(&runtime, 2, "0xH0001=10_d0xH0001=9"); + assert_activate_achievement(&runtime, 3, "0xH0001=10_d0xH0001=9"); + + /* prep the delta and make sure the achievements are active */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* trigger all three */ + ram[1] = 10; + event_count = 0; + discarding_event_handler_runtime = &runtime; + rc_runtime_do_frame(&runtime, discarding_event_handler, peek, &memory, NULL); + discarding_event_handler_runtime = NULL; + + ASSERT_NUM_EQUALS(event_count, 3); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 3, 0); + + /* triggers are no longer active and should have been removed from the runtime */ + ASSERT_NUM_EQUALS(runtime.trigger_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_trigger_with_resetif() { + uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* never(byte(3)==1) && once(byte(4)==1) && trigger_when(byte(0)==1) */ + assert_activate_achievement(&runtime, 1, "R:0xH0003=1_0xH0004=1.1._T:0xH0000=1"); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* non-trigger condition is true */ + ram[4] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* ResetIf is true */ + ram[3] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + + /* ResetIf no longer true */ + ram[3] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_trigger_with_resetnextif() { + uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* once(byte(4)==1 && never(repeated(2, byte(3)==1 && never(byte(1)==1 || byte(2)==1))) && trigger_when(byte(0)==1) */ + assert_activate_achievement(&runtime, 1, "O:0xH0001=1_Z:0xH0002=1_Z:0xH0003=1.2._0xH0004=1.1._T:0xH0000=1"); + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.trigger_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* non-trigger condition is true */ + ram[4] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* second ResetNextIf is true */ + ram[3] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* OrNext resets second ResetNextIf */ + ram[1] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + + /* OrNext no longer true */ + ram[1] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second ResetNextIf fires */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_reset_event(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + rc_condition_t* cond; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10.2._R:0xH0002=10"); + cond = runtime.triggers[0].trigger->requirement->conditions; + + /* reset is true, so achievement is false and should activate, but not notify reset */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 0); + + /* reset is still true, but since no hits were accumulated there shouldn't be a reset event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* reset is not true, hits should increment */ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 1); + + /* reset is true. hits will reset. expect event */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 0); + + /* reset is still true, but since hits were previously reset there shouldn't be a reset event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* reset is not true, hits should increment */ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 1); + + /* reset is not true, hits should increment, causing achievement to trigger */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 2); + + /* reset is true, but hits shouldn't reset as achievement is no longer active */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(cond->current_hits, 2); + + rc_runtime_destroy(&runtime); +} + +static void test_paused_event(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=10.2._P:0xH0002=10"); + + /* pause is true, so achievement is false and should activate, but only notify pause */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); + + /* pause is still true, but previously paused, so no event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* pause is not true, expect activate event */ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* pause is true. expect event */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); + + /* pause is still true, but previously paused, so no event */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* pause is not true, expect trigger*/ + ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* pause is true, but shouldn't notify as achievement is no longer active */ + ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_primed_event(void) +{ + uint8_t ram[] = { 0, 1, 0, 1, 0, 0 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(0)==1 && trigger(byte(1)==1) && byte(2)==1 && trigger(byte(3)==1) && unless(byte(4)==1) && never(byte(5) == 1) */ + assert_activate_achievement(&runtime, 1, "0xH0000=1_T:0xH0001=1_0xH0002=1_T:0xH0003=1_P:0xH0004=1_R:0xH0005=1"); + + /* trigger conditions are true, but nothing else */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); + + /* primed */ + ram[1] = ram[3] = 0; + ram[0] = ram[2] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* no longer primed */ + ram[0] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + + /* primed */ + ram[0] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* paused */ + ram[4] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); + + /* unpaused */ + ram[4] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* reset */ + ram[5] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); + + /* not reset */ + ram[5] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); + + /* all conditions are true */ + ram[1] = ram[3] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_progress_event(void) +{ + uint8_t ram[] = { 0, 1 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* measured(byte(0x0001) >= 10) */ + assert_activate_achievement(&runtime, 1, "M:0xH0001>=10"); + runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* should not receive notification when initialized first measured value */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased */ + ram[1] = 2; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 2); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* decreased */ + ram[1] = 0; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 0); + + /* increased */ + ram[1] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 9); + + /* triggered. should not receive change event */ + ram[1] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* no longer active */ + ram[1] = 11; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_progress_event_as_percent(void) +{ + uint8_t ram[] = { 0, 1 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* measured(byte(0x0001) >= 200, format='percent') */ + assert_activate_achievement(&runtime, 1, "G:0xH0001>=200"); + runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* should not receive notification when initialized first measured value */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased (0% -> 1%) */ + ram[1] = 2; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 1); + + /* unchanged */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased (1% -> 1%, no event) */ + ram[1] = 3; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increased (1% -> 2%) */ + ram[1] = 4; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 2); + + /* decreased */ + ram[1] = 1; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 0); + + /* increased */ + ram[1] = 199; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 99); + + /* triggered. should not receive change event */ + ram[1] = 200; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); + + /* no longer active */ + ram[1] = 40; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_lboard(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0000"); + assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0000*2"); + + /* both start conditions are true, leaderboards will not be active */ + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are false, leaderboards will activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are true, leaderboards will start */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 1, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 2, 4); + + /* start condition no longer true, leaderboard should continue processing */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* value changed */ + ram[0] = 3; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_UPDATED, 1, 3); + assert_event(RC_RUNTIME_EVENT_LBOARD_UPDATED, 2, 6); + + /* value changed; first leaderboard submit, second canceled - expect events for submit and cancel, none for update */ + ram[0] = 4; + ram[1] = 11; + ram[2] = 12; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_TRIGGERED, 1, 4); + assert_event(RC_RUNTIME_EVENT_LBOARD_CANCELED, 2, 0); + + /* both start conditions are true, leaderboards will not be active */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_CANCELED); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are false, leaderboards will re-activate */ + ram[1] = ram[2] = 9; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event_count, 0); + + /* both start conditions are true, leaderboards will start */ + ram[1] = ram[2] = 10; + assert_do_frame(&runtime, &memory); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(event_count, 2); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 1, 4); + assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 2, 8); + + rc_runtime_destroy(&runtime); +} + +static void test_format_lboard_value(int format, int value, const char* expected) { + char buffer[64]; + int result; + + result = rc_runtime_format_lboard_value(buffer, sizeof(buffer), value, format); + ASSERT_STR_EQUALS(buffer, expected); + ASSERT_NUM_EQUALS(result, strlen(expected)); +} + +static void test_richpresence(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* initial value */ + assert_richpresence_display_string(&runtime, &memory, ""); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\nScore is @Points(0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "Score is 0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2,570 Points"); + + /* calling rc_runtime_get_richpresence without calling rc_runtime_do_frame should return the same string as memrefs aren't updated */ + ram[1] = 20; + assert_richpresence_display_string(&runtime, &memory, "Score is 2,570 Points"); + + /* call rc_runtime_do_frame to update memrefs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_starts_with_macro(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,570 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_macro_only(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001)"); + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,570"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_conditional(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0000=2?@Points(0x 0001) points\nScore is @Points(0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "Score is 0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,570 points"); + + /* update display string */ + ram[0] = 0; + ram[1] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_conditional_with_hits(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0000=1.2.?Score is @Points(0x 0001) Points\n@Points(0x 0001) points"); + assert_richpresence_display_string(&runtime, &memory, "0 points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,570 points"); + + /* one hit is not enough to switch display strings, but the memref does get updated */ + ram[0] = 1; + ram[1] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,580 points"); + + /* second hit is enough */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); + + /* no more hits are accumulated */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); + + /* same test without intermediary evaluation of display string */ + rc_runtime_reset(&runtime); + ram[0] = 2; + ram[1] = 30; + assert_do_frame(&runtime, &memory); /* no hits */ + + ram[0] = 1; + assert_do_frame(&runtime, &memory); /* one hit */ + assert_do_frame(&runtime, &memory); /* two hits */ + assert_richpresence_display_string(&runtime, &memory, "Score is 2,590 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_conditional_with_hits_after_match(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0002=10?It's @Points(0x 0001)\n?0xH0000=1.2.?Score is @Points(0x 0001) Points\n@Points(0x 0001) points"); + assert_richpresence_display_string(&runtime, &memory, "0 points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "It's 2,570"); + + /* first condition is true, but one hit should still be tallied on the second conditional */ + ram[0] = 1; + ram[1] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "It's 2,580"); + + /* first conditio no longer true, second condtion will get it's second hit, which is enough */ + ram[2] = 20; + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Score is 5,140 Points"); + + /* same test without intermediary evaluation of display string */ + rc_runtime_reset(&runtime); + ram[0] = 2; + ram[1] = 10; + ram[2] = 10; + assert_do_frame(&runtime, &memory); /* no hits */ + + ram[0] = 1; + ram[1] = 20; + assert_do_frame(&runtime, &memory); /* one hit */ + ram[2] = 20; + assert_do_frame(&runtime, &memory); /* two hits */ + assert_richpresence_display_string(&runtime, &memory, "Score is 5,140 Points"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_reload(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,570 Points"); + + /* reloading should generate display string with current memrefs */ + ram[1] = 20; + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Bananas"); + assert_richpresence_display_string(&runtime, &memory, "2,570 Bananas"); + + /* first frame after reloading should update display string */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,580 Bananas"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_reload_addaddress(void) +{ + /* ram[1] must be non-zero */ + uint8_t ram[] = { 1, 10, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(I:0xH0000_M:0x 0001) Points"); + assert_richpresence_display_string(&runtime, &memory, "0 Points"); + + /* first frame should update display string with correct memrfs */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,570 Points"); + + /* reloading should generate display string with current memrefs */ + /* the entire AddAddress expression will be a single variable, which will have a current value. */ + ram[2] = 20; + assert_activate_richpresence(&runtime, + "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(I:0xH0000_M:0x 0001) Bananas"); + assert_richpresence_display_string(&runtime, &memory, "2,570 Bananas"); + + /* first frame after reloading should update display string */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "2,580 Bananas"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_static(void) +{ + uint8_t ram[] = { 2, 10, 10 }; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_richpresence(&runtime, "Display:\nHello, world!"); + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Hello, world!"); + + /* first frame won't affect the display string */ + assert_do_frame(&runtime, &memory); + assert_richpresence_display_string(&runtime, &memory, "Hello, world!"); + + rc_runtime_destroy(&runtime); +} + +static void test_richpresence_addsource_chain(void) +{ + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + /* large number of AddSources will exceed the runtime buffer of 32 memrefs and 16 modified memrefs */ + assert_activate_richpresence(&runtime, + "Display:\n" + "@Number(0xH0000_0xH0001_0xH0002_0xH0003_0xH0004_0xH0005_0xH0006_0xH0007_" + "0xH0010_0xH0011_0xH0012_0xH0013_0xH0014_0xH0015_0xH0016_0xH0017_" + "0xH0020_0xH0021_0xH0022_0xH0023_0xH0024_0xH0025_0xH0026_0xH0027_" + "0xH0030_0xH0031_0xH0032_0xH0033_0xH0034_0xH0035_0xH0036_0xH0037_" + "0xH0040_0xH0041_0xH0042_0xH0043)\n"); + + rc_runtime_destroy(&runtime); +} + +typedef struct { + memory_t memory; + rc_runtime_t* runtime; + uint32_t invalid_address; +} +memory_invalid_t; + +static uint32_t peek_invalid(uint32_t address, uint32_t num_bytes, void* ud) +{ + memory_invalid_t* memory = (memory_invalid_t*)ud; + if (memory->invalid_address != address) + return peek(address, num_bytes, &memory->memory); + + rc_runtime_invalidate_address(memory->runtime, address); + return 0; +} + +static void assert_do_frame_invalid(rc_runtime_t* runtime, memory_invalid_t* memory, uint32_t invalid_address) +{ + event_count = 0; + memory->runtime = runtime; + memory->invalid_address = invalid_address; + rc_runtime_do_frame(runtime, event_handler, peek_invalid, memory, NULL); +} + +static void test_invalidate_address(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_invalid_t memory; + rc_runtime_t runtime; + + memory.memory.ram = ram; + memory.memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + + /* achievements should start in waiting state */ + assert_do_frame_invalid(&runtime, &memory, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* nothing depends on address 3 */ + assert_do_frame_invalid(&runtime, &memory, 3); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* second achievement depends on address 2 */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 2); + + /* second achievement already disabled, don't raise event again */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + + rc_runtime_destroy(&runtime); +} + +static void test_invalidate_address_no_memrefs(void) +{ + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + /* simple test to ensure a null reference doesn't occur when no memrefs are present */ + rc_runtime_invalidate_address(&runtime, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_invalidate_address_shared_memref(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_invalid_t memory; + rc_runtime_t runtime; + + memory.memory.ram = ram; + memory.memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0002=10"); + assert_activate_achievement(&runtime, 3, "0xH0001=10S0xH0002=10S0xH0003=10"); + + /* achievements should start in waiting state */ + assert_do_frame_invalid(&runtime, &memory, 0); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); + + /* second and third achievements depend on address 2 */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_DISABLED); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 2); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 3, 2); + + rc_runtime_destroy(&runtime); +} + +static void test_invalidate_address_leaderboard(void) +{ + uint8_t ram[] = { 0, 10, 10 }; + memory_invalid_t memory; + rc_runtime_t runtime; + + memory.memory.ram = ram; + memory.memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0001"); + assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0002*2"); + + /* leaderboards should start in waiting state */ + assert_do_frame_invalid(&runtime, &memory, 0); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second leaderboard depends on address 2 */ + assert_do_frame_invalid(&runtime, &memory, 2); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_DISABLED); + assert_event(RC_RUNTIME_EVENT_LBOARD_DISABLED, 2, 2); + + rc_runtime_destroy(&runtime); +} + +static int validate_address_handler(uint32_t address) +{ + return (address & 1) == 0; /* all even addresses are valid */ +} + +static void test_validate_addresses(void) +{ + rc_runtime_t runtime; + + rc_runtime_init(&runtime); + event_count = 0; + + assert_activate_achievement(&runtime, 1, "0xH0001=10"); + assert_activate_achievement(&runtime, 2, "0xH0003=10"); /* put two invalid memrefs next to each other */ + assert_activate_achievement(&runtime, 3, "0xH0002=10"); + assert_activate_achievement(&runtime, 4, "0xH0001=10"); /* shared reference to invalid memref */ + assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0001"); + assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0002*2"); + + /* everything should start in waiting state */ + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[3].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + + /* validate_addresses should immediately disable the achievements and raise the event */ + rc_runtime_validate_addresses(&runtime, event_handler, validate_address_handler); + ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(runtime.triggers[3].trigger->state, RC_TRIGGER_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_DISABLED); + ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); + + ASSERT_NUM_EQUALS(event_count, 4); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 1, 1); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 3); + assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 4, 1); + assert_event(RC_RUNTIME_EVENT_LBOARD_DISABLED, 1, 1); + + rc_runtime_destroy(&runtime); +} + +void test_runtime(void) { + TEST_SUITE_BEGIN(); + + /* achievements */ + TEST(test_two_achievements_activate_and_trigger); + TEST(test_deactivate_achievements); + TEST(test_achievement_measured); + TEST(test_achievement_measured_maxint); + TEST(test_two_achievements_differing_resets_in_alts); + + TEST(test_shared_memref); + TEST(test_replace_active_trigger); + TEST(test_trigger_deactivation); + TEST(test_trigger_with_resetif); + TEST(test_trigger_with_resetnextif); + + /* achievement events */ + TEST(test_reset_event); + TEST(test_paused_event); + TEST(test_primed_event); + TEST(test_progress_event); + TEST(test_progress_event_as_percent); + + /* leaderboards */ + TEST(test_lboard); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, 12345, "12,345"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, -12345, "-12,345"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, 0xFFFFFFFF, "-1"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_SCORE, 12345, "012345"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_SECONDS, 345, "5:45"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_CENTISECS, 12345, "2:03.45"); + TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_FRAMES, 12345, "3:25.75"); + + /* rich presence */ + TEST(test_richpresence); + TEST(test_richpresence_starts_with_macro); + TEST(test_richpresence_macro_only); + TEST(test_richpresence_conditional); + TEST(test_richpresence_conditional_with_hits); + TEST(test_richpresence_conditional_with_hits_after_match); + TEST(test_richpresence_reload); + TEST(test_richpresence_reload_addaddress); + TEST(test_richpresence_static); + TEST(test_richpresence_addsource_chain); + + /* invalidate address */ + TEST(test_invalidate_address); + TEST(test_invalidate_address_no_memrefs); + TEST(test_invalidate_address_shared_memref); + TEST(test_invalidate_address_leaderboard); + + TEST(test_validate_addresses); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_runtime_progress.c b/src/rcheevos/test/rcheevos/test_runtime_progress.c new file mode 100644 index 0000000000..8303aa291c --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_runtime_progress.c @@ -0,0 +1,1821 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "../test_framework.h" +#include "../rhash/md5.h" +#include "mock_memory.h" + +static void _assert_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr) +{ + int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") + +static void _assert_activate_leaderboard(rc_runtime_t* runtime, uint32_t id, const char* script) +{ + int result = rc_runtime_activate_lboard(runtime, id, script, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_leaderboard(runtime, id, script) ASSERT_HELPER(_assert_activate_leaderboard(runtime, id, script), "assert_activate_leaderboard") + +static void _assert_activate_rich_presence(rc_runtime_t* runtime, const char* script) +{ + int result = rc_runtime_activate_richpresence(runtime, script, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_rich_presence(runtime, script) ASSERT_HELPER(_assert_activate_rich_presence(runtime, script), "assert_activate_rich_presence") + +static void _assert_richpresence_output(rc_runtime_t* runtime, memory_t* memory, const char* expected_display_string) { + char output[256]; + int result; + + result = rc_runtime_get_richpresence(runtime, output, sizeof(output), peek, memory, NULL); + ASSERT_STR_EQUALS(output, expected_display_string); + ASSERT_NUM_EQUALS(result, strlen(expected_display_string)); +} +#define assert_richpresence_output(runtime, memory, expected_display_string) ASSERT_HELPER(_assert_richpresence_output(runtime, memory, expected_display_string), "assert_richpresence_output") + +static void event_handler(const rc_runtime_event_t* e) +{ + (void)e; +} + +static void assert_do_frame(rc_runtime_t* runtime, memory_t* memory) +{ + rc_runtime_do_frame(runtime, event_handler, peek, memory, NULL); +} + +static void _assert_serialize(rc_runtime_t* runtime, uint8_t* buffer, size_t buffer_size) +{ + int result; + unsigned* overflow; + + uint32_t size = rc_runtime_progress_size(runtime, NULL); + ASSERT_NUM_LESS(size, buffer_size); + + overflow = (unsigned*)(buffer + size); + *overflow = 0xCDCDCDCD; + + result = rc_runtime_serialize_progress(buffer, runtime, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_serialize(runtime, buffer, buffer_size) ASSERT_HELPER(_assert_serialize(runtime, buffer, buffer_size), "assert_serialize") + +static void _assert_deserialize(rc_runtime_t* runtime, uint8_t* buffer) +{ + int result = rc_runtime_deserialize_progress(runtime, buffer, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_deserialize(runtime, buffer) ASSERT_HELPER(_assert_deserialize(runtime, buffer), "assert_deserialize") + +static void _assert_sized_memref(rc_runtime_t* runtime, uint32_t address, uint8_t size, uint32_t value, uint32_t prev, uint32_t prior) +{ + rc_memref_list_t* memref_list = &runtime->memrefs->memrefs; + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + for (; memref < memref_end; ++memref) { + if (memref->address == address && memref->value.size == size) { + ASSERT_NUM_EQUALS(memref->value.value, value); + ASSERT_NUM_EQUALS(memref->value.prior, prior); + + if (value == prior) { + ASSERT_NUM_EQUALS(memref->value.changed, 0); + } + else { + ASSERT_NUM_EQUALS(memref->value.changed, (prev == prior) ? 1 : 0); + } + + return; + } + } + } + + ASSERT_FAIL("could not find memref for address %u", address); +} +#define assert_sized_memref(runtime, address, size, value, prev, prior) ASSERT_HELPER(_assert_sized_memref(runtime, address, size, value, prev, prior), "assert_sized_memref") +#define assert_memref(runtime, address, value, prev, prior) ASSERT_HELPER(_assert_sized_memref(runtime, address, RC_MEMSIZE_8_BITS, value, prev, prior), "assert_memref") + +static rc_trigger_t* find_trigger(rc_runtime_t* runtime, uint32_t ach_id) +{ + uint32_t i; + for (i = 0; i < runtime->trigger_count; ++i) { + if (runtime->triggers[i].id == ach_id && runtime->triggers[i].trigger) + return runtime->triggers[i].trigger; + } + + ASSERT_MESSAGE("could not find trigger for achievement %u", ach_id); + return NULL; +} + +static rc_condition_t* find_trigger_cond(rc_trigger_t* trigger, uint32_t group_idx, uint32_t cond_idx) +{ + rc_condset_t* condset; + rc_condition_t* cond; + + if (!trigger) + return NULL; + + condset = trigger->requirement; + if (group_idx > 0) { + condset = trigger->alternative; + while (condset && --group_idx != 0) + condset = condset->next; + } + + if (!condset) + return NULL; + + cond = condset->conditions; + while (cond && cond_idx > 0) { + --cond_idx; + cond = cond->next; + } + + return cond; +} + +static void _assert_hitcount(rc_runtime_t* runtime, uint32_t ach_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_hitcount(runtime, ach_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_hitcount(runtime, ach_id, group_idx, cond_idx, expected_hits), "assert_hitcount") + +static void _assert_cond_memref(rc_runtime_t* runtime, uint32_t ach_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_value, uint32_t expected_prior, uint8_t expected_changed) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.value, expected_value); + ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.prior, expected_prior); + ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.changed, expected_changed); +} +#define assert_cond_memref(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed) \ + ASSERT_HELPER(_assert_cond_memref(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed), "assert_cond_memref") + +static void _assert_cond_memref2(rc_runtime_t* runtime, uint32_t ach_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_value, uint32_t expected_prior, uint8_t expected_changed) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.value, expected_value); + ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.prior, expected_prior); + ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.changed, expected_changed); +} +#define assert_cond_memref2(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed) \ + ASSERT_HELPER(_assert_cond_memref2(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed), "assert_cond_memref2") + +static void _assert_achievement_state(rc_runtime_t* runtime, uint32_t ach_id, uint8_t state) +{ + rc_trigger_t* trigger = find_trigger(runtime, ach_id); + ASSERT_PTR_NOT_NULL(trigger); + + ASSERT_NUM_EQUALS(trigger->state, state); +} +#define assert_achievement_state(runtime, ach_id, state) ASSERT_HELPER(_assert_achievement_state(runtime, ach_id, state), "assert_achievement_state") + +static rc_lboard_t* find_lboard(rc_runtime_t* runtime, uint32_t lboard_id) +{ + uint32_t i; + for (i = 0; i < runtime->lboard_count; ++i) { + if (runtime->lboards[i].id == lboard_id && runtime->lboards[i].lboard) + return runtime->lboards[i].lboard; + } + + ASSERT_MESSAGE("could not find leaderboard %u", lboard_id); + return NULL; +} + +static void _assert_sta_hitcount(rc_runtime_t* runtime, uint32_t lboard_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) +{ + rc_lboard_t* lboard = find_lboard(runtime, lboard_id); + rc_condition_t* cond = find_trigger_cond(&lboard->start, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_sta_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_sta_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_sta_hitcount") + +static void _assert_sub_hitcount(rc_runtime_t* runtime, uint32_t lboard_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) +{ + rc_lboard_t* lboard = find_lboard(runtime, lboard_id); + rc_condition_t* cond = find_trigger_cond(&lboard->submit, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_sub_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_sub_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_sub_hitcount") + +static void _assert_can_hitcount(rc_runtime_t* runtime, uint32_t lboard_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) +{ + rc_lboard_t* lboard = find_lboard(runtime, lboard_id); + rc_condition_t* cond = find_trigger_cond(&lboard->cancel, group_idx, cond_idx); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); +} +#define assert_can_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_can_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_can_hitcount") + +static void update_md5(uint8_t* buffer) +{ + md5_state_t state; + + uint8_t* ptr = buffer; + while (ptr[0] != 'D' || ptr[1] != 'O' || ptr[2] != 'N' || ptr[3] != 'E') + ++ptr; + + ptr += 8; + + md5_init(&state); + md5_append(&state, buffer, (int)(ptr - buffer)); + md5_finish(&state, ptr); +} + +static void reset_runtime(rc_runtime_t* runtime) +{ + rc_memref_list_t* memref_list; + rc_memref_t* memref; + rc_trigger_t* trigger; + rc_condition_t* cond; + rc_condset_t* condset; + uint32_t i; + + for (memref_list = &runtime->memrefs->memrefs; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref_end; + memref = memref_list->items; + memref_end = memref + memref_list->count; + for (; memref < memref_end; ++memref) { + memref->value.value = 0xFF; + memref->value.changed = 0; + memref->value.prior = 0xFF; + } + } + + for (i = 0; i < runtime->trigger_count; ++i) { + trigger = runtime->triggers[i].trigger; + if (trigger) { + trigger->measured_value = 0xFF; + trigger->measured_target = 0xFF; + + if (trigger->requirement) { + cond = trigger->requirement->conditions; + while (cond) { + cond->current_hits = 0xFF; + cond = cond->next; + } + } + + condset = trigger->alternative; + while (condset) { + cond = condset->conditions; + while (cond) { + cond->current_hits = 0xFF; + cond = cond->next; + } + + condset = condset->next; + } + } + } +} + +static void test_empty() +{ + uint8_t buffer[2048]; + rc_runtime_t runtime; + + rc_runtime_init(&runtime); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + ASSERT_NUM_EQUALS(runtime.memrefs->memrefs.count, 0); + ASSERT_NUM_EQUALS(runtime.memrefs->modified_memrefs.count, 0); + ASSERT_NUM_EQUALS(runtime.trigger_count, 0); + ASSERT_NUM_EQUALS(runtime.lboard_count, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_single_achievement() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_invalid_marker() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* invalid header prevents anything from being deserialized */ + buffer[0] = 0x40; + update_md5(buffer); + + reset_runtime(&runtime); + ASSERT_NUM_EQUALS(rc_runtime_deserialize_progress(&runtime, buffer, NULL), RC_INVALID_STATE); + + assert_memref(&runtime, 1, 0xFF, 0xFF, 0xFF); + assert_memref(&runtime, 2, 0xFF, 0xFF, 0xFF); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_invalid_memref_chunk_id() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* invalid chunk is ignored, achievement hits will still be read */ + buffer[5] = 0x40; + update_md5(buffer); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 0xFF, 0xFF, 0xFF); + assert_memref(&runtime, 2, 0xFF, 0xFF, 0xFF); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_modified_data() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* this changes the current hits for the test condition to 6, but doesn't update the checksum, so should be ignored */ + ASSERT_NUM_EQUALS(buffer[84], 3); + buffer[84] = 6; + + reset_runtime(&runtime); + ASSERT_NUM_EQUALS(rc_runtime_deserialize_progress(&runtime, buffer, NULL), RC_INVALID_STATE); + + /* memrefs will have been processed and cannot be "reset" */ + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + + /* deserialization failure causes all hits to be reset */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_single_achievement_deactivated() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + + /* disabled achievement */ + rc_runtime_deactivate_achievement(&runtime, 1); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + + /* reactivate */ + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_WAITING); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_single_achievement_md5_changed() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + + /* new achievement definition - rack up a couple hits */ + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5.1."); + ram[1] = 3; + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_hitcount(&runtime, 1, 0, 0, 2); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_memref(&runtime, 1, 4, 4, 3); + + assert_deserialize(&runtime, buffer); + + /* memrefs should be restored */ + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + + /* achievement definition changed, achievement should be reset */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void setup_multiple_achievements(rc_runtime_t* runtime, memory_t* memory) +{ + rc_runtime_init(runtime); + + assert_activate_achievement(runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(runtime, 2, "0xH0002=7_0xH0000=2"); + assert_activate_achievement(runtime, 3, "0xH0003=9_0xH0000=3"); + assert_activate_achievement(runtime, 4, "0xH0004=1_0xH0000=4"); + assert_do_frame(runtime, memory); + memory->ram[1] = 4; + assert_do_frame(runtime, memory); + memory->ram[2] = 7; + assert_do_frame(runtime, memory); + memory->ram[3] = 9; + assert_do_frame(runtime, memory); + memory->ram[4] = 1; + assert_do_frame(runtime, memory); + + assert_memref(runtime, 0, 0, 0, 0); + assert_memref(runtime, 1, 4, 4, 1); + assert_memref(runtime, 2, 7, 7, 2); + assert_memref(runtime, 3, 9, 9, 3); + assert_memref(runtime, 4, 1, 4, 4); + assert_hitcount(runtime, 1, 0, 0, 4); + assert_hitcount(runtime, 2, 0, 0, 3); + assert_hitcount(runtime, 3, 0, 0, 2); + assert_hitcount(runtime, 4, 0, 0, 1); +} + +static void test_single_achievement_sized() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + uint32_t size; + int result; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + size = rc_runtime_progress_size(&runtime, NULL); + ASSERT_NUM_LESS(size, sizeof(buffer)); + + result = rc_runtime_serialize_progress_sized(buffer, size - 1, &runtime, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + result = rc_runtime_serialize_progress_sized(buffer, size, &runtime, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size - 1, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + assert_memref(&runtime, 1, 5, 5, 4); /* memrefs don't get reset on failure */ + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 16, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 0, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + assert_memref(&runtime, 1, 5, 5, 4); /* memrefs don't get reset on failure */ + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 0); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_empty_sized() +{ + uint8_t buffer[2048]; + rc_runtime_t runtime; + uint32_t size; + int result; + + rc_runtime_init(&runtime); + + size = rc_runtime_progress_size(&runtime, NULL); + ASSERT_NUM_LESS(size, sizeof(buffer)); + + result = rc_runtime_serialize_progress_sized(buffer, size - 1, &runtime, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + result = rc_runtime_serialize_progress_sized(buffer, size, &runtime, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size - 1, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 16, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 0, NULL); + ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); + + result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size, NULL); + ASSERT_NUM_EQUALS(result, RC_OK); + + rc_runtime_destroy(&runtime); +} + +static void test_no_core_group() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "S0xH0001=4_0xH0002=5"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 1, 0, 3); + assert_hitcount(&runtime, 1, 1, 1, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_hitcount(&runtime, 1, 1, 0, 3); + assert_hitcount(&runtime, 1, 1, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_shared_address() +{ + uint8_t ram[] = { 2, 3, 0, 0, 0 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0x 0001=5_0xX0001=6"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 6; + assert_do_frame(&runtime, &memory); + + assert_sized_memref(&runtime, 1, RC_MEMSIZE_8_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_16_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_32_BITS, 6, 5, 5); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 2); + assert_hitcount(&runtime, 1, 0, 2, 1); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_sized_memref(&runtime, 1, RC_MEMSIZE_8_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_16_BITS, 6, 5, 5); + assert_sized_memref(&runtime, 1, RC_MEMSIZE_32_BITS, 6, 5, 5); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 1, 0, 1, 2); + assert_hitcount(&runtime, 1, 0, 2, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_addsource() { + uint8_t ram[] = { 1, 2, 3, 4, 5 }; + uint8_t buffer1[512]; + uint8_t buffer2[512]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(2) + byte(1) == 5 - third condition just prevents the achievement from triggering*/ + assert_activate_achievement(&runtime, 1, "A:0xH0002_0xH0001=3_0xH0004=99"); + assert_do_frame(&runtime, &memory); /* $2 = 3, $1 = 2, 3 + 2 = 5 */ + ram[1] = 3; + ram[2] = 0; + assert_do_frame(&runtime, &memory); /* $2 = 0, $1 = 3, 0 + 3 = 3 */ + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + assert_serialize(&runtime, buffer1, sizeof(buffer1)); + + assert_do_frame(&runtime, &memory); + ram[1] = 6; + ram[2] = 1; + assert_do_frame(&runtime, &memory); /* $2 = 1, $1 = 6, 1 + 6 = 7 */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 1, 7, 3, 1); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer1); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 1, 7, 3, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_indirect() +{ + uint8_t ram[] = { 1, 2, 3, 4, 5 }; + uint8_t buffer1[512]; + uint8_t buffer2[512]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(byte(2) + 1) == 5 - third condition just prevents the achievement from triggering*/ + assert_activate_achievement(&runtime, 1, "I:0xH0002_0xH0001=3_0xH0004=99"); + assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5 */ + ram[1] = 3; + ram[2] = 0; + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3 */ + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + assert_serialize(&runtime, buffer1, sizeof(buffer1)); + + assert_do_frame(&runtime, &memory); + ram[1] = 6; + ram[2] = 1; + assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1 */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer1); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_indirect_delta() +{ + uint8_t ram[] = { 1, 2, 3, 4, 5 }; + uint8_t buffer1[512]; + uint8_t buffer2[512]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(byte(2) + 1) == 5 - third condition just prevents the achievement from triggering*/ + assert_activate_achievement(&runtime, 1, "I:0xH0002_d0xH0001=3_0xH0004=99"); + assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5, delta = 0 */ + ram[1] = 3; + ram[2] = 0; + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, delta = 5 */ + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, delta = 3 */ + + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 1); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + assert_serialize(&runtime, buffer1, sizeof(buffer1)); + + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, delta = 3 */ + ram[1] = 6; + ram[2] = 1; + assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, delta = 3 */ + assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, delta = 1 */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 3); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 0); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 0); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer1); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 1); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 3); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 0); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_memref_double_indirect() +{ + uint8_t ram[] = { 1, 2, 3, 4, 5 }; + uint8_t buffer1[512]; + uint8_t buffer2[512]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + /* byte(byte(2) + 1) == byte(byte(2)) - third condition just prevents the achievement from triggering*/ + assert_activate_achievement(&runtime, 1, "I:0xH0002_0xH0001=0xH0000_0xH0004=99"); + assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5, $(3+0) = 4 */ + ram[0] = 3; + ram[1] = 3; + ram[2] = 0; + assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, $(0+0) = 3 */ + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + assert_cond_memref2(&runtime, 1, 0, 1, 3, 4, 0); + + assert_serialize(&runtime, buffer1, sizeof(buffer1)); + + assert_do_frame(&runtime, &memory); + ram[1] = 6; + ram[2] = 1; + assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, $(1+0) = 6 */ + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + assert_cond_memref2(&runtime, 1, 0, 1, 6, 3, 1); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer1); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 4); + assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); + assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); + assert_cond_memref2(&runtime, 1, 0, 1, 3, 4, 0); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 1, 0, 1, 5); + assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); + assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); + assert_cond_memref2(&runtime, 1, 0, 1, 6, 3, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 0, 0, 0, 0); + assert_memref(&runtime, 1, 4, 4, 1); + assert_memref(&runtime, 2, 7, 7, 2); + assert_memref(&runtime, 3, 9, 9, 3); + assert_memref(&runtime, 4, 1, 4, 4); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 1, 0); + assert_hitcount(&runtime, 3, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 1, 0); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_ignore_triggered_and_inactive() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* trigger achievement 3 */ + ram[0] = 3; + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_TRIGGERED); + + /* reset achievement 2 to inactive */ + find_trigger(&runtime, 2)->state = RC_TRIGGER_STATE_INACTIVE; + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 0, 0, 0, 0); + assert_memref(&runtime, 1, 4, 4, 1); + assert_memref(&runtime, 2, 7, 7, 2); + assert_memref(&runtime, 3, 9, 9, 3); + assert_memref(&runtime, 4, 1, 4, 4); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_INACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_TRIGGERED); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 0xFF); /* inactive achievement should be ignored */ + assert_hitcount(&runtime, 2, 0, 1, 0xFF); + assert_hitcount(&runtime, 3, 0, 0, 0xFF); /* triggered achievement should be ignored */ + assert_hitcount(&runtime, 3, 0, 1, 0xFF); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_overwrite_waiting() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reset achievement 2 to waiting */ + rc_reset_trigger(find_trigger(&runtime, 2)); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 3); /* waiting achievement should be set back to active */ + assert_hitcount(&runtime, 2, 0, 1, 0); + assert_hitcount(&runtime, 3, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 1, 0); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_reactivate_waiting() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + setup_multiple_achievements(&runtime, &memory); + + /* reset achievement 2 to waiting */ + rc_reset_trigger(find_trigger(&runtime, 2)); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reactivate achievement 2 */ + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 4); + assert_hitcount(&runtime, 1, 0, 1, 0); + assert_hitcount(&runtime, 2, 0, 0, 0); /* active achievement should be set back to waiting */ + assert_hitcount(&runtime, 2, 0, 1, 0); + assert_hitcount(&runtime, 3, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 1, 0); + assert_hitcount(&runtime, 4, 0, 0, 1); + assert_hitcount(&runtime, 4, 0, 1, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_paused_and_primed() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + uint8_t buffer2[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0002=7_0xH0000=2_P:0xH0005=4"); + assert_activate_achievement(&runtime, 3, "0xH0003=9_0xH0000=3"); + assert_activate_achievement(&runtime, 4, "0xH0004=1_T:0xH0000=4"); + + assert_do_frame(&runtime, &memory); + ram[1] = 4; + ram[2] = 7; + ram[3] = 9; + ram[4] = 1; + ram[5] = 4; + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_PAUSED); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_PRIMED); + ASSERT_TRUE(find_trigger(&runtime, 2)->requirement->is_paused); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* unpause achievement 2 and unprime achievement 4 */ + ram[5] = 2; + ram[4] = 2; + assert_do_frame(&runtime, &memory); + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(find_trigger(&runtime, 2)->requirement->is_paused); + + assert_serialize(&runtime, buffer2, sizeof(buffer2)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_PAUSED); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_PRIMED); + ASSERT_TRUE(find_trigger(&runtime, 2)->requirement->is_paused); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer2); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(find_trigger(&runtime, 2)->requirement->is_paused); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_deactivated_memrefs() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); + assert_activate_achievement(&runtime, 3, "0xH0001=6_0xH0000=3"); + + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 6; + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 0, 1); + + /* deactivate an achievement with memrefs */ + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + rc_runtime_deactivate_achievement(&runtime, 1); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reactivate achievement 1 */ + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=2"); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_WAITING); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 0); + assert_hitcount(&runtime, 2, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 0, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_achievements_deactivated_no_memrefs() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); + assert_activate_achievement(&runtime, 3, "0xH0001=6_0xH0000=3"); + + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 6; + assert_do_frame(&runtime, &memory); + + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 0, 2); + assert_hitcount(&runtime, 3, 0, 0, 1); + + /* deactivate an achievement without memrefs - trigger should be removed */ + ASSERT_NUM_EQUALS(runtime.trigger_count, 3); + rc_runtime_deactivate_achievement(&runtime, 2); + ASSERT_NUM_EQUALS(runtime.trigger_count, 2); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + /* reactivate achievement 2 */ + assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); + assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); + assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); + assert_hitcount(&runtime, 1, 0, 0, 3); + assert_hitcount(&runtime, 2, 0, 0, 0); + assert_hitcount(&runtime, 3, 0, 0, 1); + + rc_runtime_destroy(&runtime); +} + +static void test_single_leaderboard() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_can_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + assert_memref(&runtime, 1, 5, 5, 4); + assert_memref(&runtime, 2, 6, 6, 0); + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_can_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_leaderboards() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_deserialize(&runtime, buffer); + + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_leaderboards_ignore_inactive() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + find_lboard(&runtime, 1)->state = RC_LBOARD_STATE_DISABLED; + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_DISABLED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + find_lboard(&runtime, 1)->state = RC_LBOARD_STATE_ACTIVE; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_deserialize(&runtime, buffer); + + /* non-serialized leaderboard should be reset */ + assert_sta_hitcount(&runtime, 1, 0, 0, 0); + assert_sub_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + /* serialized leaderboard should be restored */ + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_multiple_leaderboards_ignore_modified() +{ + uint8_t ram[] = { 2, 3, 6 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + + rc_runtime_init(&runtime); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); + assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); + assert_do_frame(&runtime, &memory); + ram[1] = 4; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + ram[1] = 5; + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + + assert_sta_hitcount(&runtime, 1, 0, 0, 3); + assert_sub_hitcount(&runtime, 1, 0, 0, 2); + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.3.::VAL:0xH0002"); + assert_do_frame(&runtime, &memory); + assert_do_frame(&runtime, &memory); + assert_deserialize(&runtime, buffer); + + /* modified leaderboard should be reset */ + assert_sta_hitcount(&runtime, 1, 0, 0, 0); + assert_sub_hitcount(&runtime, 1, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_WAITING); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); + + /* serialized leaderboard should be restored */ + assert_sta_hitcount(&runtime, 2, 0, 0, 2); + assert_sub_hitcount(&runtime, 2, 0, 0, 0); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); + ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_none() +{ + uint8_t buffer[2048]; + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_static() +{ + uint8_t buffer[2048]; + rc_runtime_t runtime; + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\nTest"); + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_simple_lookup() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n@Number(p0xH02)"); + assert_do_frame(&runtime, &memory); /* prev[2] = 0 */ + + ram[2] = 4; + assert_do_frame(&runtime, &memory); /* prev[2] = 2 */ + + ram[2] = 8; + assert_do_frame(&runtime, &memory); /* prev[2] = 4 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + ram[2] = 12; + assert_do_frame(&runtime, &memory); /* prev[2] = 8 */ + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* deserialized should remember prev[2] = 4 */ + assert_richpresence_output(&runtime, &memory, "4"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_tracked_hits() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + assert_do_frame(&runtime, &memory); /* count = 3 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 4 */ + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* deserialized should remember count = 3 */ + assert_richpresence_output(&runtime, &memory, "3"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_tracked_hits_md5_changed() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + assert_do_frame(&runtime, &memory); /* count = 3 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 4 */ + + assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)!"); + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* md5 changed, but variable is stored external to RP, 3 should be remembered */ + assert_richpresence_output(&runtime, &memory, "3!"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_conditional_display() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three\nLess"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 3 */ + assert_do_frame(&runtime, &memory); /* count = 4 */ + assert_richpresence_output(&runtime, &memory, "Three"); + + reset_runtime(&runtime); + assert_deserialize(&runtime, buffer); + + /* deserialized should remember count = 2 */ + assert_richpresence_output(&runtime, &memory, "Less"); + + rc_runtime_destroy(&runtime); +} + +static void test_rich_presence_conditional_display_md5_changed() +{ + uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + uint8_t buffer[2048]; + memory_t memory; + rc_runtime_t runtime; + + memory.ram = ram; + memory.size = sizeof(ram); + rc_runtime_init(&runtime); + + assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three\nLess"); + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + + assert_serialize(&runtime, buffer, sizeof(buffer)); + + assert_do_frame(&runtime, &memory); /* count = 3 */ + assert_do_frame(&runtime, &memory); /* count = 4 */ + assert_richpresence_output(&runtime, &memory, "Three"); + + reset_runtime(&runtime); + assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three!\nLess"); + assert_deserialize(&runtime, buffer); + + /* md5 changed, hit count should be discarded */ + assert_richpresence_output(&runtime, &memory, "Less"); + + assert_do_frame(&runtime, &memory); /* count = 1 */ + assert_do_frame(&runtime, &memory); /* count = 2 */ + assert_richpresence_output(&runtime, &memory, "Less"); + + assert_do_frame(&runtime, &memory); /* count = 3 */ + assert_richpresence_output(&runtime, &memory, "Three!"); + + rc_runtime_destroy(&runtime); +} + +/* ======================================================== */ + +void test_runtime_progress(void) { + TEST_SUITE_BEGIN(); + + TEST(test_empty); + TEST(test_single_achievement); + TEST(test_invalid_marker); + TEST(test_invalid_memref_chunk_id); + TEST(test_modified_data); + TEST(test_single_achievement_deactivated); + TEST(test_single_achievement_md5_changed); + TEST(test_single_achievement_sized); + TEST(test_empty_sized); + + TEST(test_no_core_group); + TEST(test_memref_shared_address); + TEST(test_memref_addsource); + TEST(test_memref_indirect); + TEST(test_memref_indirect_delta); + TEST(test_memref_double_indirect); + + TEST(test_multiple_achievements); + TEST(test_multiple_achievements_ignore_triggered_and_inactive); + TEST(test_multiple_achievements_overwrite_waiting); + TEST(test_multiple_achievements_reactivate_waiting); + TEST(test_multiple_achievements_paused_and_primed); + TEST(test_multiple_achievements_deactivated_memrefs); + TEST(test_multiple_achievements_deactivated_no_memrefs); + + TEST(test_single_leaderboard); + TEST(test_multiple_leaderboards); + TEST(test_multiple_leaderboards_ignore_inactive); + TEST(test_multiple_leaderboards_ignore_modified); + + TEST(test_rich_presence_none); + TEST(test_rich_presence_static); + TEST(test_rich_presence_simple_lookup); + TEST(test_rich_presence_tracked_hits); + TEST(test_rich_presence_tracked_hits_md5_changed); + TEST(test_rich_presence_conditional_display); + TEST(test_rich_presence_conditional_display_md5_changed); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_timing.c b/src/rcheevos/test/rcheevos/test_timing.c new file mode 100644 index 0000000000..00973db637 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_timing.c @@ -0,0 +1,166 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "mock_memory.h" + +#include "../test_framework.h" + +static rc_runtime_t runtime; +static int trigger_count[128]; + +static void event_handler(const rc_runtime_event_t* e) +{ + if (e->type == RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED) + { + rc_trigger_t* trigger = rc_runtime_get_achievement(&runtime, e->id); + trigger_count[e->id]++; + + rc_reset_trigger(trigger); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + } +} + +static void _assert_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr) +{ + int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); + ASSERT_NUM_EQUALS(result, RC_OK); +} +#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") + +static void do_timing(void) +{ + uint8_t ram[256], last, next, bitcount; + memory_t memory; + int i, j, mask; + clock_t total_clocks = 0, start, end; + double elapsed, average; + + memory.ram = ram; + memory.size = sizeof(ram); + memset(&ram[0], 0, sizeof(ram)); + for (i = 0; i < 0x3F; i++) + ram[0xC0 + i] = i; + + memset(&trigger_count, 0, sizeof(trigger_count)); + + rc_runtime_init(&runtime); + + assert_activate_achievement(&runtime, 1, "0xH0000=1"); + assert_activate_achievement(&runtime, 2, "0xH0001=1"); + assert_activate_achievement(&runtime, 3, "0xH0001=0"); + assert_activate_achievement(&runtime, 4, "0xH0002!=d0xH0002"); + assert_activate_achievement(&runtime, 5, + "A:0xH0000_A:0xH0001_A:0xH0002_A:0xH0003_A:0xH0004_A:0xH0005_A:0xH0006_A:0xH0007_" + "A:0xH0008_A:0xH0009_A:0xH000A_A:0xH000B_A:0xH000C_A:0xH000D_A:0xH000E_0xH000F=0xH0084"); + assert_activate_achievement(&runtime, 6, + "A:0xH0000_A:0xH0001_A:0xH0002_A:0xH0003_A:0xH0004_A:0xH0005_A:0xH0006_0xH0007=0xK0080_" + "A:0xH0008_A:0xH0009_A:0xH000A_A:0xH000B_A:0xH000C_A:0xH000D_A:0xH000E_0xH000F=0xK0081"); + assert_activate_achievement(&runtime, 7, "I:0xH0024_0xL00C0>8"); + assert_activate_achievement(&runtime, 8, "O:0xH0000=1_O:0xH0001=1_O:0xH0002=1_O:0xH0003=1_O:0xH0004=1_O:0xH0005=1_O:0xH0006=1_0xH0007=1"); + assert_activate_achievement(&runtime, 9, "N:0xH0000=1_N:0xH0001=1_N:0xH0002=1_N:0xH0003=1_N:0xH0004=1_N:0xH0005=1_N:0xH0006=1_0xH0007=1"); + assert_activate_achievement(&runtime, 10, "0xH008A>0xH008B_0xH008A<0xH0089"); + assert_activate_achievement(&runtime, 11, + "0xH0040=0xH0060_0xH0041=0xH0061_0xH0042=0xH0062_0xH0043=0xH0063_" + "0xH0044=0xH0064_0xH0045=0xH0065_0xH0046=0xH0066_0xH0047=0xH0067_" + "0xH0048=0xH0068_0xH0049=0xH0069_0xH004A=0xH006A_0xH004B=0xH006B_" + "0xH004C=0xH006C_0xH004D=0xH006D_0xH004E=0xH006E_0xH004F=0xH006F"); + assert_activate_achievement(&runtime, 12, "0xH0003=1.3._R:0xH0004=1_P:0xH0005=1"); + assert_activate_achievement(&runtime, 13, "0xH0003=1.3.SR:0xH0004=1_P:0xH0005=1"); + assert_activate_achievement(&runtime, 14, "I:0xH0024_0xH0040>d0xH0040"); + assert_activate_achievement(&runtime, 15, "b0xH0085=0xH0080"); + assert_activate_achievement(&runtime, 16, "0xH0026=0xH0028S0xH0023=0xH0027S0xH0025=0xH0022"); + assert_activate_achievement(&runtime, 17, + "C:0xH0000!=0_C:0xH0001!=0_C:0xH0002!=0_C:0xH0003!=0_C:0xH0004!=0_C:0xH0005!=0_C:0xH0006!=0_C:0xH0007!=0_" + "C:0xH0008!=0_C:0xH0009!=0_C:0xH000A!=0_C:0xH000B!=0_C:0xH000C!=0_C:0xH000D!=0_C:0xH000E!=0_0xH000F!=0.20."); + assert_activate_achievement(&runtime, 18, "A:0xL0087*10000_A:0xU0086*1000_A:0xL0086*100_A:0xU0085*10_0xL0x0085=0x 0080"); + + /* we expect these to only be true initially, so forcibly change them from WAITING to ACTIVE */ + rc_runtime_get_achievement(&runtime, 5)->state = RC_TRIGGER_STATE_ACTIVE; + rc_runtime_get_achievement(&runtime, 6)->state = RC_TRIGGER_STATE_ACTIVE; + rc_runtime_get_achievement(&runtime, 15)->state = RC_TRIGGER_STATE_ACTIVE; + rc_runtime_get_achievement(&runtime, 18)->state = RC_TRIGGER_STATE_ACTIVE; + + for (i = 0; i < 65536; i++) + { + /* + * $00-$1F = individual bits of i + * $20-$3F = number of times individual bits changed + * $40-$5F = number of times individual bits were 0 + * $60-$7F = number of times individual bits were 1 + * $80-$83 = i + * $84 = bitcount i + * $85-$87 = bcd i (LE) + * $88-$8C = ASCII i + * $C0-$FF = 0-63 + */ + mask = 1; + bitcount = 0; + for (j = 0; j < 32; j++) + { + next = ((i & mask) != 0) * 1; + mask <<= 1; + last = ram[j]; + + ram[j] = next; + ram[j + 0x60] += next; + bitcount += next; + ram[j + 0x40] += (1 - next); + ram[j + 0x20] += (next != last) * 1; + } + ram[0x80] = i & 0xFF; + ram[0x81] = (i >> 8) & 0xFF; + ram[0x82] = (i >> 16) & 0xFF; + ram[0x83] = (i >> 24) & 0xFF; + ram[0x84] = bitcount; + ram[0x85] = (i % 10) | ((i / 10) % 10) << 4; + ram[0x86] = ((i / 100) % 10) | ((i / 1000) % 10) << 4; + ram[0x87] = ((i / 10000) % 10); + ram[0x88] = ((i / 10000) % 10) + '0'; + ram[0x89] = ((i / 1000) % 10) + '0'; + ram[0x8A] = ((i / 100) % 10) + '0'; + ram[0x8B] = ((i / 10) % 10) + '0'; + ram[0x8C] = (i % 10) + '0'; + + start = clock(); + rc_runtime_do_frame(&runtime, event_handler, peek, &memory, NULL); + end = clock(); + + total_clocks += (end - start); + } + + elapsed = (double)total_clocks * 1000 / CLOCKS_PER_SEC; + average = elapsed * 1000 / i; + printf("\n%0.6fms elapsed, %0.6fus average", elapsed, average); + + ASSERT_NUM_EQUALS(trigger_count[ 0], 0); /* no achievement 0 */ + ASSERT_NUM_EQUALS(trigger_count[ 1], 32768); /* 0xH0000 = 1 every other frame */ + ASSERT_NUM_EQUALS(trigger_count[ 2], 32768); /* 0xH0001 = 1 2 / 4 frames */ + ASSERT_NUM_EQUALS(trigger_count[ 3], 32766); /* 0xH0001 = 0 2 / 4 frames (won't trigger until after first time it's 1) */ + ASSERT_NUM_EQUALS(trigger_count[ 4], 16383); /* 0xH0002!=d0xH0002 every two frames (no delta for first frame) */ + ASSERT_NUM_EQUALS(trigger_count[ 5], 65536); /* sum of bits = bitcount (16-bit) */ + ASSERT_NUM_EQUALS(trigger_count[ 6], 65536); /* sum of bits = bitcount (8-bit) */ + ASSERT_NUM_EQUALS(trigger_count[ 7], 6912); /* I:0xH0024_0xL00C0>8 pointer advances every eight frames */ + ASSERT_NUM_EQUALS(trigger_count[ 8], 65280); /* number of times any bit is set in lower byte */ + ASSERT_NUM_EQUALS(trigger_count[ 9], 256); /* number of times all bits are set in lower byte */ + ASSERT_NUM_EQUALS(trigger_count[10], 7400); /* hundreds digit less than thousands, but greater than tens */ + ASSERT_NUM_EQUALS(trigger_count[11], 256); /* number of times individual bits were 0 or 1 is equal */ + ASSERT_NUM_EQUALS(trigger_count[12], 2048); /* number of times bit3 was set without bit 4 or 5 */ + ASSERT_NUM_EQUALS(trigger_count[13], 3071); /* number of times bit3 was set without bit 4 xor 5 */ + ASSERT_NUM_EQUALS(trigger_count[14], 10350); /* I:0xH0024_0xH0040>d0xH0040 - delta increase across moving target */ + ASSERT_NUM_EQUALS(trigger_count[15], 1100); /* BCD check (only valid for two digits) */ + ASSERT_NUM_EQUALS(trigger_count[16], 24); /* random collection of bit changes being equal */ + ASSERT_NUM_EQUALS(trigger_count[17], 22187); /* number of times 20 bits or more are set across frames */ + ASSERT_NUM_EQUALS(trigger_count[18], 65536); /* BCD reconstruction */ + + rc_runtime_destroy(&runtime); +} + +void test_timing(void) { + TEST_SUITE_BEGIN(); + TEST(do_timing); + TEST(do_timing); + TEST(do_timing); + TEST(do_timing); + TEST(do_timing); + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_trigger.c b/src/rcheevos/test/rcheevos/test_trigger.c new file mode 100644 index 0000000000..a2db7d8870 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_trigger.c @@ -0,0 +1,2492 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void _assert_parse_trigger(rc_trigger_t** trigger, void* buffer, size_t buffer_size, const char* memaddr) +{ + int size; + unsigned* overflow; + + size = rc_trigger_size(memaddr); + ASSERT_NUM_GREATER(size, 0); + ASSERT_NUM_LESS_EQUALS(size + 4, buffer_size); + + overflow = (unsigned*)(((char*)buffer) + size); + *overflow = 0xCDCDCDCD; + + *trigger = rc_parse_trigger(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(*trigger); + + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } +} +#define assert_parse_trigger(trigger, buffer, memaddr) ASSERT_HELPER(_assert_parse_trigger(trigger, buffer, sizeof(buffer), memaddr), "assert_parse_trigger") + +static void _assert_evaluate_trigger(rc_trigger_t* trigger, memory_t* memory, int expected_result) { + int result = rc_test_trigger(trigger, peek, memory, NULL); + ASSERT_NUM_EQUALS(result, expected_result); +} +#define assert_evaluate_trigger(trigger, memory, expected_result) ASSERT_HELPER(_assert_evaluate_trigger(trigger, memory, expected_result), "assert_evaluate_trigger") + +static rc_condition_t* trigger_get_cond(rc_trigger_t* trigger, int group_index, int cond_index) { + rc_condset_t* condset = trigger->requirement; + rc_condition_t* cond; + + if (group_index != 0) { + --group_index; + condset = trigger->alternative; + while (group_index-- != 0) { + if (condset == NULL) + break; + + condset = condset->next; + } + } + + if (condset == NULL) + return NULL; + + cond = condset->conditions; + while (cond_index-- != 0) { + if (cond == NULL) + break; + + cond = cond->next; + } + + return cond; +} + +static void _assert_hit_count(rc_trigger_t* trigger, int group_index, int cond_index, uint32_t expected_hit_count) { + rc_condition_t* cond = trigger_get_cond(trigger, group_index, cond_index); + ASSERT_PTR_NOT_NULL(cond); + + ASSERT_NUM_EQUALS(cond->current_hits, expected_hit_count); +} +#define assert_hit_count(trigger, group_index, cond_index, expected_hit_count) ASSERT_HELPER(_assert_hit_count(trigger, group_index, cond_index, expected_hit_count), "assert_hit_count") + +static int evaluate_trigger(rc_trigger_t* self, memory_t* memory) { + return rc_evaluate_trigger(self, peek, memory, NULL); +} + +/* ======================================================== */ + +static void test_alt_groups() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=16S0xH0002=52S0xL0004=6"); + + /* core not true, both alts are */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 1, 0, 1); + assert_hit_count(trigger, 2, 0, 1); + + /* core and both alts true */ + ram[1] = 16; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 1, 0, 2); + assert_hit_count(trigger, 2, 0, 2); + + /* core and first alt true */ + ram[4] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2); + assert_hit_count(trigger, 1, 0, 3); + assert_hit_count(trigger, 2, 0, 2); + + /* core true, but neither alt */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 3); + assert_hit_count(trigger, 1, 0, 3); + assert_hit_count(trigger, 2, 0, 2); + + /* core and second alt true */ + ram[4] = 6; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 4); + assert_hit_count(trigger, 1, 0, 3); + assert_hit_count(trigger, 2, 0, 3); +} + +static void test_empty_core() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "S0xH0002=2S0xL0004=4"); + + /* core implicitly true, neither alt true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 0); + assert_hit_count(trigger, 2, 0, 0); + + /* first alt true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 1); + assert_hit_count(trigger, 2, 0, 0); + + /* both alts true */ + ram[4] = 4; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 2); + assert_hit_count(trigger, 2, 0, 1); + + /* second alt true */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 2); + assert_hit_count(trigger, 2, 0, 2); +} + +static void test_empty_alt() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0002=2SS0xL0004=4"); + + /* core false, first alt implicitly true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 2, 0, 0); + + /* core true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 2, 0, 0); + + /* core and both alts true */ + ram[4] = 4; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2); + assert_hit_count(trigger, 2, 0, 1); +} + +static void test_empty_last_alt() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0002=2S0xL0004=4S"); + + /* core false, second alt implicitly true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* core true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 1, 0, 0); + + /* core and both alts true */ + ram[4] = 4; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2); + assert_hit_count(trigger, 1, 0, 1); +} + +static void test_empty_all_alts() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0002=2SS"); + + /* core false, all alts implicitly true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + + /* core true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); +} + +static void test_resetif_in_alt_group() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18(1)_R:0xH0000=1S0xH0002=52(1)S0xL0004=6(1)_R:0xH0000=2"); + + /* all conditions true, no resets */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + + /* reset in core group resets everything */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 0, 0U); + + /* all conditions true, no resets */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + + /* reset in alt group resets everything */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 0, 0U); +} + +static void test_pauseif_in_alt_group() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_P:0xH0000=1S0xH0002=52S0xL0004=6_P:0xH0000=2"); + + /* all conditions true, no resets */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + + /* pause in core group only pauses core group */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 2U); + + /* unpaused */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 3U); + + /* pause in alt group only pauses alt group */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + assert_hit_count(trigger, 1, 0, 4U); + assert_hit_count(trigger, 2, 0, 3U); +} + +static void test_pauseif_resetif_in_alt_group() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=0.1._0xH0003=2SP:0xH0001=18_R:0xH0002=52"); + + /* capture hitcount */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* prevent future hit counts */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* unpause alt group, hit count should be reset */ + ram[1] = 16; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + + /* repause alt group, capture hitcount */ + ram[0] = 0; + ram[1] = 18; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* trigger condition. alt group is paused, so should be considered false */ + ram[3] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + + /* trigger condition. alt group is unpaused, so reset will prevent trigger */ + ram[1] = 16; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + + /* trigger condition. alt group is unpaused, and not resetting, so allow trigger */ + ram[2] = 30; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); +} + +static void test_pauseif_hitcount_with_reset() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_P:0xH0002=52.1._R:0xH0003=1SR:0xH0003=2"); + + /* pauseif triggered, non-pauseif conditions ignored */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* pause condition is no longer true, but hitcount keeps it paused */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* resetif in paused group is ignored */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* resetif in alternate group is honored, active resetif prevents trigger */ + ram[3] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0); + assert_hit_count(trigger, 0, 1, 0); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); + + /* resetif no longer active, pause not active, first condition true, trigger activates */ + ram[3] = 3; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 0, 1, 0); + assert_hit_count(trigger, 0, 2, 0); + assert_hit_count(trigger, 1, 0, 0); +} + +static void test_measured() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)"); + ASSERT_NUM_EQUALS(trigger->measured_as_percent, 0); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented to reach target */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - target previously met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_as_percent() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) */ + assert_parse_trigger(&trigger, buffer, "G:0xH0002=52(3)"); + ASSERT_NUM_EQUALS(trigger->measured_as_percent, 1); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented to reach target */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - target previously met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_comparison() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(byte(2) >= 80) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002>=80"); + + /* condition is not true */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0x34U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is still not true */ + ram[2] = 79; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 79U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true - value matches */ + ram[2] = 80; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 80U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true - value exceeds */ + ram[2] = 255; + trigger->state = RC_TRIGGER_STATE_ACTIVE; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 255U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); +} + +static void test_measured_addhits() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(5, byte(1) == 10 || byte(2) == 10)) */ + assert_parse_trigger(&trigger, buffer, "C:0xH0001=10_M:0xH0002=10(5)"); + + /* neither is true - hit count should not be captured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second is true - hit count should be incremented by one */ + ram[2] = 10; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* both are true - hit count should be incremented by two */ + ram[1] = 10; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* only first is true - hit count should be incremented by one */ + ram[2] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* neither is true - hit count should not increment */ + ram[1] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first is true - hit count should be incremented by one and trigger */ + ram[1] = 10; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); +} + +static void test_measured_indirect() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[384]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(byte(0) + 2) == 52)) */ + assert_parse_trigger(&trigger, buffer, "I:0xH0000_M:0xH0002=52(3)"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is no longer true - hit count should not be incremented */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is no longer true - hit count should not be incremented */ + ram[2] = 30; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_multiple() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple measured conditions are only okay if they all have the same target, in which + * case, the maximum of all the measured values is returned */ + + /* measured(repeated(3, byte(2) == 52)) || measured(repeated(3, byte(3) == 17)) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(3)SM:0xH0003=17(3)"); + + /* first condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* second condition is true - second hit count should be incremented - both will be the same */ + ram[2] = 9; + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* second condition still true - second hit count should be incremented and become prominent */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* switch back to first condition */ + ram[2] = 52; + ram[3] = 8; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* first hit count will be incremented and target met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* both true, only second will increment as first target is met */ + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* both true, both targets met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_multiple_with_hitcount_in_core() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple measured conditions are only okay if they all have the same target, in which + * case, the maximum of all the measured values is returned */ + + /* repeated(7, byte(1) == 18) && (measured(repeated(3, byte(2) == 52)) || measured(repeated(3, byte(3) == 17))) */ + assert_parse_trigger(&trigger, buffer, "0xH0001=18(7)SM:0xH0002=52(3)SM:0xH0003=17(3)"); + + /* first condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* second condition is true - second hit count should be incremented - both will be the same */ + /* core hit target is greater than any measured value, but should not be measured */ + ram[2] = 9; + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_while_paused() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* unpaused - hit count should be incremented */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); +} + +static void test_measured_while_paused_multiple() { + uint8_t ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* (measured(repeated(6, byte(2) == 52)) && unless(bit0(1) == 1)) || (measured(repeated(6, byte(0) == 0)) && unless(bit1(1) == 1)) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(6)_P:0xM0001=1SM:0xH0000=0(6)_P:0xN0001=1"); + + /* both alts should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* first alt paused - second should update */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* first still paused - second should update again */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* both paused - neither should update - expect last measured value to be kept */ + ram[1] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* first unpaused - it will update, measured will use the active value */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* both paused - neither should update - expect last measured value to be kept */ + ram[1] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); + + /* both unpaused - both updated, will use higher */ + ram[1] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 4U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 6U); +} + +static void test_measured_while_paused_reset_alt() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1SR:0xH0003=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* reset - hit count should be cleared */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* unpaused, reset still true, hit count should remain cleared */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* reset not true - hit count should be incremented */ + ram[3] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); +} + +static void test_measured_while_paused_reset_core() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ + assert_parse_trigger(&trigger, buffer, "R:0xH0003=1SM:0xH0002=52(3)_P:0xH0001=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* reset - hit count should be cleared */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* unpaused, reset still true, hit count should remain cleared */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* reset not true - hit count should be incremented */ + ram[3] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); +} + +static void test_measured_while_paused_reset_non_hitcount() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=99_P:0xH0001=1SR:0xH0003=1"); + + /* initial value */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + ASSERT_NUM_EQUALS(trigger->measured_target, 99U); + + /* paused - capture value and return that*/ + ram[1] = 1; + ram[2] = 60; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + + /* reset - captured value should not be cleared */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + + /* unpaused, reset still true, value should reflect current */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 60U); +} + +static void test_measured_while_paused_extra_alts() { + uint8_t ram[] = { 0x00, 0x00, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* (measured(byte(2) == 99) && unless(byte(1) == 1)) || (byte(3) == 1) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=99_P:0xH0001=1S0xH0003=1"); + + /* alt1 will capture measured value */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + ASSERT_NUM_EQUALS(trigger->measured_target, 99U); + + /* first alt paused - expect last measured value to be kept */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + ASSERT_NUM_EQUALS(trigger->measured_target, 99U); + + /* measured value changed but alt still paused - expect last measured value to be kept */ + ram[2] = 61; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 52U); + ASSERT_NUM_EQUALS(trigger->measured_target, 99U); + + /* first alt unpaused - it will update, measured will use the active value */ + ram[1] = 0; + ram[2] = 63; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 63U); + ASSERT_NUM_EQUALS(trigger->measured_target, 99U); +} + +static void test_measured_reset_hitcount() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && never(byte(3) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1_R:0xH0003=1"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* reset primed, but ignored by pause */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* unpaused, reset should clear value */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* no longer reset, hit count should increment */ + ram[3] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + + /* reset again, hit count should go back to 0 */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); +} + +static void test_measured_reset_comparison() { + uint8_t ram[] = {0x00, 0x12, 0x02, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(byte(2) >= 10) && unless(byte(1) == 1) && never(byte(3) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002>=10_P:0xH0001=1_R:0xH0003=1"); + + /* condition is true - measured will come from value */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 10U); + + /* condition is true - value updated */ + ram[2] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + + /* paused - updated value should be ignored */ + ram[1] = 1; + ram[2] = 4; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + + /* reset primed, but ignored by pause */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + + /* unpaused, reset should not affect non-hitcount measurement */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + + /* no longer reset, value updated */ + ram[3] = 0; + ram[2] = 5; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + + /* reset again, should not affect non-hitcount measurement */ + ram[3] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); +} + +static void test_measured_if() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52), when=byte(0) == 1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_Q:0xH0000=1"); + + /* condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented and measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - hit count should be incremented to reach target, but it's not measured */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* condition is true - target previously met, but now it's measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_measured_if_comparison() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(byte(2) >= 80, when=byte(0)==1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002>=80_Q:0xH0000=1"); + + /* condition is not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition not true, but measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0x34U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is still not true, but measured */ + ram[2] = 79; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 79U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true, but not measured */ + ram[0] = 0; + ram[2] = 80; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); + + /* condition is true and measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 80U); + ASSERT_NUM_EQUALS(trigger->measured_target, 80U); +} + +static void test_measured_if_multiple_measured() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* multiple measured conditions are only okay if they all have the same target, in which + * case, the maximum of all the measured values is returned */ + + /* measured(repeated(5, byte(2) == 52), when=byte(0)=1) || measured(repeated(5, byte(3) == 17), when=byte(0)=2) */ + assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(5)_Q:0xH0000=1SM:0xH0003=17(5)_Q:0xH0000=2"); + + /* first condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition is true - second hit count should be incremented - both will be the same; still not measured */ + ram[2] = 9; + ram[3] = 17; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition still true - second hit count should be incremented and become prominent, but first is measured */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 1U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* switch back to first condition */ + ram[2] = 52; + ram[3] = 8; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 2U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first hit count will be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 3U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first hit count will be incremented, but neither measured */ + ram[0] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 4U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first will increment to trigger state, but it's not measured - second is */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 1, 0, 5U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* first is measured and will trigger */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 1, 0, 5U); + assert_hit_count(trigger, 2, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); +} + +static void test_measured_if_multiple_measured_if() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(5, byte(2) == 52), when=byte(0)=1 && byte(1)==1) */ + assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(5)_Q:0xH0000=1_Q:0xH0001=1"); + + /* first condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition is true - hit count still incremented, but not measured because of third condition */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* third condition is true, measured should be measured */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition no longer true, measured ignored */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 4U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* hit target met, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 5U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* hit target met, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 0, 5U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); + + /* second condition true, measured should be measured, trigger will fire */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 0, 5U); + ASSERT_NUM_EQUALS(trigger->measured_value, 5U); + ASSERT_NUM_EQUALS(trigger->measured_target, 5U); +} + +static void test_measured_if_while_paused() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* measured(repeated(3, byte(2) == 52), when=byte(0)==1) && unless(byte(1) == 1) */ + /* NOTE: this test also verifies the behavior when the MeasuredIf is first */ + assert_parse_trigger(&trigger, buffer, "Q:0xH0000=1_M:0xH0002=52(3)_P:0xH0001=1"); + + /* condition is true - hit count should be incremented, but not measured */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* paused - hit count should not be incremented */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* paused - but measured - measured_value is not updated when paused */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + + /* unpaused - hit count should be incremented and measured value captured */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused - hit count should not be incremented, and last hit count should be measured */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + + /* paused but not measured - pause will prevent evaluation of MeasuredIf, so measured retained */ + ram[0] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); +} + +static void test_measured_trigger() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* never(byte(0) != 0) && trigger_when(measured(repeated(3, byte(2) == 52))) */ + assert_parse_trigger(&trigger, buffer, "R:0xH0000!=0SM:0xH0002=52(3)ST:0=1"); + ASSERT_NUM_EQUALS(trigger->measured_as_percent, 0); + + /* condition is true - hit count should be incremented, and trigger shown */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 1, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* core condition is false - trigger should not be shown and hit count reset */ + ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + assert_hit_count(trigger, 1, 0, 0U); + ASSERT_NUM_EQUALS(trigger->measured_value, 0U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* core condition is true again - hit count should be incremented, and trigger shown */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 1, 0, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 1U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* increment hit count */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 1, 0, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); + + /* trigger */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 1, 0, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 3U); + ASSERT_NUM_EQUALS(trigger->measured_target, 3U); +} + +static void test_resetnextif_trigger() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* ResetNextIf byte(0x0002)=1 + * byte(0x0001)=1 (1) + * Trigger byte(0x0003)=0 + */ + assert_parse_trigger(&trigger, buffer, "Z:0xH0002=1_0xH0001=1.1._T:0xH0003=0"); + + /* both conditions false */ + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); +} + +static void test_evaluate_trigger_inactive() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_INACTIVE; + + /* Inactive is a permanent state - trigger is initially true */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ram[2] = 24; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* Trigger no longer true, still inactive */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* hits should not be tallied when inactive */ + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 0U); + + /* memrefs should be updated while inactive */ + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.value, 24U); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.changed, 0); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.prior, 52U); + + /* reset should be ignored while inactive */ + ram[4] = 4; + trigger_get_cond(trigger, 0, 0)->current_hits = 1U; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + assert_hit_count(trigger, 0, 0, 1U); +} + +static void test_evaluate_trigger_waiting() { + uint8_t ram[] = {0x00, 0x12, 0x18, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_WAITING; + + /* trigger is ready to fire, but won't as long as its waiting */ + /* prevents triggers from uninitialized memory */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + ram[2] = 16; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + + /* waiting trigger should not tally hits */ + ASSERT_FALSE(trigger->has_hits); + + /* ResetIf makes the trigger state false, so the trigger should become active */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* reset to previous state */ + trigger->state = RC_TRIGGER_STATE_WAITING; + ram[4] = 9; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + ASSERT_FALSE(trigger->has_hits); + + /* trigger is no longer true, proceed to active state */ + ram[1] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 0U); + assert_hit_count(trigger, 0, 1, 1U); +} + +static void test_evaluate_trigger_reset() { + uint8_t ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* generate a hit count */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetIf that resets hits returns RESET, but doesn't change the state */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(trigger->has_hits); + + /* ResetIf that doesn't resets hits doesn't return RESET */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_FALSE(trigger->has_hits); +} + +static void test_evaluate_trigger_reset_next() { + uint8_t ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "Z:0xL0004=4_0xH0001=5.2._0xH0003=3"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* generate a hit count */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext that resets hits returns RESET, but doesn't change the state */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); /* ResetNext will have a hit */ + + /* ResetNext that doesn't resets hits doesn't return RESET */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); /* ResetNext will have a hit */ + + /* Secondary hit should still be tallied, ResetNext that doesn't reset hits doesn't return RESET */ + ram[3] = 3; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext no longer true, tally hit */ + ram[4] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext that resets hits returns RESET, but doesn't reset the secondary hits */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* ResetNext no longer true, tally hit */ + ram[4] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + + /* tally second hit to trigger */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_triggered() { + uint8_t ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* transition to TRIGGERED */ + ram[1] = 18; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 1U); + + /* triggered trigger remains triggered, but returns INACTIVE and does not increment hit counts */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 1U); + + /* triggered trigger remains triggered when no longer true */ + ram[1] = 5; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); + assert_hit_count(trigger, 0, 0, 1U); + assert_hit_count(trigger, 0, 1, 1U); + + /* triggered trigger does not update deltas */ + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.value.memref->value.value, 18U); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.value.memref->value.changed, 1U); +} + +static void test_evaluate_trigger_paused() { + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0003=171_P:0xH0002=1SR:0xH0004=4"); + + /* INACTIVE is a permanent state - trigger is initially true */ + trigger->state = RC_TRIGGER_STATE_INACTIVE; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* PauseIf is ignored when INACTIVE */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + + /* unpause, switch to WAITING, ready to trigger, so will stay WAITING */ + ram[2] = 2; + trigger->state = RC_TRIGGER_STATE_WAITING; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); + + /* PauseIf makes the evaluation false, so will transition to ACTIVE, but PAUSED */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + ASSERT_TRUE(trigger->has_hits); /* the PauseIf has a hit */ + assert_hit_count(trigger, 0, 0, 0U); + + /* hitcounts will update when unpaused; adjust memory so trigger is no longer true */ + ram[2] = 2; + ram[3] = 99; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + ASSERT_TRUE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 1U); + + /* hitcounts should remain while paused */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + ASSERT_TRUE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 1U); + + /* ResetIf while paused should notify, but not change state */ + ram[4] = 4; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PAUSED); + ASSERT_FALSE(trigger->has_hits); + assert_hit_count(trigger, 0, 0, 0U); + + /* ResetIf without hitcounts should return current state */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + + /* trigger while paused is ignored */ + ram[4] = 0; + ram[3] = 171; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); + + /* trigger should file when unpaused */ + ram[2] = 2; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + + /* triggered trigger ignore pause */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_primed() { + uint8_t ram[] = {0x00, 0x01, 0x00, 0x01, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[640]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1_T:0xH0001=1_0xH0002=1_T:0xH0003=1_0xH0004=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* T (trigger) conditions are true, but nothing else */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* one non-trigger condition is still false */ + ram[0] = ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* all non-trigger conditions are true, one trigger condition is not true */ + ram[1] = 0; ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* non-trigger condition is false again */ + ram[0] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* all conditions are true */ + ram[0] = ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); + + /* one non-trigger condition is false */ + ram[3] = 0; + trigger->state = RC_TRIGGER_STATE_ACTIVE; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* all conditions are true */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_primed_in_alts() { + uint8_t ram[] = {0x01, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1_0xH0002=1ST:0xH0003=1_0xH0004=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* core is true, but neither alt is primed */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* both alts primed */ + ram[2] = ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* only second alt is primed */ + ram[4] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* neither alt is primed */ + ram[2] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* both alts primed */ + ram[2] = ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* alt 2 is true */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_primed_one_alt() { + uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1S0xH0002=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* core must be true for trigger to be primed */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* second alt is true, but core is not */ + ram[2] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* first alt is true, but core is not */ + ram[2] = 0; ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + + /* only core is true, first alt is marked as Trigger, eligible to fire */ + ram[1] = 0; ram[0] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + + /* alt is true */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); +} + +static void test_evaluate_trigger_disabled() { + uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1S0xH0002=1"); + trigger->state = RC_TRIGGER_STATE_DISABLED; + + /* state stays DISABLED, but evaluate returns INACTIVE */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_DISABLED); +} + +static void test_evaluate_trigger_chained_resetnextif() { + uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; + memory_t memory; + rc_trigger_t* trigger; + char buffer[640]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* once(byte(4)==1 && never(repeated(2, byte(3)==1 && never(byte(1)==1 || byte(2)==1))) && trigger_when(byte(0)==1) */ + assert_parse_trigger(&trigger, buffer, "O:0xH0001=1_Z:0xH0002=1_Z:0xH0003=1.2._0xH0004=1.1._T:0xH0000=1"); + trigger->state = RC_TRIGGER_STATE_ACTIVE; + + /* nothing is true */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); + assert_hit_count(trigger, 0, 0, 0); /* OrNext 0x0001 == 1 */ + assert_hit_count(trigger, 0, 1, 0); /* ResetNextIf 0x0002 == 1 */ + assert_hit_count(trigger, 0, 2, 0); /* ResetNextIf 0x0003 == 1 (2) */ + assert_hit_count(trigger, 0, 3, 0); /* 0x0004 == 1 (1) */ + assert_hit_count(trigger, 0, 4, 0); /* Trigger 0x0000 == 1 */ + + /* non-trigger condition is true */ + ram[4] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 0, 3, 1); + + /* second ResetNextIf is true */ + ram[3] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 0, 2, 1); + assert_hit_count(trigger, 0, 3, 1); + + /* OrNext resets second ResetNextIf */ + ram[1] = 1; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); /* result is RESET */ + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); /* state is PRIMED */ + assert_hit_count(trigger, 0, 0, 1); /* OrNext tallies a hit of its own */ + assert_hit_count(trigger, 0, 1, 1); /* ResetNextIf gets a hit from the OrNext */ + assert_hit_count(trigger, 0, 2, 0); /* hit is reset by the ResetNextIf */ + assert_hit_count(trigger, 0, 3, 1); /* hit is not affected by the reset ResetNextIf */ + + /* OrNext no longer true */ + ram[1] = 0; + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 1); + assert_hit_count(trigger, 0, 3, 1); + + /* second ResetNextIf fires */ + ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); + assert_hit_count(trigger, 0, 0, 1); + assert_hit_count(trigger, 0, 1, 1); + assert_hit_count(trigger, 0, 2, 2); + assert_hit_count(trigger, 0, 3, 0); +} + +static void test_prev_prior_share_memref() { + rc_trigger_t* trigger; + rc_memrefs_t* memrefs; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xH0001=d0xH0001_0xH0001!=p0xH0001"); + + memrefs = rc_trigger_get_memrefs(trigger); + ASSERT_PTR_NOT_NULL(memrefs); + ASSERT_NUM_EQUALS(memrefs->memrefs.count, 1); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 1U); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); + + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand2.type, RC_OPERAND_DELTA); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand2.type, RC_OPERAND_PRIOR); +} + +static void test_bit_lookups_share_memref() { + rc_trigger_t* trigger; + rc_memrefs_t* memrefs; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xM0001=1_0xN0x0001=0_0xO0x0001=1"); + + memrefs = rc_trigger_get_memrefs(trigger); + ASSERT_PTR_NOT_NULL(memrefs); + ASSERT_NUM_EQUALS(memrefs->memrefs.count, 1); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 1U); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); + + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.size, RC_MEMSIZE_BIT_0); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.size, RC_MEMSIZE_BIT_1); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 2)->operand1.size, RC_MEMSIZE_BIT_2); +} + +static void test_bitcount_shares_memref() { + rc_trigger_t* trigger; + rc_memrefs_t* memrefs; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xH0001>5_0xK0001!=3"); + + memrefs = rc_trigger_get_memrefs(trigger); + ASSERT_PTR_NOT_NULL(memrefs); + ASSERT_NUM_EQUALS(memrefs->memrefs.count, 1); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 1U); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); + + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.size, RC_MEMSIZE_8_BITS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.type, RC_OPERAND_ADDRESS); + ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.size, RC_MEMSIZE_BITCOUNT); +} + +static void test_large_memref_not_shared() { + rc_trigger_t* trigger; + rc_memrefs_t* memrefs; + char buffer[512]; + + assert_parse_trigger(&trigger, buffer, "0xH1234=1_0xX1234>d0xX1234"); + + memrefs = rc_trigger_get_memrefs(trigger); + ASSERT_PTR_NOT_NULL(memrefs); + + /* this could be shared, but isn't currently */ + ASSERT_NUM_EQUALS(memrefs->memrefs.count, 2); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 0x1234U); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); + + ASSERT_NUM_EQUALS(memrefs->memrefs.items[1].address, 0x1234U); + ASSERT_NUM_EQUALS(memrefs->memrefs.items[1].value.size, RC_MEMSIZE_32_BITS); +} + +static void test_remember_recall() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[256]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "K:1_{recall}=1(3)"); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + + /* condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + + /* condition is true - hit count should be incremented to reach target */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 1, 3U); + + /* condition is true - target previously met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 1, 3U); +} + +static void test_remember_recall_separate_accumulator_per_group() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[640]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "K:1_{recall}=1.3.S{recall}=1.3.SK:1_K:{recall}*2_{recall}=2.5."); + + /* core group condition is true - hit count should be incremented */ + /* alt1 group condition is false since it's a different recall accumulator */ + /* alt2 group condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 2, 1U); + + /* core group condition is true - hit count should be incremented */ + /* alt group condition is false since it's a different recall accumulator */ + /* alt2 group condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 2, 2U); + + /* core group condition is true - hit count should be incremented to reach target */ + /* alt group condition is false since it's a different recall accumulator */ + /* alt2 group condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 3U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 2, 3U); + + /* core group condition is true - target previously met */ + /* alt group condition is false since it's a different recall accumulator */ + /* alt2 group condition is true - hit count should be incremented */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 3U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 2, 4U); + + /* core group condition is true - target previously met */ + /* alt group condition is false since it's a different recall accumulator */ + /* alt2 group condition is true - hit count incremented to reach target */ + /* core + alt2 now satisfied, trigger is true*/ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 1, 3U); + assert_hit_count(trigger, 1, 0, 0U); + assert_hit_count(trigger, 2, 2, 5U); +} + +static void test_remember_recall_separate_accumulator_per_group_complex() +{ + uint8_t ram[128]; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1280]; + + memory.ram = ram; + memory.size = sizeof(ram); + memset(ram, 0, sizeof(ram)); + + assert_parse_trigger(&trigger, buffer, "0=0SK:0x 0002&1023_K:{recall}*2_K:{recall}+4_{recall}=100SK:0x 0004&1023_K:{recall}*2_K:{recall}+4_{recall}=100"); + + /* $2=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ + /* $4=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ + assert_evaluate_trigger(trigger, &memory, 0); + + /* $2=0030 & 03FF = 0030 * 2 = 0060 + 4 = 0064 ?= 100 = true */ + /* $4=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ + ram[2] = 0x30; + assert_evaluate_trigger(trigger, &memory, 1); + + /* $2=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ + /* $4=0030 & 03FF = 0030 * 2 = 0060 + 4 = 0064 ?= 100 = true */ + ram[2] = 0x00; + ram[4] = 0x30; + assert_evaluate_trigger(trigger, &memory, 1); + + /* $2=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ + /* $4=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ + ram[4] = 0x00; + assert_evaluate_trigger(trigger, &memory, 0); +} + +static void test_remember_recall_use_same_value_multiple() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[640]; + + memory.ram = ram; + memory.size = sizeof(ram); + + ram[0] = 1; + assert_parse_trigger(&trigger, buffer, "K:5_A:0xH00_C:{recall}=6_B:0xH00_C:{recall}=4_M:0=1.4."); + + /* because the recall accumulator can be re-used, both add hits are true and increment hits */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 2, 1U); + assert_hit_count(trigger, 0, 4, 1U); + ASSERT_NUM_EQUALS(trigger->measured_value, 2U); + ASSERT_NUM_EQUALS(trigger->measured_target, 4U); + + /* because the recall accumulator can be re-used, both add hits are true and increment hits to reach target*/ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 2, 2U); + assert_hit_count(trigger, 0, 4, 2U); + ASSERT_NUM_EQUALS(trigger->measured_value, 4U); + ASSERT_NUM_EQUALS(trigger->measured_target, 4U); + + /* condition is true - previously met */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 2, 3U); + assert_hit_count(trigger, 0, 4, 3U); + ASSERT_NUM_EQUALS(trigger->measured_value, 6U); + ASSERT_NUM_EQUALS(trigger->measured_target, 4U); +} + +static void test_remember_recall_in_pause_and_main() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[640]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "K:0xH00_{recall}<3.4._K:0xH00*2_{recall}>0xH01_K:0xH00*2_P:{recall}=2"); + + /* pause checks 0*2=2, not paused. Condition 2 gets hit since recalled value < 3 */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + + ram[0] = 1; + /* pause checks 1*2 = 2, pause active. condition 2 does not get new hit */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 1U); + + ram[0] = 2; + /* pause checks 2*2 = 2, pause inactive. condition 2 dgets hit because < 3 */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + + ram[0] = 3; + /* pause checks 2*2 = 2, pause inactive. condition 2 gets no because = 3 */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 2U); + + ram[0] = 0; + /* pause checks 0*2=2, not paused. Condition 2 gets hit since recalled value < 3 */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 3U); + + ram[0] = 1; + /* pause checks 1*2 = 2, pause active. condition 2 does not get new hit */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 3U); + + ram[0] = 2; + /* pause checks 2*2 = 2, pause inactive. condition 2 dgets hit because < 3, not true because condition 4 untrue */ + assert_evaluate_trigger(trigger, &memory, 0); + assert_hit_count(trigger, 0, 1, 4U); + + ram[0] = 10; + /* condition is true - hits on condition 2 previously met, no active pause. */ + assert_evaluate_trigger(trigger, &memory, 1); + assert_hit_count(trigger, 0, 1, 4U); +} + +static void test_remember_recall_in_pause_with_chain() +{ + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + rc_condition_t* condition; + char buffer[640]; + + memory.ram = ram; + memory.size = sizeof(ram); + + assert_parse_trigger(&trigger, buffer, "I:{recall}_I:0xH02_0xH03=10_K:0xH00_P:0=1"); + + /* ensure recall memrefs point at the remember for the pause chain */ + condition = trigger->requirement->conditions; + ASSERT_NUM_EQUALS(condition->operand1.type, RC_OPERAND_RECALL); + ASSERT_PTR_NOT_NULL(condition->operand1.value.memref); + + condition = condition->next; + ASSERT_NUM_EQUALS(condition->operand1.value.memref->value.memref_type, RC_MEMREF_TYPE_MODIFIED_MEMREF); + ASSERT_NUM_EQUALS(((rc_modified_memref_t*)condition->operand1.value.memref)->parent.type, RC_OPERAND_RECALL); + ASSERT_PTR_NOT_NULL(((rc_modified_memref_t*)condition->operand1.value.memref)->parent.value.memref); + + /* byte(0)=0, remember. byte(0+2)=0x34, byte(0x34+3)=n/a */ + assert_evaluate_trigger(trigger, &memory, 0); + + ram[2] = 1; + /* byte(0)=0, remember. byte(0+2)=1, byte(1+3)=0x56 */ + assert_evaluate_trigger(trigger, &memory, 0); + + ram[4] = 10; + /* byte(0)=0, remember. byte(0+2)=1, byte(1+3)=10 */ + assert_evaluate_trigger(trigger, &memory, 1); +} + +static void test_remember_recall_in_addaddress() +{ + uint8_t ram[] = { 0x02, 0x03, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0x0001) + (byte(0x0000) - 1) * 2) == 60 */ + assert_parse_trigger(&trigger, buffer, "K:0xH0000-1_K:{recall}*2_I:{recall}+0xH0001_0xH0000=60"); + + /* byte(3 + (2 - 1) * 2) => byte(3+2) => byte(5) == 60 */ + assert_evaluate_trigger(trigger, &memory, 0); + + /* condition is true */ + ram[5] = 60; + assert_evaluate_trigger(trigger, &memory, 1); + + /* byte(3 + (3 - 1) * 2) => byte(3+4) => byte(7) == 60 */ + ram[0] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + + /* condition is true */ + ram[7] = 60; + assert_evaluate_trigger(trigger, &memory, 1); + + /* byte(0 + (3 - 1) * 2) => byte(0+4) => byte(4) == 60 */ + ram[1] = 0; + assert_evaluate_trigger(trigger, &memory, 0); + + /* condition is true */ + ram[4] = 60; + assert_evaluate_trigger(trigger, &memory, 1); +} + +static void test_remember_recall_self_in_addaddress() +{ + uint8_t ram[] = { 0x02, 0x03, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[1024]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(byte(0x0000) + 1) == 60 */ + assert_parse_trigger(&trigger, buffer, "K:0xH0000_K:{recall}_I:{recall}_0xH0001=60"); + + /* byte(2 + 1) => byte(3) == 60 */ + assert_evaluate_trigger(trigger, &memory, 0); + + /* condition is true */ + ram[3] = 60; + assert_evaluate_trigger(trigger, &memory, 1); + + /* byte(3 + 1) => byte(4) == 60 */ + ram[0] = 3; + assert_evaluate_trigger(trigger, &memory, 0); + + /* condition is true */ + ram[4] = 60; + assert_evaluate_trigger(trigger, &memory, 1); + + /* byte(2 + 1) => byte(3) == 60 */ + ram[0] = 2; + assert_evaluate_trigger(trigger, &memory, 1); + + /* condition is false */ + ram[3] = 9; + assert_evaluate_trigger(trigger, &memory, 0); +} + +/* ======================================================== */ + +static void test_trailing_andnext() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002)=1 + * AndNext byte(0x0001)=1 -- ignored as compound condition is incomplete + */ + assert_parse_trigger(&trigger, buffer, "0xH0002=1_N:0xH0001=1"); + ASSERT_PTR_NOT_NULL(trigger->requirement->conditions); + ASSERT_PTR_NOT_NULL(trigger->requirement->conditions->next); + ASSERT_PTR_NULL(trigger->requirement->conditions->next->next); + + /* both conditions false */ + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); +} + +static void test_trailing_andnext_with_alt() { + uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; + memory_t memory; + rc_trigger_t* trigger; + char buffer[512]; + + memory.ram = ram; + memory.size = sizeof(ram); + + /* byte(0x0002)=1 + * AndNext byte(0x0001)=1 -- ignored as compound condition is incomplete + */ + assert_parse_trigger(&trigger, buffer, "0xH0002=1_N:0xH0001=1SR:0xH0003=1"); + ASSERT_PTR_NOT_NULL(trigger->requirement->conditions); + ASSERT_PTR_NOT_NULL(trigger->requirement->conditions->next); + ASSERT_PTR_NULL(trigger->requirement->conditions->next->next); + ASSERT_PTR_NOT_NULL(trigger->alternative->conditions); + ASSERT_PTR_NULL(trigger->alternative->conditions->next); + + /* both conditions false */ + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition true */ + ram[1] = 1; + assert_evaluate_trigger(trigger, &memory, 0); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + + /* second condition not true */ + ram[1] = 2; + assert_evaluate_trigger(trigger, &memory, 0); + + /* first condition true */ + ram[2] = 1; + assert_evaluate_trigger(trigger, &memory, 1); + + /* first condition not true */ + ram[2] = 2; + assert_evaluate_trigger(trigger, &memory, 0); +} + +/* ======================================================== */ + +void test_trigger(void) { + TEST_SUITE_BEGIN(); + + /* alt groups */ + TEST(test_alt_groups); + TEST(test_empty_core); + TEST(test_empty_alt); + TEST(test_empty_last_alt); + TEST(test_empty_all_alts); + + /* resetif */ + TEST(test_resetif_in_alt_group); + + /* pauseif */ + TEST(test_pauseif_in_alt_group); + TEST(test_pauseif_resetif_in_alt_group); + TEST(test_pauseif_hitcount_with_reset); + + /* measured */ + TEST(test_measured); + TEST(test_measured_as_percent); + TEST(test_measured_comparison); + TEST(test_measured_addhits); + TEST(test_measured_indirect); + TEST(test_measured_multiple); + TEST(test_measured_multiple_with_hitcount_in_core); + TEST(test_measured_while_paused); + TEST(test_measured_while_paused_multiple); + TEST(test_measured_while_paused_reset_alt); + TEST(test_measured_while_paused_reset_core); + TEST(test_measured_while_paused_reset_non_hitcount); + TEST(test_measured_while_paused_extra_alts); + TEST(test_measured_reset_hitcount); + TEST(test_measured_reset_comparison); + TEST(test_measured_if); + TEST(test_measured_if_comparison); + TEST(test_measured_if_multiple_measured); + TEST(test_measured_if_multiple_measured_if); + TEST(test_measured_if_while_paused); + TEST(test_measured_trigger); + + /* trigger */ + TEST(test_resetnextif_trigger); + + /* rc_evaluate_trigger */ + TEST(test_evaluate_trigger_inactive); + TEST(test_evaluate_trigger_waiting); + TEST(test_evaluate_trigger_reset); + TEST(test_evaluate_trigger_reset_next); + TEST(test_evaluate_trigger_triggered); + TEST(test_evaluate_trigger_paused); + TEST(test_evaluate_trigger_primed); + TEST(test_evaluate_trigger_primed_in_alts); + TEST(test_evaluate_trigger_primed_one_alt); + TEST(test_evaluate_trigger_disabled); + TEST(test_evaluate_trigger_chained_resetnextif); + + /* memref sharing */ + TEST(test_prev_prior_share_memref); + TEST(test_bit_lookups_share_memref); + TEST(test_bitcount_shares_memref); + TEST(test_large_memref_not_shared); + + /* accumulator - remember and recall*/ + TEST(test_remember_recall); + TEST(test_remember_recall_separate_accumulator_per_group); + TEST(test_remember_recall_separate_accumulator_per_group_complex); + TEST(test_remember_recall_use_same_value_multiple); + TEST(test_remember_recall_in_pause_and_main); + TEST(test_remember_recall_in_pause_with_chain); + TEST(test_remember_recall_in_addaddress); + TEST(test_remember_recall_self_in_addaddress); + + /* incomplete logic */ + TEST(test_trailing_andnext); + TEST(test_trailing_andnext_with_alt); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rcheevos/test_value.c b/src/rcheevos/test/rcheevos/test_value.c new file mode 100644 index 0000000000..0cffe9bf46 --- /dev/null +++ b/src/rcheevos/test/rcheevos/test_value.c @@ -0,0 +1,868 @@ +#include "rc_internal.h" + +#include "../test_framework.h" +#include "mock_memory.h" + +static void test_evaluate_value(const char* memaddr, int expected_value) { + rc_value_t* self; + /* bytes 5-8 are the float value for pi */ + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; + memory_t memory; + char buffer[2048]; + unsigned* overflow; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + overflow = (unsigned*)(((char*)buffer) + ret); + *overflow = 0xCDCDCDCD; + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + ret = rc_evaluate_value(self, peek, &memory, NULL); + ASSERT_NUM_EQUALS(ret, expected_value); +} + +static void test_invalid_value(const char* memaddr, int expected_error) { + int ret = rc_value_size(memaddr); + ASSERT_NUM_EQUALS(ret, expected_error); +} + +static void test_measured_value_target(const char* memaddr, int expected_target) { + rc_value_t* self; + char buffer[2048]; + unsigned* overflow; + int ret; + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + overflow = (unsigned*)(((char*)buffer) + ret); + *overflow = 0xCDCDCDCD; + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + if (*overflow != 0xCDCDCDCD) { + ASSERT_FAIL("write past end of buffer"); + } + + ASSERT_NUM_EQUALS(self->conditions->conditions->required_hits, expected_target); +} + +static void test_evaluate_measured_value_with_pause() { + rc_value_t* self; + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + char buffer[2048]; + const char* memaddr = "P:0xH0003=hAB_M:0xH0002!=d0xH0002"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + /* should initially be paused, no hits captured */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* pause should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* unpause should not report the change that occurred while paused */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* pause should return current hitcount */ + ram[3] = 0xAB; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* pause should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* unpause should not report the change that occurred while paused */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* additional hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 2); +} + +static void test_evaluated_and_next_measured_if_value() { + rc_value_t* self; + const rc_condition_t* cond2; + const rc_condition_t* cond4; + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + char buffer[2048]; + const char* memaddr = "R:0xH0004=1_N:0xH0000=1.1._N:d0xH0000=9_Q:0xH0000=0.1._M:100"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + cond2 = self->conditions->conditions->next; + cond4 = cond2->next->next; + + /* measured if cannot be true */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + ASSERT_NUM_EQUALS(cond2->current_hits, 0); + ASSERT_NUM_EQUALS(cond4->current_hits, 0); + + /* capture first hit, measured_if still not true */ + ram[0] = 1; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + ASSERT_NUM_EQUALS(cond2->current_hits, 1); + ASSERT_NUM_EQUALS(cond4->current_hits, 0); + + /* reset */ + ram[4] = 1; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + ASSERT_NUM_EQUALS(cond2->current_hits, 0); + ASSERT_NUM_EQUALS(cond4->current_hits, 0); + + /* clear reset */ + ram[4] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + ASSERT_NUM_EQUALS(cond2->current_hits, 1); + ASSERT_NUM_EQUALS(cond4->current_hits, 0); + + /* prime measured_if */ + ram[0] = 9; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + ASSERT_NUM_EQUALS(cond2->current_hits, 1); + ASSERT_NUM_EQUALS(cond4->current_hits, 0); + + /* trigger measured if */ + ram[0] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 100); + ASSERT_NUM_EQUALS(cond2->current_hits, 1); + ASSERT_NUM_EQUALS(cond4->current_hits, 1); + + /* measured if should remain triggered */ + ram[0] = 1; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 100); + ASSERT_NUM_EQUALS(cond2->current_hits, 1); + ASSERT_NUM_EQUALS(cond4->current_hits, 1); + + /* reset */ + ram[4] = 1; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + ASSERT_NUM_EQUALS(cond2->current_hits, 0); + ASSERT_NUM_EQUALS(cond4->current_hits, 0); +} + +static void test_evaluate_measured_value_with_reset() { + rc_value_t* self; + uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; + memory_t memory; + char buffer[2048]; + const char* memaddr = "R:0xH0003=hAB_M:0xH0002!=d0xH0002"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + /* reset should initially be true, no hits captured */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset no longer true, change while reset shouldn't be captured */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* additional hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* reset should clear hit count */ + ram[3] = 0xAB; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset should prevent hitcount */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* reset no longer true, change while reset shouldn't be captured */ + ram[3] = 0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* additional hitcount should be captured */ + ram[2]++; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); +} + +static void init_typed_value(rc_typed_value_t* value, uint8_t type, uint32_t u32, double f32) { + value->type = type; + + switch (type) { + case RC_VALUE_TYPE_UNSIGNED: + value->value.u32 = u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = (int)u32; + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = (float)f32; + break; + + case RC_VALUE_TYPE_NONE: + value->value.u32 = 0xCDCDCDCD; /* force uninitialized value */ + break; + + default: + break; + } +} + +static void _assert_typed_value(const rc_typed_value_t* value, uint8_t type, uint32_t u32, double f32) { + ASSERT_NUM_EQUALS(value->type, type); + + switch (type) { + case RC_VALUE_TYPE_UNSIGNED: + ASSERT_NUM_EQUALS(value->value.u32, u32); + break; + + case RC_VALUE_TYPE_SIGNED: + ASSERT_NUM_EQUALS(value->value.i32, (int)u32); + break; + + case RC_VALUE_TYPE_FLOAT: + ASSERT_FLOAT_EQUALS(value->value.f32, (float)f32); + break; + + default: + break; + } +} +#define assert_typed_value(value, type, u32, f32) ASSERT_HELPER(_assert_typed_value(value, type, u32, f32), "assert_typed_value") + +static void test_typed_value_convert(uint8_t type, uint32_t u32, double f32, uint8_t new_type, uint32_t new_u32, double new_f32) { + rc_typed_value_t value; + init_typed_value(&value, type, u32, f32); + + rc_typed_value_convert(&value, new_type); + + assert_typed_value(&value, new_type, new_u32, new_f32); +} + +static void test_typed_value_conversion() { + /* unsigned source */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4294967295.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* signed source */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_FLOAT, 0, -1.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_FLOAT, 0, -1.0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 3.14159); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 3, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, 3, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0); + + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_FLOAT, 0, -3.14159); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFD, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-3, 0); + TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_NONE, 0, 0); +} + +static void test_typed_value_add(uint8_t type, uint32_t u32, double f32, + uint8_t amount_type, uint32_t amount_u32, double amount_f32, uint32_t result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_add(&value, &amount); + + if (result_f32 != 0.0) { + assert_typed_value(&value, RC_VALUE_TYPE_FLOAT, result_u32, result_f32); + } + else if (type == RC_VALUE_TYPE_NONE) { + assert_typed_value(&value, amount_type, result_u32, result_f32); + } + else { + assert_typed_value(&value, type, result_u32, result_f32); + } +} + +static void test_typed_value_addition() { + /* no source */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 8, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, 0, 8.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 0xFFFFFFFE, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, (unsigned)-2, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, 0, -2.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 0.0); + + /* unsigned source */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 14, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, 0, 14.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, 0, 4.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, 6, 0.0); + + /* signed source */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, (unsigned)-4, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, -4.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 2, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, (unsigned)-8, 0.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, -4.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, (unsigned)-6, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, 0, 8.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, 8.0); + + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 0, 4294967300.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 0, 4.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, 0, -2.0); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 6.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, 0, 5.14159); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 0, 1.14159); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, 5.14159); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, 0, 9.16459); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, 0, -4.85841); + TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 3.14159); +} + +static void test_typed_value_multiply(char type, uint32_t u32, double f32, + uint8_t amount_type, uint32_t amount_u32, double amount_f32, + uint8_t result_type, uint32_t result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_multiply(&value, &amount); + + assert_typed_value(&value, result_type, result_u32, result_f32); +} + +static void test_typed_value_multiplication() { + /* unsigned source */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, RC_VALUE_TYPE_UNSIGNED, 48, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, RC_VALUE_TYPE_FLOAT, 0, 48.0); + + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFF4, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFF4, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* signed source */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-12, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); + + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 25769803764.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -48.0); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 6.28318); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -6.28318); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 6.28318); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 18.92179657); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -25.13272); + TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); +} + +static void test_typed_value_divide(uint8_t type, uint32_t u32, double f32, + uint8_t amount_type, uint32_t amount_u32, double amount_f32, + uint8_t result_type, uint32_t result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_divide(&value, &amount); + + assert_typed_value(&value, result_type, result_u32, result_f32); +} + +static void test_typed_value_division() { + /* division by zero */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* unsigned source */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_UNSIGNED, 3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.5, RC_VALUE_TYPE_FLOAT, 0, 2.4); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); + + /* signed source */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_SIGNED, 3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_SIGNED, 3, 0.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + + /* float source (whole numbers) */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); + + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.00000000139698386); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -0.75); + + /* float source (non-whole numbers) */ + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 1.570795); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -1.570795); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 1.570795); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 0.52159887099); + TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -0.39269875); +} + +static void test_typed_value_mod(uint8_t type, uint32_t u32, double f32, + uint8_t amount_type, uint32_t amount_u32, double amount_f32, + uint8_t result_type, uint32_t result_u32, double result_f32) { + rc_typed_value_t value, amount; + + init_typed_value(&value, type, u32, f32); + init_typed_value(&amount, amount_type, amount_u32, amount_f32); + + rc_typed_value_modulus(&value, &amount); + + assert_typed_value(&value, result_type, result_u32, result_f32); +} + +static void test_typed_value_modulus() { + /* modulus with zero divisor */ + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); + + /* unsigned source */ + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 4, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.25, RC_VALUE_TYPE_FLOAT, 0, 1.5); + + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFC, 0.0, RC_VALUE_TYPE_UNSIGNED, 6, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-4, 0.0, RC_VALUE_TYPE_UNSIGNED, 6, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -4.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); + + /* signed source */ + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 4, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4.0, RC_VALUE_TYPE_FLOAT, 0, -2.0); + + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFC, 0.0, RC_VALUE_TYPE_SIGNED, -2, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-4, 0.0, RC_VALUE_TYPE_SIGNED, -2, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -4.0, RC_VALUE_TYPE_FLOAT, 0, -2.0); + + /* float source (whole numbers) */ + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 4, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 4.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); + + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 6.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -5.0, RC_VALUE_TYPE_FLOAT, 0, 1.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 1.141590); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-4, 0.0, RC_VALUE_TYPE_FLOAT, 0, 3.141590); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 1.141590); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 1.570795, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 3.141590); + TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, 1.141590); +} + +static void test_typed_value_negate(char type, int i32, double f32, char expected_type, signed result_i32, double result_f32) { + rc_typed_value_t value; + + init_typed_value(&value, type, (unsigned)i32, f32); + + rc_typed_value_negate(&value); + + assert_typed_value(&value, expected_type, (unsigned)result_i32, result_f32); +} + +static void test_typed_value_negation() { + /* unsigned source */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 99, 0.0, RC_VALUE_TYPE_SIGNED, -99, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_SIGNED, 1, 0.0); + + /* signed source */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, 99, 0.0, RC_VALUE_TYPE_SIGNED, -99, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, -1, 0.0, RC_VALUE_TYPE_SIGNED, 1, 0.0); + + /* float source (whole numbers) */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 99.0, RC_VALUE_TYPE_FLOAT, 0, -99.0); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_FLOAT, 0, 1.0); + + /* float source (non-whole numbers) */ + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 0.1, RC_VALUE_TYPE_FLOAT, 0, -0.1); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -3.14159); + TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, -2.7, RC_VALUE_TYPE_FLOAT, 0, 2.7); +} + +static void test_addhits_float_coercion() { + rc_value_t* self; + uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F }; /* fF0004 = 1.5 */ + memory_t memory; + char buffer[2048]; + /* measured(tally(0, (0 + float(4) * 10 - prev(float(4)) * 10) == 1)) */ + const char* memaddr = "A:0_A:fF0004*10_B:dfF0004*10_C:0=1_M:0=1"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + /* The 0+ at the start of the expression changes the accumulator to an integer, + * so when float(4)*10 is added and prev(float(4))*10 is subtracted, they'll also + * be converted to integers before they're combined. + */ + + /* float(4) = 1.5, prev(float(4)) = 0.0. 0+15-0=1 is false => 0 */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* float(4) = 1.75, prev(float(4)) = 1.5. 0+17-15 => 2 => 2=1 is false => 0 */ + ram[7] = 0x3f; ram[6] = 0xe0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* float(4) = 1.82, prev(float(4)) = 1.75. 0+18-17 => 1 => 1=1 is true => 1 */ + ram[6] = 0xe8; ram[5] = 0xf5; ram[4] = 0xc3; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* float(4) = 2.06, prev(float(4)) = 1.82. 0+20-18 => 2 => 2=1 is false => 1 */ + ram[7] = 0x40; ram[6] = 0x03; ram[5] = 0xd7; ram[4] = 0x0a; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); +} + +static void test_addhits_float_coercion_remembered() { + rc_value_t* self; + uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F }; /* fF0004 = 1.5 */ + memory_t memory; + char buffer[2048]; + /* measured(tally(0, remembered(0 - prev(float(4)) * 10) + (0 + float(4) * 10) == 1)) */ + const char* memaddr = "A:0_B:dfF0004*10_K:0_A:0_A:fF0004*10_C:{recall}=1_M:0=1"; + int ret; + + memory.ram = ram; + memory.size = sizeof(ram); + + ret = rc_value_size(memaddr); + ASSERT_NUM_GREATER_EQUALS(ret, 0); + + self = rc_parse_value(buffer, memaddr, NULL, 0); + ASSERT_PTR_NOT_NULL(self); + + /* using remember allows for both sides to explicitly be cast to integer before + * performing the subtraction. */ + + /* float(4) = 1.5, prev(float(4)) = 0.0. 15-0 => 15=1 is false => 0 */ + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* float(4) = 1.75, prev(float(4)) = 1.5. 17-15 => 2=1 is false => 0 */ + ram[7] = 0x3f; ram[6] = 0xe0; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); + + /* float(4) = 1.82, prev(float(4)) = 1.75. 18-17 => 1=1 is true => 1 */ + ram[6] = 0xe8; ram[5] = 0xf5; ram[4] = 0xc3; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); + + /* float(4) = 2.06, prev(float(4)) = 1.82. 20-18 => 2=1 is false => 1 */ + ram[7] = 0x40; ram[6] = 0x03; ram[5] = 0xd7; ram[4] = 0x0a; + ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); +} + +void test_value(void) { + TEST_SUITE_BEGIN(); + + /* ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; */ + + /* classic format - supports multipliers, max, inversion */ + TEST_PARAMS2(test_evaluate_value, "V6", 6); + TEST_PARAMS2(test_evaluate_value, "V6*2", 12); + TEST_PARAMS2(test_evaluate_value, "V6*0.5", 3); + TEST_PARAMS2(test_evaluate_value, "V-6", -6); + TEST_PARAMS2(test_evaluate_value, "V-6*2", -12); + + TEST_PARAMS2(test_evaluate_value, "0xH0001_0xH0002", 0x12 + 0x34); + TEST_PARAMS2(test_evaluate_value, "0xH0001*100_0xH0002*0.5_0xL0003", 0x12 * 100 + 0x34 / 2 + 0x0B); + TEST_PARAMS2(test_evaluate_value, "0xH0001$0xH0002", 0x34); + TEST_PARAMS2(test_evaluate_value, "0xH0001_0xH0004*3$0xH0002*0xL0003", 0x34 * 0x0B); + TEST_PARAMS2(test_evaluate_value, "0xH0001_V-20", 0x12 - 20); + TEST_PARAMS2(test_evaluate_value, "0xH0001_H10", 0x12 + 0x10); + TEST_PARAMS2(test_evaluate_value, "100-0xH0002", 100 - 0x34); + TEST_PARAMS2(test_evaluate_value, "0xh0000*-1_99_0xh0001*-100_5900", 4199); + TEST_PARAMS2(test_evaluate_value, "v5900_0xh0000*-1.0_0xh0001*-100.0", 4100); + TEST_PARAMS2(test_evaluate_value, "v5900_0xh0000*v-1_0xh0001*v-100", 4100); + + TEST_PARAMS2(test_evaluate_value, "0xH01*4", 0x12 * 4); /* multiply by constant */ + TEST_PARAMS2(test_evaluate_value, "0xH01*0.5", 0x12 / 2); /* multiply by fraction */ + TEST_PARAMS2(test_evaluate_value, "0xH01/2", 0x12 / 2); /* divide by constant */ + TEST_PARAMS2(test_evaluate_value, "0xH01*0xH02", 0x12 * 0x34); /* multiply by second address */ + TEST_PARAMS2(test_evaluate_value, "0xH01*0xT02", 0); /* multiply by bit */ + TEST_PARAMS2(test_evaluate_value, "0xH01*~0xT02", 0x12); /* multiply by inverse bit */ + TEST_PARAMS2(test_evaluate_value, "0xH01*~0xH02", 0x12 * (0x34 ^ 0xff)); /* multiply by inverse byte */ + + TEST_PARAMS2(test_evaluate_value, "B0xH01", 12); + TEST_PARAMS2(test_evaluate_value, "B0x00001", 3412); + TEST_PARAMS2(test_evaluate_value, "B0xH03", 111); /* 0xAB not really BCD */ + TEST_PARAMS2(test_evaluate_value, "B0xW00", 341200); + + /* non-comparison measured values just return the value at the address and have no target */ + TEST_PARAMS2(test_measured_value_target, "M:0xH0002", 0); + + /* hitcount based measured values always have unbounded targets, even if one is specified */ + TEST_PARAMS2(test_measured_value_target, "M:0xH0002!=d0xH0002", (unsigned)-1); + TEST_PARAMS2(test_measured_value_target, "M:0xH0002!=d0xH0002.99.", (unsigned)-1); + /* measured values always assumed to be hitcount based - they do not stop/trigger when the condition is met */ + TEST_PARAMS2(test_measured_value_target, "M:0xH0002<100", (unsigned)-1); + + /* measured format - supports hit counts and combining flags + * (AddSource, SubSource, AddHits, SubHits, AndNext, OrNext, and AddAddress) */ + TEST_PARAMS2(test_evaluate_value, "M:0xH0002", 0x34); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001_M:0xH0002", 0x12 + 0x34); + TEST_PARAMS2(test_evaluate_value, "B:0xH0001_M:0xH0002", 0x34 - 0x12); + TEST_PARAMS2(test_evaluate_value, "C:0xH0000=0_M:0xH0002=52", 2); + TEST_PARAMS2(test_evaluate_value, "C:0xH0000=0_D:0xH0001=18_M:0xH0002=52", 1); + TEST_PARAMS2(test_evaluate_value, "N:0xH0000=0_M:0xH0002=52", 1); + TEST_PARAMS2(test_evaluate_value, "O:0xH0000=0_M:0xH0002=0", 1); + TEST_PARAMS2(test_evaluate_value, "I:0xH0000_M:0xH0002", 0x34); + + TEST_PARAMS2(test_evaluate_value, "M:0xH0002*2", 0x34 * 2); + TEST_PARAMS2(test_evaluate_value, "M:0xH0002/2", 0x34 / 2); + TEST_PARAMS2(test_evaluate_value, "M:0xH0002%2", 0x34 % 2); + TEST_PARAMS2(test_evaluate_value, "M:0xH0001%3", 0x12 % 3); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001*2_A:0xH0002*2_M:0", 0x12 * 2 + 0x34 * 2); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%3_A:0xH0002%4_M:0", 0x12 % 3 + 0x34 % 4); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001*2_M:0xH0002*2", 0x12 * 2 + 0x34 * 2); /* multiplier in final condition */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/2_M:0xH0002/2", 0x12 / 2 + 0x34 / 2); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%3_M:0xH0002%4", 0x12 % 3 + 0x34 % 4); + TEST_PARAMS2(test_evaluate_value, "A:0xH0001&15_M:0xH0002&15", (0x12 & 15) + (0x34 & 15)); + + /* measured format does not support alt groups */ + TEST_PARAMS2(test_invalid_value, "M:0xH0002=6SM:0xH0003=6", RC_INVALID_VALUE_FLAG); + /* does not start with X:, so legacy parser says it's an invalid memory accessor */ + TEST_PARAMS2(test_invalid_value, "SM:0xH0002=6SM:0xH0003=6", RC_INVALID_MEMORY_OPERAND); + + /* measured format does not support trigger flag */ + TEST_PARAMS2(test_invalid_value, "T:0xH0002=6", RC_INVALID_VALUE_FLAG); + + /* measured format requires a measured condition */ + TEST_PARAMS2(test_invalid_value, "A:0xH0002_0xH0003>10.99.", RC_INVALID_VALUE_FLAG); /* no flag on condition 2 */ + TEST_PARAMS2(test_invalid_value, "A:0xH0002_A:0xH0003", RC_MISSING_VALUE_MEASURED); + + /* measured value with float data */ + TEST_PARAMS2(test_evaluate_value, "M:fF0005", 3); /* 3.141592 -> 3 */ + TEST_PARAMS2(test_evaluate_value, "A:fF0005*10_M:0", 31); /* 3.141592 x 10 -> 31.415 -> 31 */ + TEST_PARAMS2(test_evaluate_value, "A:fF0005*f11.2_M:f6.9", 42); /* 3.141592 x 11.2 -> 35.185 + 6.9 -> -> 42.085 -> 42 */ + TEST_PARAMS2(test_evaluate_value, "A:fF0005*f5.555555555555555555555555555555555555555555555556_M:f6.9", 24); /* 3.141592 x 5.555556 -> 17.4532902 + 6.9 -> -> 24.353290 -> 24 */ + + /* delta should initially be 0, so a hit will be tallied */ + TEST_PARAMS2(test_evaluate_value, "M:0xH0002!=d0xH0002", 1); + + /* division */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/2_M:0", 9); /* 18/2 = 9 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/5_M:0", 3); /* 18/5 = 3 */ + TEST_PARAMS2(test_evaluate_value, "M:0xH0001/5", 3); /* 18/5 = 3 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0002/0xH0001_M:0", 2); /* 52/18 = 2 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0002_M:0", 0); /* 18/52 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0001_M:0", 1); /* 18/18 = 1 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0000_M:0", 0); /* 18/0 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000/0xH0000_M:0", 0); /* 0/0 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000/0xH0001_M:0", 0); /* 0/18 = 0 */ + + /* modulus */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%3_M:0", 0); /* 18%3 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%5_M:0", 3); /* 18%5 = 3 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%7_M:0", 4); /* 18%7 = 4 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0002%0xH0001_M:0", 16); /* 52%18 = 16 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%0xH0002_M:0", 18); /* 18%52 = 18 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%0xH0001_M:0", 0); /* 18%18 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001%0xH0000_M:0", 0); /* 18%0 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000%0xH0000_M:0", 0); /* 0%0 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000%0xH0001_M:0", 0); /* 0%18 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0000%0xH0001_M:0", 0); /* 0%18 = 0 */ + TEST_PARAMS2(test_evaluate_value, "A:f5.5%f2.0_M:0", 1) /* 5.5 % 2.0 = 1.5 -> 1 */ + TEST_PARAMS2(test_evaluate_value, "A:f123.7%f5.65_M:0", 5) /* 123.7 % 5.65 = 5.05 -> 5 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH01%f7.5_A:0xH02%f5.5_M:0", 5); /* 18%7.3 = 3.4, 52%5.5=2.5, becomes 3+2=5*/ + + /* addition */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001+3_M:0", 21); /* 18+3 = 21 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0002+0xH0001_M:0", 70); /* 52+18 = 70 */ + TEST_PARAMS2(test_evaluate_value, "A:fF5+f2_M:0", 5) /* PI + 2.0 = 5.141592 -> 5 */ + TEST_PARAMS2(test_evaluate_value, "A:f5.5+f2.0_M:0", 7) /* 5.5 + 2.0 = 7.5 -> 7 */ + TEST_PARAMS2(test_evaluate_value, "A:f5.5+f2.7_M:0", 8) /* 5.5 + 2.7 = 8.2 -> 8 */ + TEST_PARAMS2(test_evaluate_value, "B:0xH0001+3_M:100", 79) /* 100 - (18+3) = 79 */ + TEST_PARAMS2(test_evaluate_value, "I:0xH0000+3_M:0x0", 0x56AB) /* Add Address (0+3) -> Offset 0. 16-Bit Read @ Byte 3 = 0x56AB */ + + /* subtraction */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0001-3_M:0", 15); /* 18-3 = 15 */ + TEST_PARAMS2(test_evaluate_value, "A:0xH0002-0xH0001_M:0", 34); /* 52-18 = 34 */ + TEST_PARAMS2(test_evaluate_value, "A:fF5-f2_M:0", 1) /* PI - 2.0 = 1.141592 -> 1 */ + TEST_PARAMS2(test_evaluate_value, "A:f5.5-f2.0_M:0", 3) /* 5.5 - 2.0 = 2.5 -> 3 */ + TEST_PARAMS2(test_evaluate_value, "A:f5.5-f2.7_M:0", 2) /* 5.5 - 2.7 = 2.8 -> 2 */ + TEST_PARAMS2(test_evaluate_value, "B:0xH0001-3_M:100",85) /* 100 - (18-3) = 85 */ + + /* rounding */ + TEST_PARAMS2(test_evaluate_value, "0xH03/2_0xH03/2", 0xAA); /* integer division results in rounding */ + TEST_PARAMS2(test_evaluate_value, "0xH03/f2.0_0xH03/f2.0", 0xAB); /* float division does not result in rounding */ + TEST_PARAMS2(test_evaluate_value, "0xH03*0.5_0xH03*0.5", 0xAB); /* float multiplication does not result in rounding */ + TEST_PARAMS2(test_evaluate_value, "A:0xH03/2_A:0xH03/2_M:0", 0xAA); /* integer division results in rounding */ + TEST_PARAMS2(test_evaluate_value, "A:0xH03/f2.0_A:0xH03/f2.0_M:0", 0xAB); /* float division does not result in rounding */ + TEST_PARAMS2(test_evaluate_value, "I:0xH0001-17_M:0x0", 0x3412) /* Add Address (18-17) -> Offset 0. 16-Bit Read @ Byte 1 = 0x3412 */ + + /* using measured_if */ + TEST_PARAMS2(test_evaluate_value, "Q:0xH0001!=0_M:0xH0002", 0x34); + TEST_PARAMS2(test_evaluate_value, "Q:0xH0001=0_M:0xH0002", 0); + TEST_PARAMS2(test_evaluate_value, "Q:0xH0001!=0_M:1", 1); + TEST(test_evaluated_and_next_measured_if_value); + + /* using accumulator */ + TEST_PARAMS2(test_evaluate_value, "K:0xH01_M:{recall}", 0x12); /* 18-> recall accumulator, Measurement = 18 */ + TEST_PARAMS2(test_evaluate_value, "K:0xH01_K:{recall}*2_M:{recall}", 0x24); /* 18-> recall accumulator, recall accumulator*2 -> recall accumulator, Measurement 18*2 = 36 */ + TEST_PARAMS2(test_evaluate_value, "K:0xH01*0xH02_M:{recall}", 0x3A8); /* 18*52-> recall accumulator, Measurement = 936 */ + TEST_PARAMS2(test_evaluate_value, "A:4_K:0xH01_K:{recall}*2_M:{recall}", 44); /* Chain Addsource into Remember (4 + 18) * 2 = 44 */ + TEST_PARAMS2(test_evaluate_value, "A:4_K:2*8_M:{recall}", 20); /* Chain Addsource into Remember 4 + (2 * 8) = 20 */ + TEST_PARAMS2(test_evaluate_value, "A:4_K:2*8_A:{recall}*2_M:4*{recall}", 120); /* Use remembered value multiple times */ + TEST_PARAMS2(test_evaluate_value, "K:0xH01*2_Q:{recall}<40_P:{recall}=36_M:{recall}", 36); /* Pause happens before recall accumulator is set because remember not part of pause chain. */ + TEST_PARAMS2(test_evaluate_value, "K:0xH01*2_P:{recall}=18_M:{recall}", 36); /* Measures the accumulated value, which was set in the pause pass. */ + TEST_PARAMS2(test_evaluate_value, "K:1_I:{recall}_M:0x02", 0x56AB); /* using recall accumulator as pointer */ + TEST_PARAMS2(test_evaluate_value, "K:1_I:{recall}_K:0x02_M:{recall}", 0x56AB); /* Use recall accumulator as pointer, then store pointed-to data in recall accumulator and measure that */ + TEST_PARAMS2(test_evaluate_value, "K:5_C:{recall}>3_C:{recall}<7_C:{recall}>5_M:0=1", 2); /* with addhits, reusing the recall accumulator in each. */ + TEST_PARAMS2(test_evaluate_value, "K:5_A:1_M:{recall}", 6); /* Add Source onto a read of the recall accumulator as the value */ + TEST_PARAMS2(test_evaluate_value, "K:5_A:1_C:{recall}=6_B:1_C:{recall}=4_M:0=1", 2); + TEST_PARAMS2(test_evaluate_value, "A:{recall}_M:1", 1); /* Using recall without remember. 0 + 1 = 1*/ + TEST_PARAMS2(test_evaluate_value, "K:{recall}^255_M:{recall}", 255); /* Using recall in first remember. 0x00^0xFF = 0xFF */ + TEST_PARAMS2(test_evaluate_value, "A:1_K:2_M:{recall}", 3); /* Remembered value includes AddSource chain */ + + /* pause and reset affect hit count */ + TEST(test_evaluate_measured_value_with_pause); + TEST(test_evaluate_measured_value_with_reset); + + /* overflow - 145406052 * 86 = 125049208332 -> 0x1D1D837E0C, leading 0x1D is truncated off */ + TEST_PARAMS2(test_evaluate_value, "0xX0001*0xH0004", 0x1D837E0C); + + test_typed_value_conversion(); + test_typed_value_addition(); + test_typed_value_multiplication(); + test_typed_value_division(); + test_typed_value_modulus(); + test_typed_value_negation(); + + test_addhits_float_coercion(); + test_addhits_float_coercion_remembered(); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/data.c b/src/rcheevos/test/rhash/data.c new file mode 100644 index 0000000000..0161ec5b4e --- /dev/null +++ b/src/rcheevos/test/rhash/data.c @@ -0,0 +1,657 @@ +#include "data.h" + +#include "../src/rc_compat.h" + +#include +#include + +void fill_image(uint8_t* image, size_t size) +{ + int seed = (int)(size ^ (size >> 8) ^ ((size - 1) * 25387)); + int count; + uint8_t value; + + while (size > 0) + { + switch (seed & 0xFF) + { + case 0: + count = (((seed >> 8) & 0x3F) & ~(size & 0x0F)); + if (count == 0) + count = 1; + value = 0; + break; + + case 1: + count = ((seed >> 8) & 0x07) + 1; + value = ((seed >> 16) & 0xFF); + break; + + case 2: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0xFF; + break; + + case 3: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0xA5; + break; + + case 4: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0xC3; + break; + + case 5: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0x96; + break; + + case 6: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0x78; + break; + + case 7: + count = ((seed >> 8) & 0x03) + 1; + value = ((seed >> 16) & 0xFF) ^ 0x78; + break; + + default: + count = 1; + value = ((seed >> 8) ^ (seed >> 16)) & 0xFF; + break; + } + + do + { + *image++ = value; + --size; + } while (size && --count); + + /* state mutation from psuedo-random number generator */ + seed = (seed * 0x41C64E6D + 12345) & 0x7FFFFFFF; + } +} + +uint8_t* generate_gamecube_iso(size_t mb, size_t* image_size) +{ + uint8_t* image; + const size_t size_needed = mb * 1024 * 1024; + int ix; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) + { + uint32_t apploader_sizes_addr = 0x2440 + 0x14; + uint32_t dol_offset_addr = 0x420; + uint32_t dol_sizes_addr = 0x3000; + + fill_image(image, size_needed); + + image[0x1c] = 0xC2; + image[0x1d] = 0x33; + image[0x1e] = 0x9F; + image[0x1f] = 0x3D; + + for (ix = 0; ix < 8; ix++) + { + /* 0x000000ff for both */ + image[apploader_sizes_addr + ix] = (ix % 4 == 3) ? 0xff : 0; + } + for (ix = 0; ix < 4; ix++) + { + /* 0x00003000 */ + image[dol_offset_addr + ix] = (ix % 4 == 2) ? 0x30 : 0; + } + for (ix = 0; ix < 18 * 4; ix++) + { + /* offsets start at 0x00003100 and increment */ + image[dol_sizes_addr + ix] = (ix % 4 == 2) ? (0x30 + 1 + ix / 4) : 0; + /* 0x000000ff for every other size */ + image[dol_sizes_addr + 0x90 + ix] = (ix % 8 == 3) ? 0xff : 0; + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +uint8_t* generate_3do_bin(uint32_t root_directory_sectors, uint32_t binary_size, size_t* image_size) +{ + const uint8_t volume_header[] = { + 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01, 0x00, /* header */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* comment */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 'C', 'D', '-', 'R', 'O', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* label */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x2D, 0x79, 0x6E, 0x96, /* identifier */ + 0x00, 0x00, 0x08, 0x00, /* block size */ + 0x00, 0x00, 0x05, 0x00, /* block count */ + 0x31, 0x5a, 0xf2, 0xe6, /* root directory identifier */ + 0x00, 0x00, 0x00, 0x01, /* root directory size in blocks */ + 0x00, 0x00, 0x08, 0x00, /* block size in root directory */ + 0x00, 0x00, 0x00, 0x06, /* number of copies of root directory */ + 0x00, 0x00, 0x00, 0x01, /* block location of root directory */ + 0x00, 0x00, 0x00, 0x01, /* block location of first copy of root directory */ + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, /* block location of last copy of root directory */ + }; + + const uint8_t directory_data[] = { + 0xFF, 0xFF, 0xFF, 0xFF, /* next block */ + 0xFF, 0xFF, 0xFF, 0xFF, /* previous block */ + 0x00, 0x00, 0x00, 0x00, /* flags */ + 0x00, 0x00, 0x00, 0xA4, /* end of block */ + 0x00, 0x00, 0x00, 0x14, /* start of block */ + + 0x00, 0x00, 0x00, 0x07, /* flags - directory */ + 0x00, 0x00, 0x00, 0x00, /* identifier */ + 0x00, 0x00, 0x00, 0x00, /* type */ + 0x00, 0x00, 0x08, 0x00, /* block size */ + 0x00, 0x00, 0x00, 0x00, /* length in bytes */ + 0x00, 0x00, 0x00, 0x00, /* length in blocks */ + 0x00, 0x00, 0x00, 0x00, /* burst */ + 0x00, 0x00, 0x00, 0x00, /* gap */ + 'f', 'o', 'l', 'd', 'e', 'r', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* filename */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x00, 0x00, 0x00, 0x00, /* extra copies */ + 0x00, 0x00, 0x00, 0x00, /* directory block address */ + + 0x00, 0x00, 0x00, 0x02, /* flags - file */ + 0x00, 0x00, 0x00, 0x00, /* identifier */ + 0x00, 0x00, 0x00, 0x00, /* type */ + 0x00, 0x00, 0x08, 0x00, /* block size */ + 0x00, 0x00, 0x00, 0x00, /* length in bytes */ + 0x00, 0x00, 0x00, 0x00, /* length in blocks */ + 0x00, 0x00, 0x00, 0x00, /* burst */ + 0x00, 0x00, 0x00, 0x00, /* gap */ + 'L', 'a', 'u', 'n', 'c', 'h', 'M', 'e', 0, 0, 0, 0, 0, 0, 0, 0, /* filename */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x00, 0x00, 0x00, 0x00, /* extra copies */ + 0x00, 0x00, 0x00, 0x02, /* directory block address */ + }; + + size_t size_needed = (root_directory_sectors + 1 + ((binary_size + 2047) / 2048)) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + size_t offset = 2048; + uint32_t i; + + if (!image) + return NULL; + + /* first sector - volume header */ + memcpy(image, volume_header, sizeof(volume_header)); + image[0x5B] = (uint8_t)root_directory_sectors; + + /* root directory sectors */ + for (i = 0; i < root_directory_sectors; ++i) + { + memcpy(&image[offset], directory_data, sizeof(directory_data)); + if (i < root_directory_sectors - 1) + { + image[offset + 0] = 0; + image[offset + 1] = 0; + image[offset + 2] = 0; + image[offset + 3] = (uint8_t)(i + 1); + + memcpy(&image[offset + 0x14 + 0x48 + 0x20], "filename", 8); + } + else + { + image[offset + 0x14 + 0x48 + 0x11] = (binary_size >> 16) & 0xFF; + image[offset + 0x14 + 0x48 + 0x12] = (binary_size >> 8) & 0xFF; + image[offset + 0x14 + 0x48 + 0x13] = (binary_size & 0xFF); + + image[offset + 0x14 + 0x48 + 0x16] = (((binary_size + 2047) / 2048) >> 8) & 0xFF; + image[offset + 0x14 + 0x48 + 0x17] = ((binary_size + 2047) / 2048) & 0xFF; + + image[offset + 0x14 + 0x48 + 0x47] = (uint8_t)(i + 2); + } + + if (i > 0) + { + image[offset + 4] = 0; + image[offset + 5] = 0; + image[offset + 6] = 0; + image[offset + 7] = (uint8_t)(i - 1); + } + + offset += 2048; + } + + /* binary data */ + fill_image(&image[offset], binary_size); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_dreamcast_bin(uint32_t track_first_sector, uint32_t binary_size, size_t* image_size) +{ + /* https://mc.pp.se/dc/ip0000.bin.html */ + const uint8_t volume_header[] = + "SEGA SEGAKATANA " + "SEGA ENTERPRISES" + "5966 GD-ROM1/1 " /* device info */ + " U 918FA01 " /* region and peripherals */ + "X-1234N V1.001" /* product number and version */ + "20200910 " /* release date */ + "1ST_READ.BIN " /* boot file */ + "RETROACHIEVEMENT" /* company name */ + "UNIT TEST " /* product name */ + " " + " " + " " + " " + " " + " " + " "; + + const uint8_t directory_data[] = { + 0x30, /* length of directory record */ + 0x00, /* extended attribute record length */ + 0xD9, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* first logical block of file */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* length in bytes */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* date/time */ + 0x00, 0x00, 0x00, /* flags, unit size, gap size */ + 0x00, 0x00, 0x00, 0x00, /* sequence number*/ + 0x0E, /* length of file identifier */ + '1', 'S', 'T', '_', 'R', 'E', 'A', 'D', '.', 'B', 'I', 'N', ';', '1', /* file identifier */ + }; + + const size_t binary_sectors = (binary_size + 2047) / 2048; + const size_t size_needed = (binary_sectors + 18) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + if (!image) + return NULL; + + /* volume header goes in sector 0 */ + memcpy(&image[0], volume_header, sizeof(volume_header)); + + /* directory information goes in sectors 16 and 17 */ + memcpy(&image[2048 * 16], "1CD001", 6); + image[2048 * 16 + 156 + 2] = 45017 & 0xFF; + image[2048 * 16 + 156 + 3] = (45017 >> 8) & 0xFF; + image[2048 * 16 + 156 + 4] = (45017 >> 16) & 0xFF; + memcpy(&image[2048 * 17], directory_data, sizeof(directory_data)); + + track_first_sector += 18; + image[2048 * 17 + 2] = (track_first_sector & 0xFF); + image[2048 * 17 + 3] = ((track_first_sector >> 8) & 0xFF); + image[2048 * 17 + 4] = ((track_first_sector >> 16) & 0xFF); + image[2048 * 17 + 10] = (binary_size & 0xFF); + image[2048 * 17 + 11] = ((binary_size >> 8) & 0xFF); + image[2048 * 17 + 12] = ((binary_size >> 16) & 0xFF); + image[2048 * 17 + 13] = ((binary_size >> 24) & 0xFF); + + /* binary data */ + fill_image(&image[2048 * 18], binary_sectors * 2048); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_pce_cd_bin(uint32_t binary_sectors, size_t* image_size) +{ + const uint8_t volume_header[] = { + 0x00, 0x00, 0x02, /* first sector of boot code */ + 0x14, /* number of sectors for boot code */ + 0x00, 0x40, /* program load address */ + 0x00, 0x40, /* program execute address */ + 0, 1, 2, 3, 4, /* IPLMPR */ + 0, /* open mode */ + 0, 0, 0, 0, 0, 0, /* GRPBLK and addr */ + 0, 0, 0, 0, 0, /* ADPBLK and rate */ + 0, 0, 0, 0, 0, 0, 0, /* reserved */ + 'P', 'C', ' ', 'E', 'n', 'g', 'i', 'n', 'e', ' ', 'C', 'D', '-', 'R', 'O', 'M', + ' ', 'S', 'Y', 'S', 'T', 'E', 'M', '\0', 'C', 'o', 'p', 'y', 'r', 'i', 'g', 'h', + 't', ' ', 'H', 'U', 'D', 'S', 'O', 'N', ' ', 'S', 'O', 'F', 'T', ' ', '/', ' ', + 'N', 'E', 'C', ' ', 'H', 'o', 'm', 'e', ' ', 'E', 'l', 'e', 'c', 't', 'r', 'o', + 'n', 'i', 'c', 's', ',', 'L', 't', 'd', '.', '\0', 'G', 'A', 'M', 'E', 'N', 'A', + 'M', 'E', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' + }; + + size_t size_needed = (binary_sectors + 2) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + if (!image) + return NULL; + + /* volume header goes in second sector */ + memcpy(&image[2048], volume_header, sizeof(volume_header)); + image[2048 + 0x03] = (uint8_t)binary_sectors; + + /* binary data */ + fill_image(&image[4096], binary_sectors * 2048); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_pcfx_bin(uint32_t binary_sectors, size_t* image_size) +{ + const uint8_t volume_header[] = { + 'G', 'A', 'M', 'E', 'N', 'A', 'M', 'E', 0, 0, 0, 0, 0, 0, 0, 0, /* title (32-bytes) */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x02, 0x00, 0x00, 0x00, /* first sector of boot code */ + 0x14, 0x00, 0x00, 0x00, /* number of sectors for boot code */ + 0x00, 0x80, 0x00, 0x00, /* program load address */ + 0x00, 0x80, 0x00, 0x00, /* program execute address */ + 'N', '/', 'A', '\0', /* maker id */ + 'r', 'c', 'h', 'e', 'e', 'v', 'o', 's', 't', 'e', 's', 't', 0, 0, 0, 0, /* maker name (60-bytes) */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x00, 0x00, 0x00, 0x00, /* volume number */ + 0x00, 0x01, /* version */ + 0x01, 0x00, /* country */ + '2', '0', '2', '0', 'X', 'X', 'X', 'X', /* date */ + }; + + size_t size_needed = (binary_sectors + 2) * 2048; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + if (!image) + return NULL; + + /* volume header goes in second sector */ + strcpy_s((char*)&image[0], size_needed, "PC-FX:Hu_CD-ROM"); + memcpy(&image[2048], volume_header, sizeof(volume_header)); + image[2048 + 0x24] = (uint8_t)binary_sectors; + + /* binary data */ + fill_image(&image[4096], binary_sectors * 2048); + + *image_size = size_needed; + return image; +} + +uint8_t* generate_iso9660_bin(uint32_t num_sectors, const char* volume_label, size_t* image_size) +{ + const uint8_t identifier[] = { 0x01, 'C', 'D', '0', '0', '1', 0x01, 0x00 }; + uint8_t* volume_descriptor;; + uint8_t* image; + + *image_size = num_sectors * 2048; + image = calloc(*image_size, 1); + if (!image) + return NULL; + + volume_descriptor = &image[16 * 2048]; + + /* CD001 identifier */ + memcpy(volume_descriptor, identifier, 8); + + /* volume label */ + memcpy(&volume_descriptor[40], volume_label, strlen(volume_label)); + + /* number of sectors (little endian, then big endian) */ + volume_descriptor[80] = image[87] = num_sectors & 0xFF; + volume_descriptor[81] = image[86] = (num_sectors >> 8) & 0xFF; + volume_descriptor[82] = image[85] = (num_sectors >> 16) & 0xFF; + volume_descriptor[83] = image[84] = (num_sectors >> 24) & 0xFF; + + /* size of each sector */ + volume_descriptor[128] = (2048) & 0xFF; + volume_descriptor[129] = (2048 >> 8) & 0xFF; + + /* root directory record location */ + volume_descriptor[158] = 17; + + /* helper for tracking next free sector - not actually part of iso9660 spec */ + image[17 * 2048 - 4] = 18; + + return image; +} + +uint8_t* generate_iso9660_file(uint8_t* image, const char* filename, const uint8_t* contents, size_t contents_size) +{ + const uint32_t root_directory_record_offset = 17 * 2048; + uint8_t* file_entry_start = &image[root_directory_record_offset]; + uint8_t* file_contents_start; + size_t filename_len; + uint32_t next_free_sector = image[root_directory_record_offset - 4] | + (image[root_directory_record_offset - 3] << 8) | (image[root_directory_record_offset - 2] << 16); + const char* separator; + + /* we start at the root. ignore explicit root path */ + if (*filename == '\\') + ++filename; + + /* handle subdirectories */ + do + { + separator = filename; + while (*separator && *separator != '\\') + ++separator; + + if (!*separator) + break; + + filename_len = separator - filename; + int found = 0; + while (*file_entry_start) + { + if (file_entry_start[25] && /* is directory */ + file_entry_start[33 + filename_len] == '\0' && memcmp(&file_entry_start[33], filename, filename_len) == 0) + { + const uint32_t directory_sector = file_entry_start[2]; + file_entry_start = &image[directory_sector * 2048]; + found = 1; + break; + } + + file_entry_start += *file_entry_start; + } + + if (!found) + { + /* entry size*/ + file_entry_start[0] = (filename_len & 0xFF) + 48; + + /* directory sector */ + file_entry_start[2] = next_free_sector & 0xFF; + file_entry_start[3] = (next_free_sector >> 8) & 0xFF; + + /* is directory */ + file_entry_start[25] = 1; + + /* directory name */ + file_entry_start[32] = filename_len & 0xFF; + memcpy(&file_entry_start[33], filename, filename_len); + file_entry_start[33 + filename_len] = '\0'; + + /* advance to next sector */ + file_entry_start = &image[next_free_sector * 2048]; + next_free_sector++; + } + + filename = separator + 1; + } while (1); + + /* skip over any items already in the directory */ + while (*file_entry_start) + file_entry_start += *file_entry_start; + + /* create the new entry */ + + /* entry size*/ + filename_len = separator - filename; + file_entry_start[0] = (filename_len & 0xFF) + 48; + + /* file sector */ + file_entry_start[2] = next_free_sector & 0xFF; + file_entry_start[3] = (next_free_sector >> 8) & 0xFF; + + /* file size */ + file_entry_start[10] = contents_size & 0xFF; + file_entry_start[11] = (contents_size >> 8) & 0xFF; + file_entry_start[12] = (contents_size >> 16) & 0xFF; + + /* file name */ + file_entry_start[32] = (filename_len + 2) & 0xFF; + memcpy(&file_entry_start[33], filename, filename_len); + file_entry_start[33 + filename_len] = ';'; + file_entry_start[34 + filename_len] = '1'; + + /* contents */ + file_contents_start = &image[next_free_sector * 2048]; + + if (contents) + memcpy(file_contents_start, contents, contents_size); + else + fill_image(file_contents_start, contents_size); + + /* update next free sector */ + next_free_sector += (unsigned)(contents_size + 2047) / 2048; + image[root_directory_record_offset - 4] = (next_free_sector & 0xFF); + image[root_directory_record_offset - 3] = (next_free_sector >> 8) & 0xFF; + image[root_directory_record_offset - 2] = (next_free_sector >> 16) & 0xFF; + + /* return pointer to contents so caller can modify if desired */ + return file_contents_start; +} + +uint8_t* generate_jaguarcd_bin(uint32_t header_offset, uint32_t binary_size, int byteswapped, size_t* image_size) +{ + size_t size_needed = (((binary_size + 64 + 32 + 8) + 2351) / 2352) * 2352; + uint8_t* image = (uint8_t*)calloc(size_needed, 1); + size_t i; + + if (!image) + return NULL; + + /* header is 64 bytes of ATRI repeating followed by approved data message, load address, and binary size */ + for (i = 0; i < 64; i += 4) + memcpy(&image[header_offset + i], "ATRI", 4); + memcpy(&image[header_offset + 64], "ATARI APPROVED DATA HEADER ATRI ", 32); + image[header_offset + 64 + 32 + 2] = 0xA0; + image[header_offset + 64 + 32 + 4 + 1] = (binary_size >> 16); + image[header_offset + 64 + 32 + 4 + 2] = (binary_size >> 8) & 0xFF; + image[header_offset + 64 + 32 + 4 + 3] = (binary_size & 0xFF); + + /* binary data */ + fill_image(&image[header_offset + 64 + 32 + 8], size_needed - (header_offset + 64 + 32 + 8)); + + if (byteswapped) + { + for (i = 0; i < size_needed; i += 2) + { + uint8_t tmp = image[i]; + image[i] = image[i + 1]; + image[i + 1] = tmp; + } + } + + *image_size = size_needed; + return image; +} + +uint8_t* generate_psx_bin(const char* binary_name, uint32_t binary_size, size_t* image_size) +{ + const uint32_t sectors_needed = (((binary_size + 2047) / 2048) + 20); + char system_cnf[256]; + uint8_t* image; + uint8_t* exe; + + snprintf(system_cnf, sizeof(system_cnf), "BOOT=cdrom:\\%s;1\nTCB=4\nEVENT=10\nSTACK=801FFFF0\n", binary_name); + + image = generate_iso9660_bin(sectors_needed, "TEST", image_size); + generate_iso9660_file(image, "SYSTEM.CNF", (const uint8_t*)system_cnf, strlen(system_cnf)); + + /* binary data */ + exe = generate_iso9660_file(image, binary_name, NULL, binary_size); + memcpy(exe, "PS-X EXE", 8); + + binary_size -= 2048; + exe[28] = binary_size & 0xFF; + exe[29] = (binary_size >> 8) & 0xFF; + exe[30] = (binary_size >> 16) & 0xFF; + exe[31] = (binary_size >> 24) & 0xFF; + + return image; +} + +uint8_t* generate_ps2_bin(const char* binary_name, uint32_t binary_size, size_t* image_size) +{ + const uint32_t sectors_needed = (((binary_size + 2047) / 2048) + 20); + char system_cnf[256]; + uint8_t* image; + uint8_t* exe; + + snprintf(system_cnf, sizeof(system_cnf), "BOOT2 = cdrom0:\\%s;1\nVER = 1.0\nVMODE = NTSC\n", binary_name); + + image = generate_iso9660_bin(sectors_needed, "TEST", image_size); + generate_iso9660_file(image, "SYSTEM.CNF", (const uint8_t*)system_cnf, strlen(system_cnf)); + + /* binary data */ + exe = generate_iso9660_file(image, binary_name, NULL, binary_size); + memcpy(exe, "\x7f\x45\x4c\x46", 4); + + return image; +} + +uint8_t* generate_generic_file(size_t size) +{ + uint8_t* image; + image = (uint8_t*)calloc(size, 1); + if (image != NULL) + fill_image(image, size); + + return image; +} + +uint8_t* convert_to_2352(uint8_t* input, size_t* size, uint32_t first_sector) +{ + const uint32_t num_sectors = (uint32_t)((*size + 2047) / 2048); + const uint32_t output_size = num_sectors * 2352; + const uint8_t sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + }; + uint8_t* output = (uint8_t*)malloc(output_size); + uint8_t* input_ptr = input; + uint8_t* ptr = output; + uint8_t minutes, seconds, frames; + uint32_t i; + + first_sector += 150; + frames = (first_sector % 75); + first_sector /= 75; + seconds = (first_sector % 60); + minutes = first_sector / 60; + + for (i = 0; i < num_sectors; i++) + { + /* 16 - byte sync header */ + memcpy(ptr, sync_pattern, 12); + ptr += 12; + *ptr++ = ((minutes / 10) << 4) | (minutes % 10); + *ptr++ = ((seconds / 10) << 4) | (seconds % 10); + *ptr++ = ((frames / 10) << 4) | (frames % 10); + if (++frames == 75) + { + frames = 0; + if (++seconds == 60) + { + seconds = 0; + ++minutes; + } + } + *ptr++ = 2; + + /* 2048 bytes data */ + memcpy(ptr, input_ptr, 2048); + input_ptr += 2048; + + /* 288 bytes parity / checksums */ + ptr += 2352 - 16; + } + + free(input); + *size = output_size; + return output; +} + diff --git a/src/rcheevos/test/rhash/data.h b/src/rcheevos/test/rhash/data.h new file mode 100644 index 0000000000..509d6cea68 --- /dev/null +++ b/src/rcheevos/test/rhash/data.h @@ -0,0 +1,32 @@ +#ifndef RHASH_TEST_DATA_H +#define RHASH_TEST_DATA_H + +#include "rc_export.h" + +#include +#include + +RC_BEGIN_C_DECLS + +uint8_t* generate_generic_file(size_t size); + +uint8_t* convert_to_2352(uint8_t* input, size_t* input_size, uint32_t first_sector); + +uint8_t* generate_3do_bin(uint32_t root_directory_sectors, uint32_t binary_size, size_t* image_size); +uint8_t* generate_dreamcast_bin(uint32_t track_first_sector, uint32_t binary_size, size_t* image_size); +uint8_t* generate_jaguarcd_bin(uint32_t header_offset, uint32_t binary_size, int byteswapped, size_t* image_size); +uint8_t* generate_pce_cd_bin(uint32_t binary_sectors, size_t* image_size); +uint8_t* generate_pcfx_bin(uint32_t binary_sectors, size_t* image_size); +uint8_t* generate_psx_bin(const char* binary_name, uint32_t binary_size, size_t* image_size); +uint8_t* generate_ps2_bin(const char* binary_name, uint32_t binary_size, size_t* image_size); + +uint8_t* generate_gamecube_iso(size_t mb, size_t* image_size); + +uint8_t* generate_iso9660_bin(uint32_t binary_sectors, const char* volume_label, size_t* image_size); +uint8_t* generate_iso9660_file(uint8_t* image, const char* filename, const uint8_t* contents, size_t contents_size); + +void fill_image(uint8_t* image, size_t size); + +RC_END_C_DECLS + +#endif /* RHASH_TEST_DATA_H */ diff --git a/src/rcheevos/test/rhash/mock_filereader.c b/src/rcheevos/test/rhash/mock_filereader.c new file mode 100644 index 0000000000..2c48654368 --- /dev/null +++ b/src/rcheevos/test/rhash/mock_filereader.c @@ -0,0 +1,236 @@ +#include "rc_hash.h" + +#include "../rc_compat.h" + +#include +#include + +typedef struct mock_file_data +{ + const char* path; + const uint8_t* data; + int64_t size; + int64_t pos; + int first_sector; +} mock_file_data; + +static mock_file_data mock_file_instance[16]; +static int mock_cd_tracks; + +static void* _mock_file_open(const char* path) +{ + size_t i; + for (i = 0; i < sizeof(mock_file_instance) / sizeof(mock_file_instance[0]); ++i) + { + if (strcmp(path, mock_file_instance[i].path) == 0) + { + mock_file_instance[i].pos = 0; + return &mock_file_instance[i]; + } + } + + return NULL; +} + +static void _mock_file_seek(void* file_handle, int64_t offset, int origin) +{ + mock_file_data* file = (mock_file_data*)file_handle; + switch (origin) + { + case SEEK_SET: + file->pos = offset; + break; + case SEEK_CUR: + file->pos += offset; + break; + case SEEK_END: + file->pos = file->size - offset; + break; + } + + if (file->pos > file->size) + file->pos = file->size; +} + +static int64_t _mock_file_tell(void* file_handle) +{ + mock_file_data* file = (mock_file_data*)file_handle; + return file->pos; +} + +static size_t _mock_file_read(void* file_handle, void* buffer, size_t count) +{ + mock_file_data* file = (mock_file_data*)file_handle; + const size_t remaining = (size_t)(file->size - file->pos); + if (count > remaining) + count = remaining; + + if (count > 0) + { + if (file->data) + memcpy(buffer, &file->data[file->pos], count); + else + memset(buffer, 0, count); + + file->pos += count; + } + + return count; +} + +static void _mock_file_close(void* file_handle) +{ + (void)file_handle; +} + +static void reset_mock_files() +{ + size_t i; + + memset(&mock_file_instance, 0, sizeof(mock_file_instance)); + for (i = 0; i < sizeof(mock_file_instance) / sizeof(mock_file_instance[0]); ++i) + mock_file_instance[i].path = ""; + + mock_cd_tracks = 0; +} + +void get_mock_filereader(struct rc_hash_filereader* reader) +{ + reader->open = _mock_file_open; + reader->seek = _mock_file_seek; + reader->tell = _mock_file_tell; + reader->read = _mock_file_read; + reader->close = _mock_file_close; + + reset_mock_files(); +} + +void init_mock_filereader() +{ + struct rc_hash_filereader reader; + get_mock_filereader(&reader); + + rc_hash_init_custom_filereader(&reader); +} + +void mock_file(int index, const char* filename, const uint8_t* buffer, size_t buffer_size) +{ + if (index == 0) + reset_mock_files(); + + mock_file_instance[index].path = filename; + mock_file_instance[index].data = buffer; + mock_file_instance[index].size = buffer_size; + mock_file_instance[index].pos = 0; + mock_file_instance[index].first_sector = 0; +} + +void mock_file_text(int index, const char* filename, const char* contents) +{ + mock_file(index, filename, (const uint8_t*)contents, strlen(contents)); +} + +void mock_file_first_sector(int index, int first_sector) +{ + mock_file_instance[index].first_sector = first_sector; +} + +void mock_file_size(int index, size_t mock_size) +{ + mock_file_instance[index].size = mock_size; +} + +void mock_empty_file(int index, const char* filename, size_t mock_size) +{ + mock_file(index, filename, NULL, mock_size); +} + +#ifndef RC_HASH_NO_DISC + +static void* _mock_cd_open_track(const char* path, uint32_t track) +{ + if (track == RC_HASH_CDTRACK_LAST) + track = mock_cd_tracks; + + if (track == 1 || track == RC_HASH_CDTRACK_FIRST_DATA || track == RC_HASH_CDTRACK_LARGEST) + { + if (strstr(path, ".cue")) + { + mock_file_data* file = (mock_file_data*)_mock_file_open(path); + if (!file) + return file; + + return _mock_file_open((const char*)file->data); + } + + return _mock_file_open(path); + } + else if (strstr(path, ".cue")) + { + mock_file_data* file = (mock_file_data*)_mock_file_open(path); + if (file) + { + char buffer[256]; + + const size_t file_len = strlen((const char*)file->data); + memcpy(buffer, file->data, file_len - 4); + snprintf(&buffer[file_len - 4], sizeof(buffer) - (file_len - 4), "%d%s", track, &file->data[file_len - 4]); + + return _mock_file_open(buffer); + } + } + else if (strstr(path, ".gdi")) + { + mock_file_data* file = (mock_file_data*)_mock_file_open(path); + if (file) + { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "track%02d.bin", track); + return _mock_file_open(buffer); + } + } + + return NULL; +} + +static size_t _mock_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + mock_file_data* file = (mock_file_data*)track_handle; + sector -= file->first_sector; + + _mock_file_seek(track_handle, sector * 2048, SEEK_SET); + return _mock_file_read(track_handle, buffer, requested_bytes); +} + +static uint32_t _mock_cd_first_track_sector(void* track_handle) +{ + mock_file_data* file = (mock_file_data*)track_handle; + return file->first_sector; +} + +void mock_cd_num_tracks(int num_tracks) +{ + mock_cd_tracks = num_tracks; +} + +void init_mock_cdreader() +{ + struct rc_hash_cdreader cdreader; + memset(&cdreader, 0, sizeof(cdreader)); + cdreader.open_track = _mock_cd_open_track; + cdreader.close_track = _mock_file_close; + cdreader.read_sector = _mock_cd_read_sector; + cdreader.first_track_sector = _mock_cd_first_track_sector; + + rc_hash_init_custom_cdreader(&cdreader); + + mock_cd_tracks = 0; +} + +#endif + +const char* get_mock_filename(void* file_handle) +{ + mock_file_data* file = (mock_file_data*)file_handle; + return file->path; +} diff --git a/src/rcheevos/test/rhash/mock_filereader.h b/src/rcheevos/test/rhash/mock_filereader.h new file mode 100644 index 0000000000..552c71c645 --- /dev/null +++ b/src/rcheevos/test/rhash/mock_filereader.h @@ -0,0 +1,31 @@ +#ifndef RHASH_MOCK_FILEREADER_H +#define RHASH_MOCK_FILEREADER_H + +#include "rc_export.h" + +#include +#include + +RC_BEGIN_C_DECLS + +void init_mock_filereader(); +void get_mock_filereader(struct rc_hash_filereader* reader); + +#ifndef RC_HASH_NO_DISC +void init_mock_cdreader(); +void mock_cd_num_tracks(int num_tracks); +#endif + +void rc_hash_reset_filereader(); + +void mock_file(int index, const char* filename, const uint8_t* buffer, size_t buffer_size); +void mock_file_text(int index, const char* filename, const char* contents); +void mock_empty_file(int index, const char* filename, size_t mock_size); +void mock_file_size(int index, size_t mock_size); +void mock_file_first_sector(int index, int first_sector); + +const char* get_mock_filename(void* file_handle); + +RC_END_C_DECLS + +#endif /* RHASH_MOCK_FILEREADER_H */ diff --git a/src/rcheevos/test/rhash/test_cdreader.c b/src/rcheevos/test/rhash/test_cdreader.c new file mode 100644 index 0000000000..e3c475ae09 --- /dev/null +++ b/src/rcheevos/test/rhash/test_cdreader.c @@ -0,0 +1,920 @@ +#include "rc_hash.h" + +#include "../rc_compat.h" +#include "../src/rhash/rc_hash_internal.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +static const uint8_t sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 +}; + +static char cue_single_track[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE2/2352\n" + " INDEX 01 00:00:00\n"; + +static char cue_single_bin_multiple_data[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " PREGAP 00:03:00\n" + " INDEX 01 00:55:45\n" + " TRACK 03 MODE1/2352\n" + " INDEX 01 11:30:74\n" + " TRACK 04 MODE1/2352\n" + " INDEX 01 13:31:51\n" + " TRACK 05 MODE1/2352\n" + " INDEX 01 13:48:56\n" + " TRACK 06 MODE1/2352\n" + " INDEX 01 34:48:19\n" + " TRACK 07 MODE1/2352\n" + " INDEX 01 50:42:74\n" + " TRACK 08 MODE1/2352\n" + " INDEX 01 55:20:74\n" + " TRACK 09 MODE1/2352\n" + " INDEX 01 56:25:67\n" + " TRACK 10 MODE1/2352\n" + " INDEX 01 59:04:08\n" + " TRACK 11 MODE1/2352\n" + " INDEX 01 61:17:18\n" + " TRACK 12 MODE1/2352\n" + " INDEX 01 62:44:33\n" + " TRACK 13 AUDIO\n" + " PREGAP 00:02:00\n" + " INDEX 01 66:24:37\n"; + +static char cue_multiple_bin_multiple_data[] = + "FILE \"track1.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track2.bin\" BINARY\n" + " TRACK 02 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:03:00\n" + "FILE \"track3.bin\" BINARY\n" + " TRACK 03 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track4.bin\" BINARY\n" + " TRACK 04 AUDIO\n" + " INDEX 00 00:00:00\n"; + +static char gdi_three_tracks[] = + "3\n" + "1 0 4 2352 track01.bin 0\n" + "2 600 0 2352 track02.raw 0\n" + "3 45000 4 2352 track03.bin 0"; + +static char gdi_many_tracks[] = + "26\n" + "1 0 4 2352 track01.bin 0\n" + "2 450 0 2352 track02.raw 0\n" + "3 45000 4 2352 track03.bin 0\n" + "4 370673 0 2352 track04.raw 0\n" + "5 371347 0 2352 track05.raw 0\n" + "6 372014 0 2352 track06.raw 0\n" + "7 372915 0 2352 track07.raw 0\n" + "8 373626 0 2352 track08.raw 0\n" + "9 379011 0 2352 track09.raw 0\n" + "10 384738 0 2352 track10.raw 0\n" + "11 390481 0 2352 track11.raw 0\n" + "12 395473 0 2352 track12.raw 0\n" + "13 398926 0 2352 track13.raw 0\n" + "14 404448 0 2352 track14.raw 0\n" + "15 425246 0 2352 track15.raw 0\n" + "16 445520 0 2352 track16.raw 0\n" + "17 466032 0 2352 track17.raw 0\n" + "18 474231 0 2352 track18.raw 0\n" + "19 485598 0 2352 track19.raw 0\n" + "20 486386 0 2352 track20.raw 0\n" + "21 487098 0 2352 track21.raw 0\n" + "22 487822 0 2352 track22.raw 0\n" + "23 498356 0 2352 track23.raw 0\n" + "24 508297 0 2352 track24.raw 0\n" + "25 527383 0 2352 track25.raw 0\n" + "26 548106 4 2352 track26.bin 0\n"; + +static void initialize_iterator(rc_hash_iterator_t* iterator) +{ + memset(iterator, 0, sizeof(*iterator)); + rc_hash_get_default_cdreader(&iterator->callbacks.cdreader); + get_mock_filereader(&iterator->callbacks.filereader); +} + +static rc_hash_cdrom_track_t* open_track(rc_hash_iterator_t* iterator, const char* path, uint32_t index) +{ + return (rc_hash_cdrom_track_t*)iterator->callbacks.cdreader.open_track_iterator(path, index, iterator); +} + +static void close_track(rc_hash_iterator_t* iterator, rc_hash_cdrom_track_t* track_handle) +{ + iterator->callbacks.cdreader.close_track(track_handle); +} + +static void test_open_cue_track_2() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", 2); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 9807840); /* track 2: 0x95A7E0 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_12() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", 12); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 664047216); /* track 12: 0x27948E70 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_14() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + /* only 13 tracks */ + track_handle = open_track(&iterator, "game.cue", 14); + ASSERT_PTR_NULL(track_handle); +} + +static void test_open_cue_track_missing_bin() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + + track_handle = open_track(&iterator, "game.cue", 2); + ASSERT_PTR_NULL(track_handle); +} + +static void test_open_gdi_track_3() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.gdi", gdi_three_tracks); + mock_empty_file(1, "track03.bin", 1185760800); + + track_handle = open_track(&iterator, "game.gdi", 3); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track03.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_gdi_track_3_quoted() +{ + const char gdi_contents[] = + "3\n" + "1 0 4 2352 \"track 01.bin\" 0\n" + "2 600 0 2352 \"track 02.raw\" 0\n" + "3 45000 4 2352 \"track 03.bin\" 0"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.gdi", gdi_contents); + mock_empty_file(1, "track 03.bin", 1185760800); + + track_handle = open_track(&iterator, "game.gdi", 3); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track 03.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_gdi_track_3_extra_whitespace() +{ + const char gdi_contents[] = + "3\n\n" + " 1 0 4 2352 \"track 01.bin\" 0\n\n" + " 2 600 0 2352 \"track 02.raw\" 0\n\n" + " 3 45000 4 2352 \"track 03.bin\" 0\n\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.gdi", gdi_contents); + mock_empty_file(1, "track 03.bin", 1185760800); + + track_handle = open_track(&iterator, "game.gdi", 3); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track 03.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_open_gdi_track_last() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.gdi", gdi_many_tracks); + mock_empty_file(1, "track26.bin", 2457600); + + track_handle = open_track(&iterator, "game.gdi", RC_HASH_CDTRACK_LAST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track26.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 548106); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_multiple_bin() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_multiple_bin_multiple_data); + mock_empty_file(1, "track2.bin", 406423248); + mock_empty_file(2, "track3.bin", 11553024); + + track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track2.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 225); /* INDEX 01 00:03:00 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_backwards_compatibility() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + /* before defining the enum, 0 meant largest */ + track_handle = open_track(&iterator, "game.cue", 0); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_last_track() +{ + const char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " PREGAP 00:03:00\n" + " INDEX 01 00:55:45\n" + " TRACK 03 MODE1/2352\n" + " INDEX 01 11:30:74\n" + " TRACK 04 MODE1/2352\n" + " INDEX 01 13:31:51\n" + " TRACK 05 MODE1/2352\n" + " INDEX 01 13:48:56\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 (13:48:56) */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_index0s() +{ + const char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " INDEX 00 00:44:65\n" + " INDEX 01 00:47:65\n" + " TRACK 03 AUDIO\n" + " INDEX 00 01:19:52\n" + " INDEX 01 01:21:52\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 7914480); /* track 2: 0x78C3F0 (00:44:65) */ + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 225); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_index2() +{ + const char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + " TRACK 02 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + " INDEX 02 00:08:64\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); /* 00:02:00 = 150 frames in */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_multiple_bins() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_multiple_bin_multiple_data); + mock_empty_file(1, "track1.bin", 4132464); + mock_empty_file(2, "track2.bin", 30080102); + mock_empty_file(3, "track3.bin", 40343152); + mock_empty_file(4, "track4.bin", 47277552); + + track_handle = open_track(&iterator, "game.cue", 0); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track3.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); /* 00:02:00 = 150 frames in */ + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_open_cue_track_largest_data_only_audio() +{ + const char cue[] = + "FILE \"track1.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track2.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:03:00\n" + "FILE \"track3.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track4.bin\" BINARY\n" + " TRACK 04 AUDIO\n" + " INDEX 00 00:00:00\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_empty_file(1, "track1.bin", 4132464); + mock_empty_file(2, "track2.bin", 30080102); + mock_empty_file(3, "track3.bin", 40343152); + mock_empty_file(4, "track4.bin", 47277552); + + track_handle = open_track(&iterator, "game.cue", 0); + ASSERT_PTR_NULL(track_handle); +} + +static void test_open_cue_track_first_data() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_bin_multiple_data); + mock_empty_file(1, "game.bin", 718310208); + + track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_FIRST_DATA); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 9807840); /* track 2: 0x0095a7e0 (00:55:45) */ + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + close_track(&iterator, track_handle); +} + +static void test_determine_sector_size_sync(int sector_size) +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + + const size_t image_size = (size_t)sector_size * 32; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[sector_size * 16], sync_pattern, sizeof(sync_pattern)); + + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor(int sector_size) +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + + const size_t image_size = (size_t)sector_size * 32; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[sector_size * 16], sync_pattern, sizeof(sync_pattern)); + memcpy(&image[sector_size * 16 + 25], "CD001", 5); + + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor_index0(int sector_size) +{ + char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE2/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + + const size_t image_size = (size_t)sector_size * 200; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_file(1, "game.bin", image, image_size); + + char sector_size_str[16]; + snprintf(sector_size_str, sizeof(sector_size_str), "%d", sector_size); + memcpy(&cue[40], sector_size_str, 4); + + memset(image, 0, image_size); + memcpy(&image[sector_size * (150 + 16)], sync_pattern, sizeof(sync_pattern)); + memcpy(&image[sector_size * (150 + 16) + 25], "CD001", 5); + + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_determine_sector_size_sync_2048() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + const int sector_size = 2048; + const size_t image_size = (size_t)sector_size * 32; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + + /* 2048 byte sectors don't have a sync pattern - will use mode specified in header */ + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor_2048() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + const int sector_size = 2048; + const size_t image_size = (size_t)sector_size * 32; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[sector_size * 16 + 1], "CD001", 5); + + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 0); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_determine_sector_size_sync_primary_volume_descriptor_index0_2048() +{ + char cue[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE1/2048\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + + const int sector_size = 2048; + const size_t image_size = (size_t)sector_size * 200; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_file(1, "game.bin", image, image_size); + + char sector_size_str[16]; + snprintf(sector_size_str, sizeof(sector_size_str), "%d", sector_size); + memcpy(&cue[40], sector_size_str, 4); + + memset(image, 0, image_size); + memcpy(&image[sector_size * (150 + 16) + 1], "CD001", 5); + + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); + ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 0); + ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_absolute_sector_to_track_sector_cue_pregap() +{ + const char cue[] = + "FILE \"game1.bin\" BINARY\n"/* file contains 500 sectors of data [1176000 bytes] */ + " TRACK 01 MODE2/2352\n" + " INDEX 00 00:00:00\n" /* 150 pre-gap sectors */ + " INDEX 01 00:02:00\n" /* 350 sectors of data */ + "FILE \"game2.bin\" BINARY\n" + " TRACK 02 MODE2/2352\n" + " INDEX 00 00:00:00\n" /* 150 pre-gap sectors */ + " INDEX 01 00:02:00\n"; + + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + + const size_t image_size = (size_t)60 * 200; + uint8_t* image = (uint8_t*)malloc(image_size); + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue); + mock_file(1, "game1.bin", NULL, (size_t)500 * 2352); + mock_file(2, "game2.bin", image, image_size); + + track_handle = open_track(&iterator, "game.cue", 2); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game2.bin"); + + /* pregap of second track starts at sector 500 */ + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 500); + ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); + + /* data for second track starts at sector 650 */ + ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.first_track_sector(track_handle), 650); + + close_track(&iterator, track_handle); + free(image); +} + +static void test_absolute_sector_to_track_sector_gdi() +{ + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + initialize_iterator(&iterator); + + mock_file_text(0, "game.gdi", gdi_many_tracks); + mock_file(1, "track26.bin", NULL, 1234567); + + track_handle = open_track(&iterator, "game.gdi", 26); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track26.bin"); + ASSERT_NUM_EQUALS(track_handle->track_first_sector, 548106); + + ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.first_track_sector(track_handle), 548106); + + close_track(&iterator, track_handle); +} + +static void test_read_sector() +{ + char buffer[4096]; + rc_hash_cdrom_track_t* track_handle; + rc_hash_iterator_t iterator; + const size_t image_size = (size_t)2352 * 32; + uint8_t* image = (uint8_t*)malloc(image_size); + int offset, i; + ASSERT_PTR_NOT_NULL(image); + + initialize_iterator(&iterator); + + mock_file_text(0, "game.cue", cue_single_track); + mock_file(1, "game.bin", image, image_size); + + memset(image, 0, image_size); + memcpy(&image[2352 * 16], sync_pattern, sizeof(sync_pattern)); + image[2352 * 16 + 12] = 0; + image[2352 * 16 + 13] = 2; + image[2352 * 16 + 14] = 0x16; + image[2352 * 16 + 15] = 2; + + offset = 2352 * 1 + 16; + for (i = 0; i < 26; i++) + { + memset(&image[offset], i + 'A', 256); + offset += 256; + + if ((i % 8) == 7) + offset += (2352 - 2048); + } + + track_handle = open_track(&iterator, "game.cue", 1); + ASSERT_PTR_NOT_NULL(track_handle); + + ASSERT_PTR_NOT_NULL(track_handle->file_handle); + ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); + ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); + ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); + ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); + + /* read across multiple sectors */ + ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.read_sector(track_handle, 1, buffer, sizeof(buffer)), 4096); + + ASSERT_NUM_EQUALS(buffer[0], 'A'); + ASSERT_NUM_EQUALS(buffer[255], 'A'); + ASSERT_NUM_EQUALS(buffer[256], 'B'); + ASSERT_NUM_EQUALS(buffer[2047], 'H'); + ASSERT_NUM_EQUALS(buffer[2048], 'I'); + ASSERT_NUM_EQUALS(buffer[4095], 'P'); + + /* read of partial sector */ + ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.read_sector(track_handle, 2, buffer, 10), 10); + ASSERT_NUM_EQUALS(buffer[0], 'I'); + ASSERT_NUM_EQUALS(buffer[9], 'I'); + ASSERT_NUM_EQUALS(buffer[10], 'A'); + + close_track(&iterator, track_handle); + free(image); +} + +/* ========================================================================= */ + +void test_cdreader(void) { + TEST_SUITE_BEGIN(); + + init_mock_filereader(); + rc_hash_init_default_cdreader(); + + TEST(test_open_cue_track_2); + TEST(test_open_cue_track_12); + TEST(test_open_cue_track_14); + TEST(test_open_cue_track_missing_bin); + + TEST(test_open_gdi_track_3); + TEST(test_open_gdi_track_3_quoted); + TEST(test_open_gdi_track_3_extra_whitespace); + TEST(test_open_gdi_track_last); + + TEST(test_open_cue_track_largest_data); + TEST(test_open_cue_track_largest_data_multiple_bin); + TEST(test_open_cue_track_largest_data_backwards_compatibility); + TEST(test_open_cue_track_largest_data_last_track); + TEST(test_open_cue_track_largest_data_index0s); + TEST(test_open_cue_track_largest_data_index2); + TEST(test_open_cue_track_largest_data_multiple_bins); + TEST(test_open_cue_track_largest_data_only_audio); + + TEST(test_open_cue_track_first_data); + + TEST_PARAMS1(test_determine_sector_size_sync, 2352); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor, 2352); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor_index0, 2352); + + TEST_PARAMS1(test_determine_sector_size_sync, 2336); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor, 2336); + TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor_index0, 2336); + + TEST(test_determine_sector_size_sync_2048); + TEST(test_determine_sector_size_sync_primary_volume_descriptor_2048); + TEST(test_determine_sector_size_sync_primary_volume_descriptor_index0_2048); + + TEST(test_absolute_sector_to_track_sector_cue_pregap); + TEST(test_absolute_sector_to_track_sector_gdi); + + TEST(test_read_sector); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/test_hash.c b/src/rcheevos/test/rhash/test_hash.c new file mode 100644 index 0000000000..a16402630b --- /dev/null +++ b/src/rcheevos/test/rhash/test_hash.c @@ -0,0 +1,310 @@ +#include "rc_hash.h" + +#include "../rhash/rc_hash_internal.h" + +#include "../rc_compat.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +/* ========================================================================= */ + +void test_hash_full_file(uint32_t console_id, const char* filename, size_t size, const char* expected_md5) +{ + uint8_t* image = generate_generic_file(size); + char hash_buffer[33], hash_file[33], hash_iterator[33]; + mock_file(0, filename, image, size); + + /* test full buffer hash */ + int result_buffer = rc_hash_generate_from_buffer(hash_buffer, console_id, image, size); + + /* test full file hash */ + int result_file = rc_hash_generate_from_file(hash_file, console_id, filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_buffer, 1); + ASSERT_STR_EQUALS(hash_buffer, expected_md5); + + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +void test_hash_m3u(uint32_t console_id, const char* filename, size_t size, const char* expected_md5) +{ + uint8_t* image = generate_generic_file(size); + char hash_file[33], hash_iterator[33]; + const char* m3u_filename = "test.m3u"; + + mock_file(0, filename, image, size); + mock_file(1, m3u_filename, (uint8_t*)filename, strlen(filename)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, console_id, m3u_filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void assert_valid_m3u(const char* disc_filename, const char* m3u_filename, const char* m3u_contents) +{ + const size_t size = 131072; + uint8_t* image = generate_generic_file(size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "a0f425b23200568132ba76b2405e3933"; + + mock_file(0, disc_filename, image, size); + mock_file(1, m3u_filename, (uint8_t*)m3u_contents, strlen(m3u_contents)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC8800, m3u_filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_m3u_buffered() +{ + const size_t size = 131072; + uint8_t* image = generate_generic_file(size); + char hash_iterator[33]; + const char* m3u_filename = "test.m3u"; + const char* filename = "test.d88"; + const char* expected_md5 = "a0f425b23200568132ba76b2405e3933"; + uint8_t* m3u_contents = (uint8_t*)filename; + const size_t m3u_size = strlen(filename); + + mock_file(0, filename, image, size); + mock_file(1, m3u_filename, m3u_contents, m3u_size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, m3u_contents, m3u_size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_m3u_with_comments() +{ + assert_valid_m3u("test.d88", "test.m3u", + "#EXTM3U\r\n\r\n#EXTBYT:131072\r\ntest.d88\r\n"); +} + +static void test_hash_m3u_empty() +{ + char hash_file[33], hash_iterator[33]; + const char* m3u_filename = "test.m3u"; + const char* m3u_contents = "#EXTM3U\r\n\r\n#EXTBYT:131072\r\n"; + + mock_file(0, m3u_filename, (uint8_t*)m3u_contents, strlen(m3u_contents)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC8800, m3u_filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_m3u_trailing_whitespace() +{ + assert_valid_m3u("test.d88", "test.m3u", + "#EXTM3U \r\n \r\n#EXTBYT:131072 \r\ntest.d88 \t \r\n"); +} + +static void test_hash_m3u_line_ending() +{ + assert_valid_m3u("test.d88", "test.m3u", + "#EXTM3U\n\n#EXTBYT:131072\ntest.d88\n"); +} + +static void test_hash_m3u_extension_case() +{ + assert_valid_m3u("test.D88", "test.M3U", + "#EXTM3U\r\n\r\n#EXTBYT:131072\r\ntest.D88\r\n"); +} + +static void test_hash_m3u_relative_path() +{ + assert_valid_m3u("folder1/folder2/test.d88", "folder1/test.m3u", + "#EXTM3U\r\n\r\n#EXTBYT:131072\r\nfolder2/test.d88"); +} + +static void test_hash_m3u_absolute_path(const char* absolute_path) +{ + char m3u_contents[128]; + snprintf(m3u_contents, sizeof(m3u_contents), "#EXTM3U\r\n\r\n#EXTBYT:131072\r\n%s", absolute_path); + + assert_valid_m3u(absolute_path, "relative/test.m3u", m3u_contents); +} + +#ifndef RC_HASH_NO_ROM +uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size); + +static void test_hash_file_without_ext() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + char hash_file[33], hash_iterator[33]; + const char* filename = "test"; + + mock_file(0, filename, image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO, filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + + /* specifying a console will use the appropriate hasher */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, "6a2305a2b6675a97ff792709be1ca857"); + + /* no extension will use the default full file iterator, so hash should include header */ + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, "64b131c5c7fec32985d9c99700babb7e"); +} +#endif + +static void test_hash_handler_table_order() +{ + size_t num_handlers; + const rc_hash_iterator_ext_handler_entry_t* handlers = rc_hash_get_iterator_ext_handlers(&num_handlers); + int index; + + for (index = 1; index < num_handlers; ++index) { + if (strcmp(handlers[index].ext, handlers[index - 1].ext) <= 0) { + ASSERT_FAIL("handler[%s] after handler[%s]", handlers[index].ext, handlers[index - 1].ext); + } + } +} + +/* ========================================================================= */ + +void test_hash(void) { + TEST_SUITE_BEGIN(); + + init_mock_filereader(); + + /* Amstrad CPC */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); + + /* Apple II */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.nib", 232960, "96e8d33bdc385fd494327d6e6791cbe4"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); + + /* Commodore 64 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.nib", 327936, "e7767d32b23e3fa62c5a250a08caeba3"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); + + /* MSX */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); + + /* PC-8800 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); + + /* ZX Spectrum */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tap", 1596, "714a9f455e616813dd5421c5b347e5e5"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tzx", 14971, "93723e6d1100f9d1d448a27cf6618c47"); + + /* m3u support */ + TEST(test_hash_m3u_buffered); + TEST(test_hash_m3u_with_comments); + TEST(test_hash_m3u_empty); + TEST(test_hash_m3u_trailing_whitespace); + TEST(test_hash_m3u_line_ending); + TEST(test_hash_m3u_extension_case); + TEST(test_hash_m3u_relative_path); + TEST_PARAMS1(test_hash_m3u_absolute_path, "/absolute/test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "\\absolute\\test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "C:\\absolute\\test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "\\\\server\\absolute\\test.d88"); + TEST_PARAMS1(test_hash_m3u_absolute_path, "samba:/absolute/test.d88"); + + /* other */ +#ifndef RC_HASH_NO_ROM + TEST(test_hash_file_without_ext); +#endif + TEST(test_hash_handler_table_order); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/test_hash_disc.c b/src/rcheevos/test/rhash/test_hash_disc.c new file mode 100644 index 0000000000..b45b57fbc4 --- /dev/null +++ b/src/rcheevos/test/rhash/test_hash_disc.c @@ -0,0 +1,1450 @@ +#include "rc_hash.h" + +#include "../rhash/rc_hash_internal.h" + +#include "../rc_compat.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +/* in test_hash.c */ +void test_hash_full_file(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); +void test_hash_m3u(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); + +/* ========================================================================= */ + +static void test_hash_unknown_format(uint32_t console_id, const char* path) +{ + char hash_file[33] = "", hash_iterator[33] = ""; + + /* test file hash (won't match) */ + int result_file = rc_hash_generate_from_file(hash_file, console_id, path); + + /* test file identification from iterator (won't match) */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, path, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_STR_EQUALS(hash_file, ""); + + ASSERT_NUM_EQUALS(result_iterator, 0); + ASSERT_STR_EQUALS(hash_iterator, ""); +} + +/* ========================================================================= */ + +static void test_hash_3do_bin() +{ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 123456, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "9b2266b8f5abed9c12cce780750e88d6"; + + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + mock_file_size(0, 45678901); /* must be > 32MB for iterator to consider CD formats for bin */ + rc_hash_initialize_iterator(&iterator, "game.bin", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_3do_cue() +{ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 9347, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "257d1d19365a864266b236214dbea29c"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_3do_iso() +{ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 9347, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "257d1d19365a864266b236214dbea29c"; + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_3do_invalid_header() +{ + /* this is meant to simulate attempting to open a non-3DO CD. TODO: generate PSX CD */ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 12, &image_size); + char hash_file[33]; + + /* make the header not match */ + image[3] = 0x34; + + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); +} + +static void test_hash_3do_launchme_case_insensitive() +{ + /* main executable for "Captain Quazar" is "launchme" */ + /* main executable for "Rise of the Robots" is "launchMe" */ + /* main executable for "Road Rash" is "LaunchMe" */ + /* main executable for "Sewer Shark" is "Launchme" */ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 6543, &image_size); + char hash_file[33]; + const char* expected_md5 = "59622882e3261237e8a1e396825ae4f5"; + + memcpy(&image[2048 + 0x14 + 0x48 + 0x20], "launchme", 8); + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); +} + +static void test_hash_3do_no_launchme() +{ + /* this case should not happen */ + size_t image_size; + uint8_t* image = generate_3do_bin(1, 6543, &image_size); + char hash_file[33]; + + memcpy(&image[2048 + 0x14 + 0x48 + 0x20], "filename", 8); + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); +} + +static void test_hash_3do_long_directory() +{ + /* root directory for "Dragon's Lair" uses more than one sector */ + size_t image_size; + uint8_t* image = generate_3do_bin(3, 6543, &image_size); + char hash_file[33]; + const char* expected_md5 = "8979e876ae502e0f79218f7ff7bd8c2a"; + + mock_file(0, "game.bin", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_atari_jaguar_cd() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 60024, 0, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c324d95dc5831c2d5c470eefb18c346b"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track02.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_atari_jaguar_cd_byteswapped() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 60024, 1, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c324d95dc5831c2d5c470eefb18c346b"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track02.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_atari_jaguar_cd_track3() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(1470, 99200, 1, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "060e9d223c584b581cf7d7ce17c0e5dc"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track03.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_atari_jaguar_cd_no_header() +{ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 32768, 1, &image_size); + char hash_file[33], hash_iterator[33]; + + image[2 + 64 + 12] = 'B'; /* corrupt the header */ + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track02.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_atari_jaguar_cd_no_sessions() +{ + const char* cue_file = + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size; + uint8_t* image = generate_jaguarcd_bin(2, 99200, 1, &image_size); + char hash_file[33], hash_iterator[33]; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track03.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +extern const char* _rc_hash_jaguar_cd_homebrew_hash; + +static void test_hash_atari_jaguar_cd_homebrew() +{ + /* Jaguar CD homebrew games all appear to have a common bootloader in the primary boot executable space. They only + * differ in a secondary executable in the second track (part of the first session). This doesn't appear to be + * intentional behavior based on the CD BIOS documentation, which states that all developer code should be in the + * first track of the second session. I speculate this is done to work around the authentication logic. */ + const char* cue_file = + "REM SESSION 01\n" + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 AUDIO\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 01 00:00:00\n" + "REM SESSION 02\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 AUDIO\n" + " INDEX 01 00:00:00\n"; + size_t image_size, image_size2; + uint8_t* image = generate_jaguarcd_bin(2, 45760, 1, &image_size); + uint8_t* image2 = generate_jaguarcd_bin(2, 986742, 1, &image_size2); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "3fdf70e362c845524c9e447aacaed0a9"; + + image2[0x60] = 0x21; /* ATARI APPROVED DATA HEADER ATRI! */ + memcpy(&image2[0xA2], &image2[0x62], 8); /* addr / size */ + memcpy(&image2[0x62], "RTKARTKARTKARTKA", 16); /* KARTKARTKARTKART */ + memcpy(&image2[0x72], "RTKARTKARTKARTKA", 16); + memcpy(&image2[0x82], "RTKARTKARTKARTKA", 16); + memcpy(&image2[0x92], "RTKARTKARTKARTKA", 16); + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(2, "track02.bin", image2, image_size2); + mock_file(1, "track03.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ + _rc_hash_jaguar_cd_homebrew_hash = "4e4114b2675eff21bb77dd41e141ddd6"; /* mock the hash of the homebrew bootloader */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + _rc_hash_jaguar_cd_homebrew_hash = NULL; + free(image); + free(image2); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_dreamcast_single_bin() +{ + size_t image_size; + uint8_t* image = generate_dreamcast_bin(45000, 1458208, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "2a550500caee9f06e5d061fe10a46f6e"; + + mock_file(0, "track03.bin", image, image_size); + mock_file_first_sector(0, 45000); + mock_file(1, "game.gdi", (uint8_t*)"game.bin", 8); + mock_cd_num_tracks(3); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.gdi"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.gdi", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_dreamcast_split_bin() +{ + size_t image_size; + uint8_t* image = generate_dreamcast_bin(548106, 1830912, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "771e56aff169230ede4505013a4bcf9f"; + + mock_file(0, "game.gdi", (uint8_t*)"game.bin", 8); + mock_file(1, "track03.bin", image, image_size); + mock_file_first_sector(1, 45000); + mock_file(2, "track26.bin", image, image_size); + mock_file_first_sector(2, 548106); + mock_cd_num_tracks(26); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.gdi"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.gdi", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_dreamcast_cue() +{ + const char* cue_file = + "FILE \"track01.bin\" BINARY\n" + " TRACK 01 MODE1/2352\n" + " INDEX 01 00:00:00\n" + "FILE \"track02.bin\" BINARY\n" + " TRACK 02 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track03.bin\" BINARY\n" + " TRACK 03 MODE1/2352\n" + " INDEX 01 00:00:00\n" + "FILE \"track04.bin\" BINARY\n" + " TRACK 04 AUDIO\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:02:00\n" + "FILE \"track05.bin\" BINARY\n" + " TRACK 05 MODE1/2352\n" + " INDEX 00 00:00:00\n" + " INDEX 01 00:03:00\n"; + size_t image_size; + uint8_t* image = convert_to_2352(generate_dreamcast_bin(45000, 1697028, &image_size), &image_size, 45000); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c952864c3364591d2a8793ce2cfbf3a0"; + + mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); + mock_file(1, "track01.bin", image, 1425312); /* 606 sectors */ + mock_file(2, "track02.bin", image, 1589952); /* 676 sectors */ + mock_file(3, "track03.bin", image, image_size); /* 737 sectors */ + mock_file(4, "track04.bin", image, 1237152); /* 526 sectors */ + mock_file(5, "track05.bin", image, image_size); + + rc_hash_init_default_cdreader(); /* want to test actual first_track_sector calculation */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + init_mock_cdreader(); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_gamecube() +{ + size_t image_size; + uint8_t* image = generate_gamecube_iso(32, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "c7803b704fa43d22d8f6e55f4789cb45"; + + mock_file(0, "test.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_GAMECUBE, "test.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "test.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); + + ASSERT_NUM_EQUALS(image_size, 32 * 1024 * 1024); +} + +/* ========================================================================= */ + +static void test_hash_neogeocd() +{ + const char* ipl_txt = "FIXA.FIX,0,0\r\nPROG.PRG,0,0\r\nSOUND.PCM,0,0\r\n\x1a"; + const size_t prog_prg_size = 273470; + uint8_t* prog_prg = generate_generic_file(prog_prg_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "96f35b20c6cf902286da45e81a50b2a3"; + + generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); + generate_iso9660_file(image, "PROG.PRG", prog_prg, prog_prg_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(prog_prg); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_neogeocd_multiple_prg() +{ + const char* ipl_txt = "FIXA.FIX,0,0\r\nPROG1.PRG,0,0\r\nSOUND.PCM,0,0\r\nPROG2.PRG,0,44000\r\n\x1a"; + const size_t prog1_prg_size = 273470; + uint8_t* prog1_prg = generate_generic_file(prog1_prg_size); + const size_t prog2_prg_size = 13768; + uint8_t* prog2_prg = generate_generic_file(prog2_prg_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "d62df483c4786d3c63f27b6c5f17eeca"; + + generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); + generate_iso9660_file(image, "PROG1.PRG", prog1_prg, prog1_prg_size); + generate_iso9660_file(image, "PROG2.PRG", prog2_prg, prog2_prg_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(prog1_prg); + free(prog2_prg); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_neogeocd_lowercase_ipl_contents() +{ + const char* ipl_txt = "fixa.fix,0,0\r\nprog.prg,0,0\r\nsound.pcm,0,0\r\n\x1a"; + const size_t prog_prg_size = 273470; + uint8_t* prog_prg = generate_generic_file(prog_prg_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "96f35b20c6cf902286da45e81a50b2a3"; + + generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); + generate_iso9660_file(image, "PROG.PRG", prog_prg, prog_prg_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(prog_prg); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_pce_cd() +{ + size_t image_size; + uint8_t* image = generate_pce_cd_bin(72, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "6565819195a49323e080e7539b54f251"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC_ENGINE_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_pce_cd_invalid_header() +{ + size_t image_size; + uint8_t* image = generate_pce_cd_bin(72, &image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* make the header not match */ + image[2048 + 0x24] = 0x34; + + test_hash_unknown_format(RC_CONSOLE_PC_ENGINE_CD, "game.cue"); + + free(image); +} + +/* ========================================================================= */ + +static void test_hash_pcfx() +{ + size_t image_size; + uint8_t* image = generate_pcfx_bin(72, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "0a03af66559b8529c50c4e7788379598"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PCFX, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_pcfx_invalid_header() +{ + size_t image_size; + uint8_t* image = generate_pcfx_bin(72, &image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* make the header not match */ + image[12] = 0x34; + + test_hash_unknown_format(RC_CONSOLE_PCFX, "game.cue"); + + free(image); +} + +static void test_hash_pcfx_pce_cd() +{ + /* Battle Heat is formatted as a PC-Engine CD */ + size_t image_size; + uint8_t* image = generate_pce_cd_bin(72, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "6565819195a49323e080e7539b54f251"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + mock_file(2, "game2.bin", image, image_size); /* PC-Engine CD check only applies to track 2 */ + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PCFX, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd() +{ + /* BOOT=cdrom:\SLUS_007.45 */ + size_t image_size; + uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd_no_system_cnf() +{ + size_t image_size; + uint8_t* image; + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "e494c79a7315be0dc3e8571c45df162c"; + uint32_t binary_size = 0x12000; + const uint32_t sectors_needed = (((binary_size + 2047) / 2048) + 20); + uint8_t* exe; + + image = generate_iso9660_bin(sectors_needed, "HOMEBREW", &image_size); + exe = generate_iso9660_file(image, "PSX.EXE", NULL, binary_size); + memcpy(exe, "PS-X EXE", 8); + binary_size -= 2048; + exe[28] = binary_size & 0xFF; + exe[29] = (binary_size >> 8) & 0xFF; + exe[30] = (binary_size >> 16) & 0xFF; + exe[31] = (binary_size >> 24) & 0xFF; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd_exe_in_subfolder() +{ + /* BOOT=cdrom:\bin\SLUS_012.37 */ + size_t image_size; + uint8_t* image = generate_psx_bin("bin\\SCES_012.37", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "674018e23a4052113665dfb264e9c2fc"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psx_cd_extra_slash() +{ + /* BOOT=cdrom:\\SLUS_007.45 */ + size_t image_size; + uint8_t* image = generate_psx_bin("\\SLUS_007.45", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_ps2_iso() +{ + size_t image_size; + uint8_t* image = generate_ps2_bin("SLUS_200.64", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "01a517e4ad72c6c2654d1b839be7579d"; + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION_2, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_ps2_psx() +{ + size_t image_size; + uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION_2, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); /* PSX hash */ + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation (should not generate PS2 hash for PSX file) */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_psp() +{ + const size_t param_sfo_size = 690; + uint8_t* param_sfo = generate_generic_file(param_sfo_size); + const size_t eboot_bin_size = 273470; + uint8_t* eboot_bin = generate_generic_file(eboot_bin_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "27ec2f9b7238b2ef29af31ddd254f201"; + + generate_iso9660_file(image, "PSP_GAME\\PARAM.SFO", param_sfo, param_sfo_size); + generate_iso9660_file(image, "PSP_GAME\\SYSDIR\\EBOOT.BIN", eboot_bin, eboot_bin_size); + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(eboot_bin); + free(param_sfo); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_psp_video() +{ + const size_t param_sfo_size = 690; + uint8_t* param_sfo = generate_generic_file(param_sfo_size); + const size_t eboot_bin_size = 273470; + uint8_t* eboot_bin = generate_generic_file(eboot_bin_size); + size_t image_size; + uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); + char hash_file[33], hash_iterator[33]; + + /* UMD video disc may have an UPDATE folder, but nothing in the PSP_GAME or SYSDIR folders. */ + generate_iso9660_file(image, "PSP_GAME\\SYSDIR\\UPDATE\\EBOOT.BIN", eboot_bin, eboot_bin_size); + /* the PARAM.SFO file is in the UMD_VIDEO folder. */ + generate_iso9660_file(image, "UMD_VIDEO\\PARAM.SFO", param_sfo, param_sfo_size); + + mock_file(0, "game.iso", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "game.iso"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(eboot_bin); + free(param_sfo); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 0); + ASSERT_NUM_EQUALS(result_iterator, 0); +} + +static void test_hash_psp_homebrew() +{ + const size_t image_size = 3532124; + uint8_t* image = generate_generic_file(image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "fcde8760893b09e508e5f4fe642eb132"; + + mock_file(0, "eboot.pbp", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "eboot.pbp"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "eboot.pbp", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_sega_cd() +{ + /* the first 512 bytes of sector 0 are a volume header and ROM header. + * generate a generic block and add the Sega CD marker */ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "574498e1453cb8934df60c4ab906e783"; + memcpy(image, "SEGADISCSYSTEM ", 16); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SEGA_CD, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_sega_cd_invalid_header() +{ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + test_hash_unknown_format(RC_CONSOLE_SEGA_CD, "game.cue"); + + free(image); +} + +static void test_hash_saturn() +{ + /* the first 512 bytes of sector 0 are a volume header and ROM header. + * generate a generic block and add the Sega CD marker */ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "4cd9c8e41cd8d137be15bbe6a93ae1d8"; + memcpy(image, "SEGA SEGASATURN ", 16); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SATURN, "game.cue"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_saturn_invalid_header() +{ + size_t image_size = 512; + uint8_t* image = generate_generic_file(image_size); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); + + test_hash_unknown_format(RC_CONSOLE_SATURN, "game.cue"); + + free(image); +} + +/* ========================================================================= */ + +void test_hash_disc(void) { + TEST_SUITE_BEGIN(); + + init_mock_filereader(); + init_mock_cdreader(); + + /* 3DO */ + TEST(test_hash_3do_bin); + TEST(test_hash_3do_cue); + TEST(test_hash_3do_iso); + TEST(test_hash_3do_invalid_header); + TEST(test_hash_3do_launchme_case_insensitive); + TEST(test_hash_3do_no_launchme); + TEST(test_hash_3do_long_directory); + + /* Amstrad CPC */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); + + /* Apple II */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.nib", 232960, "96e8d33bdc385fd494327d6e6791cbe4"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); + + /* Atari Jaguar CD */ + TEST(test_hash_atari_jaguar_cd); + TEST(test_hash_atari_jaguar_cd_byteswapped); + TEST(test_hash_atari_jaguar_cd_track3); + TEST(test_hash_atari_jaguar_cd_no_header); + TEST(test_hash_atari_jaguar_cd_no_sessions); + TEST(test_hash_atari_jaguar_cd_homebrew); + + /* Commodore 64 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.nib", 327936, "e7767d32b23e3fa62c5a250a08caeba3"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); + + /* Dreamcast */ + TEST(test_hash_dreamcast_single_bin); + TEST(test_hash_dreamcast_split_bin); + TEST(test_hash_dreamcast_cue); + + /* Gamecube */ + TEST(test_hash_gamecube); + + /* MSX */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); + + /* Neo Geo CD */ + TEST(test_hash_neogeocd); + TEST(test_hash_neogeocd_multiple_prg); + TEST(test_hash_neogeocd_lowercase_ipl_contents); + + /* PC-8800 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); + + /* PC Engine CD */ + TEST(test_hash_pce_cd); + TEST(test_hash_pce_cd_invalid_header); + + /* PC-FX */ + TEST(test_hash_pcfx); + TEST(test_hash_pcfx_invalid_header); + TEST(test_hash_pcfx_pce_cd); + + /* Playstation */ + TEST(test_hash_psx_cd); + TEST(test_hash_psx_cd_no_system_cnf); + TEST(test_hash_psx_cd_exe_in_subfolder); + TEST(test_hash_psx_cd_extra_slash); + + /* Playstation 2 */ + TEST(test_hash_ps2_iso); + TEST(test_hash_ps2_psx); + + /* Playstation Portable */ + TEST(test_hash_psp); + TEST(test_hash_psp_video); + TEST(test_hash_psp_homebrew); + + /* Sega CD */ + TEST(test_hash_sega_cd); + TEST(test_hash_sega_cd_invalid_header); + + /* Sega Saturn */ + TEST(test_hash_saturn); + TEST(test_hash_saturn_invalid_header); + + /* ZX Spectrum */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tap", 1596, "714a9f455e616813dd5421c5b347e5e5"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tzx", 14971, "93723e6d1100f9d1d448a27cf6618c47"); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/test_hash_rom.c b/src/rcheevos/test/rhash/test_hash_rom.c new file mode 100644 index 0000000000..323dda2b12 --- /dev/null +++ b/src/rcheevos/test/rhash/test_hash_rom.c @@ -0,0 +1,899 @@ +#include "rc_hash.h" + +#include "../rhash/rc_hash_internal.h" + +#include "../rc_compat.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +/* in test_hash.c */ +void test_hash_full_file(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); +void test_hash_m3u(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); + +/* ========================================================================= */ + +static void test_hash_arcade(const char* path, const char* expected_md5) +{ + char hash_file[33], hash_iterator[33]; + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARCADE, path); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, path, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_arduboy() +{ + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; + + const char* hex_input = + ":100000000C94690D0C94910D0C94910D0C94910D20\n" + ":100010000C94910D0C94910D0C94910D0C94910DE8\n" + ":100020000C94910D0C94910D0C94C32A0C94352BC7\n" + ":00000001FF\n"; + mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_arduboy_crlf() +{ + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; + + const char* hex_input = + ":100000000C94690D0C94910D0C94910D0C94910D20\r\n" + ":100010000C94910D0C94910D0C94910D0C94910DE8\r\n" + ":100020000C94910D0C94910D0C94C32A0C94352BC7\r\n" + ":00000001FF\r\n"; + mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_arduboy_no_final_lf() +{ + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; + + const char* hex_input = + ":100000000C94690D0C94910D0C94910D0C94910D20\n" + ":100010000C94910D0C94910D0C94910D0C94910DE8\n" + ":100020000C94910D0C94910D0C94C32A0C94352BC7\n" + ":00000001FF"; + mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static uint8_t* generate_atari_7800_file(size_t kb, int with_header, size_t* image_size) +{ + uint8_t* image; + size_t size_needed = kb * 1024; + if (with_header) + size_needed += 128; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) { + if (with_header) { + const uint8_t header[128] = { + 3, 'A', 'T', 'A', 'R', 'I', '7', '8', '0', '0', 0, 0, 0, 0, 0, 0, /* version + magic text */ + 0, 'G', 'a', 'm', 'e', 'N', 'a', 'm', 'e', 0, 0, 0, 0, 0, 0, 0, /* game name */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* game name (cont'd) */ + 0, 0, 2, 0, 0, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, /* attributes */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* unused */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* unused */ + 0, 0, 0, 0, 'A', 'C', 'T', 'U', 'A', 'L', ' ', 'C', 'A', 'R', 'T',/* magic text*/ + 'D', 'A', 'T', 'A', ' ', 'S', 'T', 'A', 'R', 'T', 'S', ' ', 'H', 'E', 'R', 'E' /* magic text */ + }; + memcpy(image, header, sizeof(header)); + image[50] = (uint8_t)(kb / 4); /* 4-byte value starting at address 49 is the ROM size without header */ + + fill_image(image + 128, size_needed - 128); + } + else { + fill_image(image, size_needed); + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +static void test_hash_atari_7800() +{ + size_t image_size; + uint8_t* image = generate_atari_7800_file(16, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_ATARI_7800, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "455f07d8500f3fabc54906737866167f"); + ASSERT_NUM_EQUALS(image_size, 16384); +} + +static void test_hash_atari_7800_with_header() +{ + size_t image_size; + uint8_t* image = generate_atari_7800_file(16, 1, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_ATARI_7800, image, image_size); + free(image); + + /* NOTE: expectation is that this hash matches the hash in test_hash_atari_7800 */ + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "455f07d8500f3fabc54906737866167f"); + ASSERT_NUM_EQUALS(image_size, 16384 + 128); +} + +/* ========================================================================= */ + +uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size) +{ + uint8_t* image; + size_t size_needed = kb * 1024; + if (with_header) + size_needed += 16; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) { + if (with_header) { + image[0] = 'N'; + image[1] = 'E'; + image[2] = 'S'; + image[3] = '\x1A'; + image[4] = (uint8_t)(kb / 16); + + fill_image(image + 16, size_needed - 16); + } + else { + fill_image(image, size_needed); + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +static uint8_t* generate_fds_file(size_t sides, int with_header, size_t* image_size) +{ + uint8_t* image; + size_t size_needed = sides * 65500; + if (with_header) + size_needed += 16; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) { + if (with_header) { + image[0] = 'F'; + image[1] = 'D'; + image[2] = 'S'; + image[3] = '\x1A'; + image[4] = (uint8_t)sides; + + fill_image(image + 16, size_needed - 16); + } + else { + fill_image(image, size_needed); + } + } + + if (image_size) + *image_size = size_needed; + return image; +} + +static void test_hash_nes_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(image_size, 32768); +} + +static void test_hash_nes_32k_with_header() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + /* NOTE: expectation is that this hash matches the hash in test_hash_nes_32k */ + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(image_size, 32768 + 16); +} + +static void test_hash_nes_256k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(256, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "545d527301b8ae148153988d6c4fcb84"); + ASSERT_NUM_EQUALS(image_size, 262144); +} + +static void test_hash_fds_two_sides() +{ + size_t image_size; + uint8_t* image = generate_fds_file(2, 0, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "fd770d4d34c00760fabda6ad294a8f0b"); + ASSERT_NUM_EQUALS(image_size, 65500 * 2); +} + +static void test_hash_fds_two_sides_with_header() +{ + size_t image_size; + uint8_t* image = generate_fds_file(2, 1, &image_size); + char hash[33]; + int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); + free(image); + + /* NOTE: expectation is that this hash matches the hash in test_hash_fds_two_sides */ + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "fd770d4d34c00760fabda6ad294a8f0b"); + ASSERT_NUM_EQUALS(image_size, 65500 * 2 + 16); +} + +static void test_hash_nes_file_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash[33]; + int result; + + mock_file(0, "test.nes", image, image_size); + result = rc_hash_generate_from_file(hash, RC_CONSOLE_NINTENDO, "test.nes"); + free(image); + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(image_size, 32768); +} + +static void test_hash_nes_iterator_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash1[33], hash2[33]; + int result1, result2; + struct rc_hash_iterator iterator; + + mock_file(0, "test.nes", image, image_size); + rc_hash_initialize_iterator(&iterator, "test.nes", NULL, 0); + result1 = rc_hash_iterate(hash1, &iterator); + result2 = rc_hash_iterate(hash2, &iterator); + rc_hash_destroy_iterator(&iterator); + free(image); + + ASSERT_NUM_EQUALS(result1, 1); + ASSERT_STR_EQUALS(hash1, "6a2305a2b6675a97ff792709be1ca857"); + + ASSERT_NUM_EQUALS(result2, 0); + ASSERT_STR_EQUALS(hash2, ""); +} + +static void test_hash_nes_file_iterator_32k() +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 0, &image_size); + char hash1[33], hash2[33]; + int result1, result2; + struct rc_hash_iterator iterator; + rc_hash_initialize_iterator(&iterator, "test.nes", image, image_size); + result1 = rc_hash_iterate(hash1, &iterator); + result2 = rc_hash_iterate(hash2, &iterator); + rc_hash_destroy_iterator(&iterator); + free(image); + + ASSERT_NUM_EQUALS(result1, 1); + ASSERT_STR_EQUALS(hash1, "6a2305a2b6675a97ff792709be1ca857"); + + ASSERT_NUM_EQUALS(result2, 0); + ASSERT_STR_EQUALS(hash2, ""); +} + +/* ========================================================================= */ + +/* first 64 bytes of SUPER MARIO 64 ROM in each N64 format */ +static uint8_t test_rom_z64[64] = { + 0x80, 0x37, 0x12, 0x40, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x24, 0x60, 0x00, 0x00, 0x00, 0x14, 0x44, + 0x63, 0x5A, 0x2B, 0xFF, 0x8B, 0x02, 0x23, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x53, 0x55, 0x50, 0x45, 0x52, 0x20, 0x4D, 0x41, 0x52, 0x49, 0x4F, 0x20, 0x36, 0x34, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x53, 0x4D, 0x45, 0x00 +}; + +static uint8_t test_rom_v64[64] = { + 0x37, 0x80, 0x40, 0x12, 0x00, 0x00, 0x0F, 0x00, 0x24, 0x80, 0x00, 0x60, 0x00, 0x00, 0x44, 0x14, + 0x5A, 0x63, 0xFF, 0x2B, 0x02, 0x8B, 0x26, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0x53, 0x45, 0x50, 0x20, 0x52, 0x41, 0x4D, 0x49, 0x52, 0x20, 0x4F, 0x34, 0x36, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x4D, 0x53, 0x00, 0x45 +}; + +static uint8_t test_rom_n64[64] = { + 0x40, 0x12, 0x37, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x60, 0x24, 0x80, 0x44, 0x14, 0x00, 0x00, + 0xFF, 0x2B, 0x5A, 0x63, 0x26, 0x23, 0x02, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x45, 0x50, 0x55, 0x53, 0x41, 0x4D, 0x20, 0x52, 0x20, 0x4F, 0x49, 0x52, 0x20, 0x20, 0x34, 0x36, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x45, 0x4D, 0x53 +}; + +/* first 64 bytes of DOSHIN THE GIANT in ndd format */ +static uint8_t test_rom_ndd[64] = { + 0xE8, 0x48, 0xD3, 0x16, 0x10, 0x13, 0x00, 0x45, 0x0C, 0x18, 0x24, 0x30, 0x3C, 0x48, 0x54, 0x60, + 0x6C, 0x78, 0x84, 0x90, 0x9C, 0xA8, 0xB4, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x02, 0x5C, 0x00, + 0x10, 0x16, 0x1C, 0x22, 0x28, 0x2A, 0x31, 0x32, 0x3A, 0x40, 0x46, 0x4C, 0x04, 0x0C, 0x14, 0x1C, + 0x24, 0x2C, 0x34, 0x3C, 0x44, 0x4C, 0x54, 0x5C, 0x04, 0x0C, 0x14, 0x1C, 0x24, 0x2C, 0x34, 0x3C +}; + +static void test_hash_n64(uint8_t* buffer, size_t buffer_size, const char* expected_hash) +{ + char hash[33]; + int result; + + rc_hash_reset_filereader(); /* explicitly unset the filereader */ + result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO_64, buffer, buffer_size); + init_mock_filereader(); /* restore the mock filereader */ + + ASSERT_NUM_EQUALS(result, 1); + ASSERT_STR_EQUALS(hash, expected_hash); +} + +static void test_hash_n64_file(const char* filename, uint8_t* buffer, size_t buffer_size, const char* expected_hash) +{ + char hash_file[33], hash_iterator[33]; + mock_file(0, filename, buffer, buffer_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_64, filename); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, filename, NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +/* ========================================================================= */ + +static uint8_t* generate_nds_file(size_t mb, uint32_t arm9_size, uint32_t arm7_size, size_t* image_size) +{ + uint8_t* image; + const size_t size_needed = mb * 1024 * 1024; + + image = (uint8_t*)calloc(size_needed, 1); + if (image != NULL) { + uint32_t arm9_addr = 65536; + uint32_t arm7_addr = arm9_addr + arm9_size; + uint32_t icon_addr = arm7_addr + arm7_size; + + fill_image(image, size_needed); + + image[0x20] = (arm9_addr & 0xFF); + image[0x21] = ((arm9_addr >> 8) & 0xFF); + image[0x22] = ((arm9_addr >> 16) & 0xFF); + image[0x23] = ((arm9_addr >> 24) & 0xFF); + image[0x2C] = (arm9_size & 0xFF); + image[0x2D] = ((arm9_size >> 8) & 0xFF); + image[0x2E] = ((arm9_size >> 16) & 0xFF); + image[0x2F] = ((arm9_size >> 24) & 0xFF); + + image[0x30] = (arm7_addr & 0xFF); + image[0x31] = ((arm7_addr >> 8) & 0xFF); + image[0x32] = ((arm7_addr >> 16) & 0xFF); + image[0x33] = ((arm7_addr >> 24) & 0xFF); + image[0x3C] = (arm7_size & 0xFF); + image[0x3D] = ((arm7_size >> 8) & 0xFF); + image[0x3E] = ((arm7_size >> 16) & 0xFF); + image[0x3F] = ((arm7_size >> 24) & 0xFF); + + image[0x68] = (icon_addr & 0xFF); + image[0x69] = ((icon_addr >> 8) & 0xFF); + image[0x6A] = ((icon_addr >> 16) & 0xFF); + image[0x6B] = ((icon_addr >> 24) & 0xFF); + } + + if (image_size) + *image_size = size_needed; + return image; +} + +static void test_hash_nds() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + mock_file(0, "game.nds", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DS, "game.nds"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +static void test_hash_nds_supercard() +{ + size_t image_size, image2_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + ASSERT_PTR_NOT_NULL(image); + ASSERT_NUM_GREATER(image_size, 0); + + /* inject the SuperCard header (512 bytes) */ + image2_size = image_size + 512; + uint8_t* image2 = malloc(image2_size); + ASSERT_PTR_NOT_NULL(image2); + memcpy(&image2[512], &image[0], image_size); + memset(&image2[0], 0, 512); + image2[0] = 0x2E; + image2[1] = 0x00; + image2[2] = 0x00; + image2[3] = 0xEA; + image2[0xB0] = 0x44; + image2[0xB1] = 0x46; + image2[0xB2] = 0x96; + image2[0xB3] = 0x00; + + mock_file(0, "game.nds", image2, image2_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DS, "game.nds"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + free(image2); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +static void test_hash_nds_buffered() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_buffer[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + /* test file hash */ + int result_buffer = rc_hash_generate_from_buffer(hash_buffer, RC_CONSOLE_NINTENDO_DS, image, image_size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", image, image_size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_buffer, 1); + ASSERT_STR_EQUALS(hash_buffer, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +/* ========================================================================= */ + +static void test_hash_dsi() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + mock_file(0, "game.nds", image, image_size); + + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DSI, "game.nds"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +static void test_hash_dsi_buffered() +{ + size_t image_size; + uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); + char hash_buffer[33], hash_iterator[33]; + const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; + + /* test file hash */ + int result_buffer = rc_hash_generate_from_buffer(hash_buffer, RC_CONSOLE_NINTENDO_DSI, image, image_size); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.nds", image, image_size); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_buffer, 1); + ASSERT_STR_EQUALS(hash_buffer, expected_hash); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_hash); +} + +/* ========================================================================= */ + +static void test_hash_scv_cart() +{ + size_t image_size = 32768 + 32; + uint8_t* image = generate_generic_file(image_size); + char hash_file[33], hash_iterator[33]; + const char* expected_md5 = "4309c9844b44f9ff8256dfc04687b8fd"; + + memcpy(image, "EmuSCV....CART..................", 32); + + mock_file(0, "game.cart", image, image_size); + /* test file hash */ + int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SUPER_CASSETTEVISION, "game.cart"); + + /* test file identification from iterator */ + int result_iterator; + struct rc_hash_iterator iterator; + + rc_hash_initialize_iterator(&iterator, "game.cart", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* cleanup */ + free(image); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +void test_hash_rom(void) { + TEST_SUITE_BEGIN(); + + init_mock_filereader(); + + /* Arcade */ + TEST_PARAMS2(test_hash_arcade, "game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "game.7z", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "roms\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "C:\\roms\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "/home/user/roms/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "/home/user/games/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "/home/user/roms/game.7z", "c8d46d341bea4fd5bff866a65ff8aea9"); + + TEST_PARAMS2(test_hash_arcade, "/home/user/nes_game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS2(test_hash_arcade, "/home/user/nes/game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS2(test_hash_arcade, "C:\\roms\\nes\\game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS2(test_hash_arcade, "C:\\roms\\NES\\game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS2(test_hash_arcade, "nes\\game.zip", "9b7aad36b365712fc93728088de4c209"); + TEST_PARAMS2(test_hash_arcade, "/home/user/snes/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + TEST_PARAMS2(test_hash_arcade, "/home/user/nes2/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); + + /* we don't care that multiple aliases for the same system generate different hashes - the point is + * that they don't generate the same hash as an actual arcade ROM with the same filename. */ + TEST_PARAMS2(test_hash_arcade, "/home/user/chf/game.zip", "6ef57f16562ea0c7f49d93853b313e32"); + TEST_PARAMS2(test_hash_arcade, "/home/user/channelf/game.zip", "7b6506637a0cc79bd1d24a43a34fa3b9"); + TEST_PARAMS2(test_hash_arcade, "/home/user/coleco/game.zip", "c546f63ae7de98add4b9f221a4749260"); + TEST_PARAMS2(test_hash_arcade, "/home/user/colecovision/game.zip", "47279207b94dbf2a45cb13efa56d685e"); + TEST_PARAMS2(test_hash_arcade, "/home/user/msx/game.zip", "59ab85f6b56324fd81b4e324b804c29f"); + TEST_PARAMS2(test_hash_arcade, "/home/user/msx1/game.zip", "33328d832dcb0854383cdd4a4565c459"); + TEST_PARAMS2(test_hash_arcade, "/home/user/pce/game.zip", "c414a783f3983bbe2e9e01d9d5320c7e"); + TEST_PARAMS2(test_hash_arcade, "/home/user/pcengine/game.zip", "49370c3cbe98bdcdce545c68379487db"); + TEST_PARAMS2(test_hash_arcade, "/home/user/sgx/game.zip", "db545ab29694bfda1010317d4bac83b8"); + TEST_PARAMS2(test_hash_arcade, "/home/user/supergrafx/game.zip", "5665c9ef4c2f6609d8e420c4d86ba692"); + TEST_PARAMS2(test_hash_arcade, "/home/user/tg16/game.zip", "8b6c5c2e54915be2cdba63973862e143"); + TEST_PARAMS2(test_hash_arcade, "/home/user/fds/game.zip", "c0c135a97e8c577cfdf9204823ff211f"); + TEST_PARAMS2(test_hash_arcade, "/home/user/gamegear/game.zip", "f6f471e952b8103032b723f57bdbe767"); + TEST_PARAMS2(test_hash_arcade, "/home/user/mastersystem/game.zip", "f4805afe0ff5647140a26bd0a1057373"); + TEST_PARAMS2(test_hash_arcade, "/home/user/sms/game.zip", "43f35f575dead94dd2f42f9caf69fe5a"); + TEST_PARAMS2(test_hash_arcade, "/home/user/megadriv/game.zip", "f99d0aaf12ba3eb6ced9878c76692c63"); + TEST_PARAMS2(test_hash_arcade, "/home/user/megadrive/game.zip", "73eb5d7034b382093b1d36414d9e84e4"); + TEST_PARAMS2(test_hash_arcade, "/home/user/genesis/game.zip", "b62f810c63e1cba7f5b7569643bec236"); + TEST_PARAMS2(test_hash_arcade, "/home/user/sg1000/game.zip", "e8f6c711c4371f09537b4f2a7a304d6c"); + TEST_PARAMS2(test_hash_arcade, "/home/user/spectrum/game.zip", "a5f62157b2617bd728c4b1bc885c29e9"); + TEST_PARAMS2(test_hash_arcade, "/home/user/ngp/game.zip", "d4133b74c4e57274ca514e27a370dcb6"); + + /* Arduboy */ + TEST(test_hash_arduboy); + TEST(test_hash_arduboy_crlf); + TEST(test_hash_arduboy_no_final_lf); + + /* Arcadia 2001 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ARCADIA_2001, "test.bin", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + + /* Atari 2600 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ATARI_2600, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + + /* Atari 7800 */ + TEST(test_hash_atari_7800); + TEST(test_hash_atari_7800_with_header); + + /* Atari Jaguar */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ATARI_JAGUAR, "test.jag", 0x400000, "a247ec8a8c42e18fcb80702dfadac14b"); + + /* Colecovision */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COLECOVISION, "test.col", 16384, "455f07d8500f3fabc54906737866167f"); + + /* Elektor TV Games Computer */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, "test.pgm", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, "test.tvc", 1861, "37097124a29aff663432d049654a17dc"); + + /* Fairchild Channel F */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_FAIRCHILD_CHANNEL_F, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_FAIRCHILD_CHANNEL_F, "test.chf", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + + /* Gameboy */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY, "test.gb", 131072, "a0f425b23200568132ba76b2405e3933"); + + /* Gameboy Color */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY_COLOR, "test.gbc", 2097152, "cf86acf519625a25a17b1246975e90ae"); + + /* Gameboy Advance */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY_COLOR, "test.gba", 4194304, "a247ec8a8c42e18fcb80702dfadac14b"); + + /* Game Gear */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAME_GEAR, "test.gg", 524288, "68f0f13b598e0b66461bc578375c3888"); + + /* Intellivision */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_INTELLIVISION, "test.bin", 8192, "ce1127f881b40ce6a67ecefba50e2835"); + + /* Interton VC 4000 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_INTERTON_VC_4000, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); + + /* Magnavox Odyssey 2 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MAGNAVOX_ODYSSEY2, "test.bin", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + + /* Master System */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MASTER_SYSTEM, "test.sms", 131072, "a0f425b23200568132ba76b2405e3933"); + + /* Mega Drive */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MEGA_DRIVE, "test.md", 1048576, "da9461b3b0f74becc3ccf6c2a094c516"); + TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MEGA_DRIVE, "test.md", 1048576, "da9461b3b0f74becc3ccf6c2a094c516"); + + /* Mega Duck */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MEGADUCK, "test.bin", 65536, "8e6576cd5c21e44e0bbfc4480577b040"); + + /* Neo Geo Pocket */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_NEOGEO_POCKET, "test.ngc", 2097152, "cf86acf519625a25a17b1246975e90ae"); + + /* NES */ + TEST(test_hash_nes_32k); + TEST(test_hash_nes_32k_with_header); + TEST(test_hash_nes_256k); + TEST(test_hash_fds_two_sides); + TEST(test_hash_fds_two_sides_with_header); + + TEST(test_hash_nes_file_32k); + TEST(test_hash_nes_file_iterator_32k); + TEST(test_hash_nes_iterator_32k); + + /* Nintendo 64 */ + TEST_PARAMS3(test_hash_n64, test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS3(test_hash_n64, test_rom_v64, sizeof(test_rom_v64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS3(test_hash_n64, test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.z64", test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.v64", test_rom_v64, sizeof(test_rom_v64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.n64", test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); + TEST_PARAMS4(test_hash_n64_file, "game.n64", test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); /* misnamed */ + TEST_PARAMS4(test_hash_n64_file, "game.z64", test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); /* misnamed */ + TEST_PARAMS3(test_hash_n64, test_rom_ndd, sizeof(test_rom_ndd), "a698b32a52970d8a52a5a52c83acc2a9"); + + /* Nintendo DS */ + TEST(test_hash_nds); + TEST(test_hash_nds_supercard); + TEST(test_hash_nds_buffered); + + /* Nintendo DSi */ + TEST(test_hash_dsi); + TEST(test_hash_dsi_buffered); + + /* Oric (no fixed file size) */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ORIC, "test.tap", 18119, "953a2baa3232c63286aeae36b2172cef"); + + /* PC Engine */ + /* NOTE: because the data after the header doesn't match, the headered and non-headered hashes won't match + * but the test results ensure that we're only hashing the portion after the header when detected */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 524288, "68f0f13b598e0b66461bc578375c3888"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 524288 + 512, "258c93ebaca1c3f488ab48218e5e8d38"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 491520 + 512, "ebb565a7f964ccdfaecdce0d6ed540af"); + + /* Pokemon Mini */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_POKEMON_MINI, "test.min", 524288, "68f0f13b598e0b66461bc578375c3888"); + + /* Sega 32X */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SEGA_32X, "test.bin", 3145728, "07d733f252896ec41b4fd521fe610e2c"); + + /* SG-1000 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.sg", 32768, "6a2305a2b6675a97ff792709be1ca857"); + + /* SNES */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_NINTENDO, "test.smc", 524288, "68f0f13b598e0b66461bc578375c3888"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_NINTENDO, "test.smc", 524288 + 512, "258c93ebaca1c3f488ab48218e5e8d38"); + + /* Super Cassette Vision */ + TEST(test_hash_scv_cart); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_CASSETTEVISION, "test.bin", 32768, "6a2305a2b6675a97ff792709be1ca857"); + + /* TI-83 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TI83, "test.83g", 1695, "bfb6048395a425c69743900785987c42"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TI83, "test.83p", 2500, "6e81d530ee9a79d4f4f505729ad74bb5"); + + /* TIC-80 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TIC80, "test.tic", 67682, "79b96f4ffcedb3ce8210a83b22cd2c69"); + + /* Uzebox */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_UZEBOX, "test.uze", 53654, "a9aab505e92edc034d3c732869159789"); + + /* Vectrex */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.vec", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); + + /* VirtualBoy */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.vb", 524288, "68f0f13b598e0b66461bc578375c3888"); + + /* Watara Supervision */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPERVISION, "test.sv", 32768, "6a2305a2b6675a97ff792709be1ca857"); + + /* WASM-4 */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WASM4, "test.wasm", 33454, "bce38bb5f05622fc7e0e56757059d180"); + + /* WonderSwan */ + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WONDERSWAN, "test.ws", 524288, "68f0f13b598e0b66461bc578375c3888"); + TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WONDERSWAN, "test.wsc", 4194304, "a247ec8a8c42e18fcb80702dfadac14b"); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/rhash/test_hash_zip.c b/src/rcheevos/test/rhash/test_hash_zip.c new file mode 100644 index 0000000000..10cc2040d5 --- /dev/null +++ b/src/rcheevos/test/rhash/test_hash_zip.c @@ -0,0 +1,551 @@ +#include "rc_hash.h" + +#include "../rhash/rc_hash_internal.h" + +#include "../rc_compat.h" +#include "../test_framework.h" +#include "data.h" +#include "mock_filereader.h" + +#include + +typedef struct mock_zip_file_t { + uint8_t* buffer; + uint8_t* ptr; + uint8_t* file_ptr[8]; + uint32_t num_files; + uint8_t is_zip64; +} mock_zip_file_t; + +static void mock_zip_add_file(mock_zip_file_t* zip, const char* filename, uint32_t crc32, uint32_t size) +{ + const size_t filename_len = strlen(filename); + uint8_t* out = zip->ptr; + + zip->file_ptr[zip->num_files++] = out; + + /* local file signature */ + *out++ = 'P'; + *out++ = 'K'; + *out++ = 0x03; + *out++ = 0x04; + + /* version needed to extract */ + *out++ = 0x14; + *out++ = 0x00; + + /* general purpose bit flag*/ + *out++ = 0x02; + *out++ = 0x00; + + /* compression method */ + *out++ = 0x08; + *out++ = 0x00; + + /* file last modified time */ + *out++ = 0x00; + *out++ = 0xBC; + + /* file list modified date */ + *out++ = 0x98; + *out++ = 0x21; + + /* CRC-32 */ + *out++ = crc32 & 0xFF; + *out++ = (crc32 >> 8) & 0xFF; + *out++ = (crc32 >> 16) & 0xFF; + *out++ = (crc32 >> 24) & 0xFF; + + /* compressed size */ + *out++ = size & 0xFF; + *out++ = (size >> 8) & 0xFF; + *out++ = (size >> 16) & 0xFF; + *out++ = (size >> 24) & 0xFF; + + /* uncompressed size */ + *out++ = size & 0xFF; + *out++ = (size >> 8) & 0xFF; + *out++ = (size >> 16) & 0xFF; + *out++ = (size >> 24) & 0xFF; + + /* file name length */ + *out++ = filename_len & 0xFF; + *out++ = (filename_len >> 8) & 0xFF; + + /* extra field length */ + *out++ = 0; + *out++ = 0; + + /* file name */ + memcpy(out, filename, filename_len); + out += filename_len; + + /* compressed content */ + *out++ = 0x73; + *out++ = 0x02; + *out++ = 0x00; + + zip->ptr = out; +} + +static size_t mock_zip_finalize(mock_zip_file_t* zip, const char* comment) +{ + size_t comment_len = strlen(comment); + uint8_t* out = zip->ptr; + uint8_t* first_cdir_entry = zip->ptr; + size_t offset; + uint32_t filename_len; + uint32_t i; + + for (i = 0; i < zip->num_files; i++) { + uint8_t* in = zip->file_ptr[i]; + + /* central directory file header */ + *out++ = 'P'; + *out++ = 'K'; + *out++ = 0x01; + *out++ = 0x02; + + /* version made by */ + *out++ = 0x14; + *out++ = 0x00; + + /* version needed to extract (2) */ + /* general purpose bit flag (2) */ + /* compression method (2) */ + /* file last modified time (2) */ + /* file list modified date (2) */ + /* CRC-32 (4) */ + /* compressed size (4) */ + /* uncompressed size (4) */ + /* file name length (2) */ + /* extra field length (2) */ + memcpy(out, &in[4], 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2); + if (zip->is_zip64) { + /* in zip64 mode, blank out the size and the actual size will be appended in an extended field */ + memset(&out[14], 0xFF, 8); + out[24] = 0x14; /* extra field length */ + } + out += 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2; + + /* file comment length */ + *out++ = 0; + *out++ = 0; + + /* disk number where file starts */ + *out++ = 0; + *out++ = 0; + + /* internal file attributes */ + *out++ = 0; + *out++ = 0; + + /* external file attributes */ + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* local file header offset */ + offset = in - zip->buffer; + *out++ = offset & 0xFF; + *out++ = (offset >> 8) & 0xFF; + *out++ = (offset >> 16) & 0xFF; + *out++ = (offset >> 24) & 0xFF; + + /* file name */ + filename_len = (in[27] << 8) | in[26]; + memcpy(out, &in[30], filename_len); + out += filename_len; + + if (zip->is_zip64) { + /* zip64 extended information extra field header id */ + *out++ = 0x01; + *out++ = 0x00; + + /* size of extra field chunk */ + *out++ = 0x10; /* only providing file sizes */ + *out++ = 0x00; + + /* uncompressed file size */ + memset(out, 0, 28); + memcpy(out, &in[22], 4); + out += 8; + + /* compressed file size */ + memset(out, 0, 16); + memcpy(out, &in[18], 4); + out += 8; + } + } + + zip->ptr = out; + + if (zip->is_zip64) { + /* end of central directory header */ + *out++ = 'P'; + *out++ = 'K'; + *out++ = 0x06; + *out++ = 0x06; + + /* size of EOCD64 minus 12 */ + *out++ = 0x2C; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* version made by */ + *out++ = 0x2D; + *out++ = 0x00; + + /* version needed to extract */ + *out++ = 0x2D; + *out++ = 0x00; + + /* disk number */ + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* disk number of central directory */ + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* number of central directory records on this disk */ + *out++ = zip->num_files & 0xFF; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* total number of central directory records */ + *out++ = zip->num_files & 0xFF; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* size of central directory */ + offset = zip->ptr - first_cdir_entry; + *out++ = offset & 0xFF; + *out++ = (offset >> 8) & 0xFF; + *out++ = (offset >> 16) & 0xFF; + *out++ = (offset >> 24) & 0xFF; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* address of first central directory entry */ + offset = first_cdir_entry - zip->buffer; + *out++ = offset & 0xFF; + *out++ = (offset >> 8) & 0xFF; + *out++ = (offset >> 16) & 0xFF; + *out++ = (offset >> 24) & 0xFF; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* end of central directory locator header */ + *out++ = 'P'; + *out++ = 'K'; + *out++ = 0x06; + *out++ = 0x07; + + /* disk number */ + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* address of central directory 64 */ + offset = zip->ptr - zip->buffer; + *out++ = offset & 0xFF; + *out++ = (offset >> 8) & 0xFF; + *out++ = (offset >> 16) & 0xFF; + *out++ = (offset >> 24) & 0xFF; + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + /* total number of disks */ + *out++ = 1; + *out++ = 0; + *out++ = 0; + *out++ = 0; + + zip->ptr = out; + } + + /* end of central directory header */ + *out++ = 'P'; + *out++ = 'K'; + *out++ = 0x05; + *out++ = 0x06; + + /* disk number */ + *out++ = 0; + *out++ = 0; + + /* central directory disk number */ + *out++ = 0; + *out++ = 0; + + /* number of central directory records on this disk */ + *out++ = zip->num_files & 0xFF; + *out++ = 0; + + /* total number of central directory records */ + *out++ = zip->num_files & 0xFF; + *out++ = 0; + + if (zip->is_zip64) { + /* size and address of central directory are -1 in zip64 */ + memset(out, 0xFF, 8); + out += 8; + } + else { + /* size of central directory */ + offset = zip->ptr - first_cdir_entry; + *out++ = offset & 0xFF; + *out++ = (offset >> 8) & 0xFF; + *out++ = (offset >> 16) & 0xFF; + *out++ = (offset >> 24) & 0xFF; + + /* address of first central directory entry */ + offset = first_cdir_entry - zip->buffer; + *out++ = offset & 0xFF; + *out++ = (offset >> 8) & 0xFF; + *out++ = (offset >> 16) & 0xFF; + *out++ = (offset >> 24) & 0xFF; + } + + /* comment length */ + *out++ = comment_len & 0xFF; + *out++ = (comment_len >> 8) & 0xFF; + + if (comment_len) { + memcpy(out, comment, comment_len); + out += comment_len; + } + + zip->ptr = out; + + return (zip->ptr - zip->buffer); +} + +/* ========================================================================= */ + +static void test_hash_arduboy_fx() +{ + char hash_file[33], hash_iterator[33]; + mock_zip_file_t zip; + uint8_t zip_contents[768]; + size_t zip_size; + const char* expected_md5 = "e696445c353e9d6b3d60bf5d194b82cf"; + int result_file, result_iterator; + + memset(&zip, 0, sizeof(zip)); + zip.ptr = zip.buffer = zip_contents; + mock_zip_add_file(&zip, "info.json", 0xA40B2541, 35); + mock_zip_add_file(&zip, "game.bin", 0x5AA654C0, 96); + mock_zip_add_file(&zip, "save.bin", 0xFF000000, 1); + mock_zip_add_file(&zip, "interp_s2_ArduboyFX.hex", 0x50648360, 71); + mock_zip_add_file(&zip, "screenshot.png", 0x30056694, 48); + zip_size = mock_zip_finalize(&zip, ""); + ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); + + mock_file(0, "game.arduboy", zip_contents, zip_size); + + /* test file hash */ + result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.arduboy"); + + /* test file identification from iterator */ + struct rc_hash_iterator iterator; + rc_hash_initialize_iterator(&iterator, "game.arduboy", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +/* ========================================================================= */ + +static void test_hash_msdos_dosz() +{ + char hash_file[33], hash_iterator[33]; + mock_zip_file_t zip; + uint8_t zip_contents[512]; + size_t zip_size; + const char* expected_md5 = "59a255662262f5ada32791b8c36e8ea7"; + int result_file, result_iterator; + + memset(&zip, 0, sizeof(zip)); + zip.ptr = zip.buffer = zip_contents; + mock_zip_add_file(&zip, "FOLDER/", 0, 0); + mock_zip_add_file(&zip, "FOLDER/SUB.TXT", 0x4AD0CF31, 1); + mock_zip_add_file(&zip, "ROOT.TXT", 0xD3D99E8B, 1); + zip_size = mock_zip_finalize(&zip, "TORRENTZIPPED-FD07C52C"); + ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); + + mock_file(0, "game.dosz", zip_contents, zip_size); + + /* test file hash */ + result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_MS_DOS, "game.dosz"); + + /* test file identification from iterator */ + struct rc_hash_iterator iterator; + rc_hash_initialize_iterator(&iterator, "game.dosz", NULL, 0); + result_iterator = rc_hash_iterate(hash_iterator, &iterator); + rc_hash_destroy_iterator(&iterator); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); + + ASSERT_NUM_EQUALS(result_iterator, 1); + ASSERT_STR_EQUALS(hash_iterator, expected_md5); +} + +static void test_hash_msdos_dosz_zip64() +{ + char hash_file[33]; + mock_zip_file_t zip; + uint8_t zip_contents[512]; + size_t zip_size; + const char* expected_md5 = "927dad0a57a2860267ab7bcdb8bc3f61"; + int result_file; + + memset(&zip, 0, sizeof(zip)); + zip.ptr = zip.buffer = zip_contents; + zip.is_zip64 = 1; + mock_zip_add_file(&zip, "README", 0x69FFE77E, 36); + zip_size = mock_zip_finalize(&zip, ""); + ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); + + mock_file(0, "game.dosz", zip_contents, zip_size); + + /* test file hash */ + result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_MS_DOS, "game.dosz"); + + /* validation */ + ASSERT_NUM_EQUALS(result_file, 1); + ASSERT_STR_EQUALS(hash_file, expected_md5); +} + +static void test_hash_msdos_dosz_with_dosc() +{ + char hash_dosc[33]; + const char* expected_dosc_md5 = "dd0c0b0c170c30722784e5e962764c35"; + mock_zip_file_t zip; + uint8_t zip_contents[512]; + size_t zip_size; + int result_dosc; + + memset(&zip, 0, sizeof(zip)); + zip.ptr = zip.buffer = zip_contents; + mock_zip_add_file(&zip, "FOLDER/", 0, 0); + mock_zip_add_file(&zip, "FOLDER/SUB.TXT", 0x4AD0CF31, 1); + mock_zip_add_file(&zip, "ROOT.TXT", 0xD3D99E8B, 1); + zip_size = mock_zip_finalize(&zip, "TORRENTZIPPED-FD07C52C"); + ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); + + /* Add main dosz file and overlay dosc file which will get hashed together */ + /* Just use the same file for both to simplify the test */ + mock_file(0, "game.dosz", zip_contents, zip_size); + mock_file(1, "game.dosc", zip_contents, zip_size); + + /* test file hash */ + result_dosc = rc_hash_generate_from_file(hash_dosc, RC_CONSOLE_MS_DOS, "game.dosz"); + + /* validation */ + ASSERT_NUM_EQUALS(result_dosc, 1); + ASSERT_STR_EQUALS(hash_dosc, expected_dosc_md5); +} + +static void test_hash_msdos_dosz_with_parent() +{ + char hash_dosz[33], hash_dosc[33], hash_dosc2[33]; + const char* expected_dosz_md5 = "623c759476b8b5adb46362f8f0b60769"; + const char* expected_dosc_md5 = "ecd9d776cbaad63094829d7b8dbe5959"; + const char* expected_dosc2_md5 = "cb55c123936ad84479032ea6444cb1a1"; + mock_zip_file_t dosz, dosc; + uint8_t dosz_contents[512], dosc_contents[512]; + size_t dosz_size, dosc_size; + int result_dosz, result_dosc, result_dosc2; + + memset(&dosz, 0, sizeof(dosz)); + dosz.ptr = dosz.buffer = dosz_contents; + mock_zip_add_file(&dosz, "FOLDER/", 0, 0); + mock_zip_add_file(&dosz, "FOLDER/SUB.TXT", 0x4AD0CF31, 1); + mock_zip_add_file(&dosz, "ROOT.TXT", 0xD3D99E8B, 1); + dosz_size = mock_zip_finalize(&dosz, "TORRENTZIPPED-FD07C52C"); + ASSERT_NUM_LESS_EQUALS(dosz_size, sizeof(dosz_contents)); + + memset(&dosc, 0, sizeof(dosz)); + dosc.ptr = dosc.buffer = dosc_contents; + mock_zip_add_file(&dosc, "base.dosz.parent", 0, 0); + mock_zip_add_file(&dosc, "CHILD.TXT", 0x22B35429, 5); + dosc_size = mock_zip_finalize(&dosc, ""); + ASSERT_NUM_LESS_EQUALS(dosz_size, sizeof(dosc_contents)); + + /* Add base dosz file and child dosz file which will get hashed together */ + mock_file(0, "base.dosz", dosz_contents, dosz_size); + mock_file(1, "child.dosz", dosc_contents, dosc_size); + + /* test file hash */ + result_dosz = rc_hash_generate_from_file(hash_dosz, RC_CONSOLE_MS_DOS, "child.dosz"); + + /* test file hash with base.dosc also existing */ + mock_file(2, "base.dosc", dosz_contents, dosz_size); + result_dosc = rc_hash_generate_from_file(hash_dosc, RC_CONSOLE_MS_DOS, "child.dosz"); + + /* test file hash with child.dosc also existing */ + mock_file(3, "child.dosc", dosz_contents, dosz_size); + result_dosc2 = rc_hash_generate_from_file(hash_dosc2, RC_CONSOLE_MS_DOS, "child.dosz"); + + /* validation */ + ASSERT_NUM_EQUALS(result_dosz, 1); + ASSERT_NUM_EQUALS(result_dosc, 1); + ASSERT_NUM_EQUALS(result_dosc2, 1); + ASSERT_STR_EQUALS(hash_dosz, expected_dosz_md5); + ASSERT_STR_EQUALS(hash_dosc, expected_dosc_md5); + ASSERT_STR_EQUALS(hash_dosc2, expected_dosc2_md5); +} + +/* ========================================================================= */ + +void test_hash_zip(void) { + TEST_SUITE_BEGIN(); + + /* Arduboy FX */ + TEST(test_hash_arduboy_fx); + + /* MS DOS */ + TEST(test_hash_msdos_dosz); + TEST(test_hash_msdos_dosz_zip64); + TEST(test_hash_msdos_dosz_with_dosc); + TEST(test_hash_msdos_dosz_with_parent); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/test.c b/src/rcheevos/test/test.c new file mode 100644 index 0000000000..25c88fd2f1 --- /dev/null +++ b/src/rcheevos/test/test.c @@ -0,0 +1,113 @@ +#include "rc_internal.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION +#include "rc_client_raintegration.h" +#endif + +#include "test_framework.h" + +#include + +#define TIMING_TEST 0 + +extern void test_timing(); + +extern void test_condition(); +extern void test_memref(); +extern void test_operand(); +extern void test_condset(); +extern void test_trigger(); +extern void test_value(); +extern void test_format(); +extern void test_lboard(); +extern void test_richpresence(); +extern void test_runtime(); +extern void test_runtime_progress(); + +extern void test_client(); +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL +extern void test_client_external(); +#endif +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION +extern void test_client_raintegration(); +#endif + +extern void test_consoleinfo(); +extern void test_rc_libretro(); +extern void test_rc_validate(); + +extern void test_hash(); +#ifndef RC_HASH_NO_ROM +extern void test_hash_rom(); +#endif +#ifndef RC_HASH_NO_DISC +extern void test_cdreader(); +extern void test_hash_disc(); +#endif +#ifndef RC_HASH_NO_ZIP +extern void test_hash_zip(); +#endif + +extern void test_rapi_common(); +extern void test_rapi_user(); +extern void test_rapi_runtime(); +extern void test_rapi_info(); +extern void test_rapi_editor(); + +TEST_FRAMEWORK_DECLARATIONS() + +int main(void) { + TEST_FRAMEWORK_INIT(); + +#if TIMING_TEST + test_timing(); +#else + test_memref(); + test_operand(); + test_condition(); + test_condset(); + test_trigger(); + test_value(); + test_format(); + test_lboard(); + test_richpresence(); + test_runtime(); + test_runtime_progress(); + + test_consoleinfo(); + test_rc_validate(); + + test_rapi_common(); + test_rapi_user(); + test_rapi_runtime(); + test_rapi_info(); + test_rapi_editor(); + + test_client(); +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + test_client_external(); +#endif +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + test_client_raintegration(); +#endif +#ifdef RC_CLIENT_SUPPORTS_HASH + /* no direct compile option for hash support, so leverage RC_CLIENT_SUPPORTS_HASH */ + test_rc_libretro(); /* libretro extensions require hash support */ + test_hash(); + #ifndef RC_HASH_NO_ROM + test_hash_rom(); + #endif + #ifndef RC_HASH_NO_DISC + test_cdreader(); + test_hash_disc(); + #endif + #ifndef RC_HASH_NO_ZIP + test_hash_zip(); + #endif +#endif +#endif + + TEST_FRAMEWORK_SHUTDOWN(); + + return TEST_FRAMEWORK_PASSED() ? 0 : 1; +} diff --git a/src/rcheevos/test/test_framework.h b/src/rcheevos/test/test_framework.h new file mode 100644 index 0000000000..4757c1ee59 --- /dev/null +++ b/src/rcheevos/test/test_framework.h @@ -0,0 +1,205 @@ +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include +#include +#include + +typedef struct +{ + const char* current_suite; + const char* current_test; + const char* current_test_file_stack[16]; + const char* current_test_func_stack[16]; + uint32_t current_test_line_stack[16]; + uint32_t current_test_stack_index; + int current_test_fail; + uint32_t fail_count; + uint32_t run_count; + time_t time_start; +} test_framework_state_t; + +extern test_framework_state_t __test_framework_state; +extern const char* test_framework_basename(const char* path); + +#define TEST_FRAMEWORK_DECLARATIONS() \ + test_framework_state_t __test_framework_state; \ + \ + const char* test_framework_basename(const char* path) { \ + const char* last_slash = path; \ + while (*path) { \ + if (*path == '/' || *path == '\\') last_slash = path + 1; \ + ++path; \ + } \ + return last_slash; \ + } + +#define TEST_FRAMEWORK_INIT() \ + memset(&__test_framework_state, 0, sizeof(__test_framework_state)); \ + __test_framework_state.time_start = time(0) + +#define TEST_FRAMEWORK_SHUTDOWN() \ + printf("\nDone. %d/%d passed in %u seconds\n", \ + __test_framework_state.run_count - __test_framework_state.fail_count, __test_framework_state.run_count, \ + (unsigned)(time(0) - __test_framework_state.time_start)) + +#define TEST_FRAMEWORK_PASSED() (__test_framework_state.fail_count == 0) + +#define TEST_SUITE_BEGIN() \ + __test_framework_state.current_suite = __func__; \ + printf("%s ", __test_framework_state.current_suite); \ + fflush(stdout) + +#define TEST_SUITE_END() __test_framework_state.current_suite = 0; \ + printf("\n"); \ + fflush(stdout) + +#define TEST_PUSH_CURRENT_LINE(func_name) \ + __test_framework_state.current_test_file_stack[__test_framework_state.current_test_stack_index] = __FILE__; \ + __test_framework_state.current_test_line_stack[__test_framework_state.current_test_stack_index] = __LINE__; \ + __test_framework_state.current_test_func_stack[__test_framework_state.current_test_stack_index] = func_name; \ + ++__test_framework_state.current_test_stack_index; + +#define TEST_POP_CURRENT_LINE() --__test_framework_state.current_test_stack_index; + +#define TEST_INIT() \ + __test_framework_state.current_test_stack_index = 0; \ + TEST_PUSH_CURRENT_LINE(__func__); \ + __test_framework_state.current_test_fail = 0; \ + ++__test_framework_state.run_count; \ + printf("."); \ + fflush(stdout); + +#define TEST(func) \ + __test_framework_state.current_test = #func; \ + TEST_INIT() \ + func(); + +#define TEST_PARAMS1(func, p1) \ + __test_framework_state.current_test = #func "(" #p1 ")"; \ + TEST_INIT() \ + func(p1); + +#define TEST_PARAMS2(func, p1, p2) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ")"; \ + TEST_INIT() \ + func(p1, p2); + +#define TEST_PARAMS3(func, p1, p2, p3) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ")"; \ + TEST_INIT() \ + func(p1, p2, p3); + +#define TEST_PARAMS4(func, p1, p2, p3, p4) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4); + +#define TEST_PARAMS5(func, p1, p2, p3, p4, p5) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5); + +#define TEST_PARAMS6(func, p1, p2, p3, p4, p5, p6) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6); + +#define TEST_PARAMS7(func, p1, p2, p3, p4, p5, p6, p7) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6, p7); + +#define TEST_PARAMS8(func, p1, p2, p3, p4, p5, p6, p7, p8) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ", " #p8 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6, p7, p8); + +#define TEST_PARAMS9(func, p1, p2, p3, p4, p5, p6, p7, p8, p9) \ + __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ", " #p8 ", " #p9 ")"; \ + TEST_INIT() \ + func(p1, p2, p3, p4, p5, p6, p7, p8, p9); + +#define ASSERT_HELPER(func_call, func_name) { \ + TEST_PUSH_CURRENT_LINE(func_name); \ + func_call; \ + TEST_POP_CURRENT_LINE(); \ + if (__test_framework_state.current_test_fail) \ + return; \ +} + +#define ASSERT_MESSAGE(message, ...) { \ + uint32_t __stack_index; \ + if (!__test_framework_state.current_test_fail) { \ + __test_framework_state.current_test_fail = 1; \ + ++__test_framework_state.fail_count; \ + fprintf(stderr, "\n* %s/%s (%s:%d)", __test_framework_state.current_suite, __test_framework_state.current_test, \ + test_framework_basename(__test_framework_state.current_test_file_stack[0]), __test_framework_state.current_test_line_stack[0]); \ + } \ + for (__stack_index = 1; __stack_index < __test_framework_state.current_test_stack_index; ++__stack_index) { \ + fprintf(stderr, "\n via %s (%s:%d)", \ + __test_framework_state.current_test_func_stack[__stack_index], \ + test_framework_basename(__test_framework_state.current_test_file_stack[__stack_index]), \ + __test_framework_state.current_test_line_stack[__stack_index]); \ + } \ + fprintf(stderr, "\n "); \ + fprintf(stderr, message "\n", ## __VA_ARGS__); \ + fflush(stderr); \ +} + +#define ASSERT_FAIL(message, ...) { ASSERT_MESSAGE(message, ## __VA_ARGS__); return; } + +#define ASSERT_COMPARE(value, compare, expected, type, format) { \ + type __v = (type)(value); \ + type __e = (type)(expected); \ + if (!(__v compare __e)) { \ + ASSERT_FAIL("Expected: " #value " " #compare " " #expected " (%s:%d)\n Found: " format " " #compare " " format, \ + test_framework_basename(__FILE__), __LINE__, __v, __e); \ + } \ +} + +#define ASSERT_NUM_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, int, "%d") +#define ASSERT_NUM_NOT_EQUALS(value, expected) ASSERT_COMPARE(value, !=, expected, int, "%d") +#define ASSERT_NUM_GREATER(value, expected) ASSERT_COMPARE(value, >, expected, int, "%d") +#define ASSERT_NUM_GREATER_EQUALS(value, expected) ASSERT_COMPARE(value, >=, expected, int, "%d") +#define ASSERT_NUM_LESS(value, expected) ASSERT_COMPARE(value, <, expected, int, "%d") +#define ASSERT_NUM_LESS_EQUALS(value, expected) ASSERT_COMPARE(value, <=, expected, int, "%d") + +#define ASSERT_FLOAT_EQUALS(value, expected) { \ + float __v = (float)value; \ + float __e = (float)expected; \ + double diff = (__v > __e) ? ((double)__v - (double)__e) : ((double)__e - (double)__v); \ + if (diff >= 0.0000002) { /* FLT_EPSILON is ~1.19e-7 */ \ + ASSERT_FAIL("Expected: " #value " = " #expected " (%s:%d)\n Found: %f = %f", \ + test_framework_basename(__FILE__), __LINE__, __v, __e); \ + } \ +} + +/* TODO: figure out some way to detect c89 so we can use int64_t and %lld on non-c89 builds */ +#define ASSERT_NUM64_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, int, "%d") + +#define ASSERT_TRUE(value) ASSERT_NUM_NOT_EQUALS(value, 0) +#define ASSERT_FALSE(value) ASSERT_NUM_EQUALS(value, 0) + +#define ASSERT_UNUM_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, unsigned, "%u") +#define ASSERT_DBL_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, double, "%g") +#define ASSERT_PTR_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, void*, "%p") +#define ASSERT_PTR_NOT_NULL(value) ASSERT_COMPARE(value, !=, NULL, void*, "%p") +#define ASSERT_PTR_NULL(value) ASSERT_COMPARE(value, ==, NULL, void*, "%p") + +#define ASSERT_STR_EQUALS(value, expected) { \ + const char* __v = (const char*)(value); \ + const char* __e = (const char*)(expected); \ + if (strcmp(__v, __e) != 0) { \ + ASSERT_FAIL( "String mismatch for: " #value " (%s:%d)\n Expected: %s\n Found: %s", test_framework_basename(__FILE__), __LINE__, __e, __v); \ + }} + +#define ASSERT_STR_NOT_EQUALS(value, expected) { \ + const char* __v = (const char*)(value); \ + const char* __e = (const char*)(expected); \ + if (strcmp(__v, __e) == 0) { \ + ASSERT_FAIL( "String match for: " #value " (%s:%d)\n Found, but not expected: %s", test_framework_basename(__FILE__), __LINE__, __v); \ + }} + +#endif /* TEST_FRAMEWORK_H */ diff --git a/src/rcheevos/test/test_rc_client.c b/src/rcheevos/test/test_rc_client.c new file mode 100644 index 0000000000..50f9a2c852 --- /dev/null +++ b/src/rcheevos/test/test_rc_client.c @@ -0,0 +1,10329 @@ +#include "rc_client.h" + +#include "rc_consoles.h" +#include "rc_internal.h" +#include "rc_api_runtime.h" + +#include "../src/rc_client_internal.h" +#include "../src/rc_client_external.h" +#include "../src/rc_version.h" + +#include "test_framework.h" + +#ifdef RC_CLIENT_SUPPORTS_HASH +#include "rc_hash.h" +#include "rhash/data.h" +#include "rhash/mock_filereader.h" +#endif + +#if defined(_WIN32) +#include +#elif defined(__unix__) && __STDC_VERSION__ >= 199309L +#include +#else +#define RC_NO_SLEEP +#endif + +static rc_client_t* g_client; +static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ + +#define GENERIC_ACHIEVEMENT_JSON(id, memaddr) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ + "\"Description\":\"Desc " id "\",\"Flags\":3,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ + "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" + +#define UNOFFICIAL_ACHIEVEMENT_JSON(id, memaddr) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ + "\"Description\":\"Desc " id "\",\"Flags\":5,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ + "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" + +#define TYPED_ACHIEVEMENT_JSON(id, memaddr, type, rarity, rarity_hardcore) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ + "\"Description\":\"Desc " id "\",\"Flags\":3,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ + "\"Type\":\"" type "\",\"Rarity\":" rarity ",\"RarityHardcore\":" rarity_hardcore "," \ + "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" + +#define GENERIC_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\"}" + +static const char* patchdata_empty = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]" + "}]}"; + +static const char* patchdata_2ach_0lbd = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}" + "]," + "\"Leaderboards\":[]" + "}]}"; + +static const char* patchdata_2ach_1lbd = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}" + "]," + "\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" + "]" + "}]}"; + +static const char* patchdata_rich_presence_only = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]" + "}]}"; + +static const char* patchdata_leaderboard_only = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}]}"; + +static const char* patchdata_leaderboard_immediate_submit = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0=1::SUB:1=1::VAL:0x 000E", "SCORE") + "]" + "}]}"; + +static const char* patchdata_bounds_check_system = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":7," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("1", "0xH0000=5") "," + GENERIC_ACHIEVEMENT_JSON("2", "0xHFFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("3", "0xH10000=5") "," + GENERIC_ACHIEVEMENT_JSON("4", "0x FFFE=5") "," + GENERIC_ACHIEVEMENT_JSON("5", "0x FFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "0x 10000=5") "," + GENERIC_ACHIEVEMENT_JSON("7", "I:0xH0000_0xHFFFF=5") + "]," + "\"Leaderboards\":[]" + "}]}"; + +static const char* patchdata_bounds_check_8 = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":7," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("408", "0xH0004=5") "," + GENERIC_ACHIEVEMENT_JSON("508", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("608", "0xH0006=5") "," + GENERIC_ACHIEVEMENT_JSON("708", "0xH0007=5") "," + GENERIC_ACHIEVEMENT_JSON("808", "0xH0008=5") "," + GENERIC_ACHIEVEMENT_JSON("416", "0x 0004=5") "," + GENERIC_ACHIEVEMENT_JSON("516", "0x 0005=5") "," + GENERIC_ACHIEVEMENT_JSON("616", "0x 0006=5") "," + GENERIC_ACHIEVEMENT_JSON("716", "0x 0007=5") "," + GENERIC_ACHIEVEMENT_JSON("816", "0x 0008=5") "," + GENERIC_ACHIEVEMENT_JSON("424", "0xW0004=5") "," + GENERIC_ACHIEVEMENT_JSON("524", "0xW0005=5") "," + GENERIC_ACHIEVEMENT_JSON("624", "0xW0006=5") "," + GENERIC_ACHIEVEMENT_JSON("724", "0xW0007=5") "," + GENERIC_ACHIEVEMENT_JSON("824", "0xW0008=5") "," + GENERIC_ACHIEVEMENT_JSON("432", "0xX0004=5") "," + GENERIC_ACHIEVEMENT_JSON("532", "0xX0005=5") "," + GENERIC_ACHIEVEMENT_JSON("632", "0xX0006=5") "," + GENERIC_ACHIEVEMENT_JSON("732", "0xX0007=5") "," + GENERIC_ACHIEVEMENT_JSON("832", "0xX0008=5") + "]," + "\"Leaderboards\":[]" + "}]}"; + +static const char* patchdata_exhaustive = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," + GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," + GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," + GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," + GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ + GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ + GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ + GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ + "]" + "}]}"; + + +static const char* patchdata_exhaustive_typed = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + TYPED_ACHIEVEMENT_JSON("5", "0xH0005=5", "", "100.0", "99.5") "," + TYPED_ACHIEVEMENT_JSON("6", "M:0xH0006=6", "progression", "95.3", "84.7") "," + TYPED_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1", "missable", "47.6", "38.2") "," + TYPED_ACHIEVEMENT_JSON("8", "0xH0008=8", "progression", "86.0", "73.1") "," + TYPED_ACHIEVEMENT_JSON("9", "0xH0009=9", "win_condition", "81.4", "66.4") "," + TYPED_ACHIEVEMENT_JSON("70", "M:0xX0010=100000", "missable", "11.4", "6.3") "," + TYPED_ACHIEVEMENT_JSON("71", "G:0xX0010=100000", "", "8.7", "3.8") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ + GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ + GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ + GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ + "]" + "}]}"; + +static const char* patchdata_big_ids = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," + GENERIC_ACHIEVEMENT_JSON("4294967295", "0xH0009=9") "," /* UINT_MAX */ + GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") + "]," + "\"Leaderboards\":[]" + "}]}"; + +#define HIDDEN_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"Hidden\":true}" + +static const char* patchdata_leaderboards_hidden = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + HIDDEN_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," + HIDDEN_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}]}"; + +static const char* patchdata_unofficial_unsupported = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=1_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xHFEFEFEFE=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," + "\"Created\":1376971283,\"Modified\":1376971283}" + "]," + "\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xHFEFEFEFE=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" + "]" + "}]}"; + +static const char* patchdata_subset = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," + GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," + GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," + GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," + GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ + GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ + GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ + GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ + "]" + "},{" + "\"AchievementSetId\":2345,\"GameId\":1235,\"Title\":\"Bonus\",\"Type\":\"bonus\"," + "\"ImageIconUrl\":\"http://server/Images/112234.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5501", "0xH0017=7") "," + GENERIC_ACHIEVEMENT_JSON("5502", "0xH0018=8") "," + GENERIC_ACHIEVEMENT_JSON("5503", "0xH0019=9") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("81", "STA:0xH0008=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("82", "STA:0xH0008=2::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}]}"; + +static const char* patchdata_subset_unofficial_unsupported = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," + GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," + UNOFFICIAL_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," + GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," + GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ + GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ + GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ + GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ + "]" + "},{" + "\"AchievementSetId\":2345,\"GameId\":1235,\"Title\":\"Bonus\",\"Type\":\"bonus\"," + "\"ImageIconUrl\":\"http://server/Images/112234.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5501", "0xH0017=7") "," + UNOFFICIAL_ACHIEVEMENT_JSON("5502", "0xH0018=8") "," + GENERIC_ACHIEVEMENT_JSON("5503", "0xHFEFEFEFE=9") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("81", "STA:0xH0008=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("82", "STA:0xH0008=2::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}]}"; + +static const char* patchdata_not_found = "{\"Success\":false,\"Error\":\"Unknown game\",\"Code\":\"not_found\",\"Status\":404}"; + +static const char* no_unlocks = "{\"Success\":true,\"Unlocks\":[],\"HardcoreUnlocks\":[]}"; + +/* startsession API only returns HardcoreUnlocks if an achievement has been earned in hardcore, + * even if the softcore unlock has a different timestamp */ +static const char* unlock_5501h_and_5502 = "{\"Success\":true,\"Unlocks\":[" + "{\"ID\":5502,\"When\":1234567899}" + "],\"HardcoreUnlocks\":[" + "{\"ID\":5501,\"When\":1234567890}" + "]}"; +static const char* unlock_5501_and_5502 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5501,\"When\":1234567890}," + "{\"ID\":5502,\"When\":1234567899}" + "]}"; +static const char* unlock_5501_5502_and_5503 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5501,\"When\":1234567890}," + "{\"ID\":5502,\"When\":1234567899}," + "{\"ID\":5503,\"When\":1234567999}" + "]}"; +static const char* unlock_8 = "{\"Success\":true,\"HardcoreUnlocks\":[{\"ID\":8,\"When\":1234567890}]}"; +static const char* unlock_8_and_5502 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":8,\"When\":1234567890}," + "{\"ID\":5502,\"When\":1234567890}" + "]}"; +static const char* unlock_6_8h_and_9 = "{\"Success\":true,\"Unlocks\":[" + "{\"ID\":6,\"When\":1234567890}," + "{\"ID\":9,\"When\":1234567899}" + "],\"HardcoreUnlocks\":[" + "{\"ID\":8,\"When\":1234567895}" + "]}"; + +static const char* response_429 = + "\n" + "429 Too Many Requests\n" + "\n" + "

429 Too Many Requests

\n" + "
nginx
\n" + "\n" + ""; + +static const char* response_502 = + "\n" + "502 Bad Gateway\n" + "\n" + "

502 Bad Gateway

\n" + "
nginx
\n" + "\n" + ""; + +static const char* response_503 = + "\n" + "503 Service Temporarily Unavailable\n" + "\n" + "

503 Service Temporarily Unavailable

\n" + "
nginx
\n" + "\n" + ""; + +static const char* default_game_badge = "https://media.retroachievements.org/Images/000001.png"; + +/* ----- helpers ----- */ + +static void _assert_achievement_state(rc_client_t* client, uint32_t id, int expected_state) +{ + const rc_client_achievement_t* achievement = rc_client_get_achievement_info(client, id); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, expected_state); +} +#define assert_achievement_state(client, id, expected_state) ASSERT_HELPER(_assert_achievement_state(client, id, expected_state), "assert_achievement_state") + +typedef struct rc_client_captured_event_t +{ + rc_client_event_t event; + rc_client_server_error_t server_error; /* server_error goes out of scope, it needs to be copied too */ + uint32_t id; +} rc_client_captured_event_t; + +static rc_client_captured_event_t events[16]; +static int event_count = 0; + +static void rc_client_event_handler(const rc_client_event_t* e, rc_client_t* client) +{ + memcpy(&events[event_count], e, sizeof(rc_client_event_t)); + memset(&events[event_count].server_error, 0, sizeof(events[event_count].server_error)); + + switch (e->type) { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + events[event_count].id = e->achievement->id; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + events[event_count].id = e->leaderboard->id; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: { + /* scoreboard is not maintained out of scope, copy it */ + static rc_client_leaderboard_scoreboard_t scoreboard; + static rc_client_leaderboard_scoreboard_entry_t scoreboard_entries[2]; + static char scoreboard_top_usernames[2][128]; + uint32_t i; + + /* cap the entries at 2, none of the mocked responses have anything larger */ + memcpy(&scoreboard, e->leaderboard_scoreboard, sizeof(scoreboard)); + scoreboard.num_top_entries = (scoreboard.num_top_entries > 2) ? 2 : scoreboard.num_top_entries; + memcpy(scoreboard_entries, e->leaderboard_scoreboard->top_entries, scoreboard.num_top_entries * sizeof(scoreboard_entries[0])); + for (i = 0; i < scoreboard.num_top_entries; i++) { + strcpy_s(scoreboard_top_usernames[i], sizeof(scoreboard_top_usernames[i]), scoreboard_entries[i].username); + scoreboard_entries[i].username = scoreboard_top_usernames[i]; + } + scoreboard.top_entries = scoreboard_entries; + + events[event_count].id = e->leaderboard_scoreboard->leaderboard_id; + events[event_count].event.leaderboard_scoreboard = &scoreboard; + break; + } + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + events[event_count].id = e->leaderboard_tracker->id; + break; + + case RC_CLIENT_EVENT_GAME_COMPLETED: + events[event_count].id = rc_client_get_game_info(client)->id; + break; + + case RC_CLIENT_EVENT_SERVER_ERROR: { + static char event_server_error_message[128]; + + /* server error data is not maintained out of scope, copy it */ + memcpy(&events[event_count].server_error, e->server_error, sizeof(events[event_count].server_error)); + strcpy_s(event_server_error_message, sizeof(event_server_error_message), e->server_error->error_message); + events[event_count].server_error.error_message = event_server_error_message; + events[event_count].event.server_error = &events[event_count].server_error; + events[event_count].id = 0; + break; + } + + case RC_CLIENT_EVENT_SUBSET_COMPLETED: + events[event_count].id = e->subset->id; + break; + + default: + events[event_count].id = 0; + break; + } + + ++event_count; +} + +static rc_client_event_t* find_event(uint8_t type, uint32_t id) +{ + int i; + + for (i = 0; i < event_count; ++i) { + if (events[i].id == id && events[i].event.type == type) + return &events[i].event; + } + + return NULL; +} + +static uint8_t* g_memory = NULL; +static uint32_t g_memory_size = 0; + +static void mock_memory(uint8_t* memory, uint32_t size) +{ + g_memory = memory; + g_memory_size = size; +} + +static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + if (g_memory_size > 0) { + if (address >= g_memory_size) + return 0; + + uint32_t num_avail = g_memory_size - address; + if (num_avail < num_bytes) + num_bytes = num_avail; + + memcpy(buffer, &g_memory[address], num_bytes); + return num_bytes; + } + + memset(&buffer, 0, num_bytes); + return num_bytes; +} + +static rc_clock_t g_now; + +static rc_clock_t rc_client_get_now_millisecs(const rc_client_t* client) +{ + return g_now; +} + +/* ----- API mocking ----- */ + +typedef struct rc_mock_api_response +{ + const char* request_params; + rc_api_server_response_t server_response; + int seen; + rc_client_server_callback_t async_callback; + void* async_callback_data; +} rc_mock_api_response; + +static rc_mock_api_response g_mock_api_responses[12]; +static int g_num_mock_api_responses = 0; + +void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + rc_api_server_response_t server_response; + + int i; + for (i = 0; i < g_num_mock_api_responses; i++) { + if (strcmp(g_mock_api_responses[i].request_params, request->post_data) == 0) { + g_mock_api_responses[i].seen++; + callback(&g_mock_api_responses[i].server_response, callback_data); + return; + } + } + + ASSERT_FAIL("No API response for: %s", request->post_data); + + /* still call the callback to prevent memory leak */ + memset(&server_response, 0, sizeof(server_response)); + server_response.body = ""; + server_response.http_status_code = 500; + callback(&server_response, callback_data); +} + +void rc_client_server_call_async(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = strdup(request->post_data); + g_mock_api_responses[g_num_mock_api_responses].async_callback = callback; + g_mock_api_responses[g_num_mock_api_responses].async_callback_data = callback_data; + g_mock_api_responses[g_num_mock_api_responses].seen = -1; + g_num_mock_api_responses++; +} + +static void _async_api_response(const char* request_params, const char* response_body, int http_status_code) +{ + int i; + for (i = 0; i < g_num_mock_api_responses; i++) + { + if (g_mock_api_responses[i].request_params && strcmp(g_mock_api_responses[i].request_params, request_params) == 0) + { + /* request_params are set to NULL, so we can't find the requests to get a count anyway */ + g_mock_api_responses[i].seen++; + g_mock_api_responses[i].server_response.body = response_body; + g_mock_api_responses[i].server_response.body_length = strlen(response_body); + g_mock_api_responses[i].server_response.http_status_code = http_status_code; + g_mock_api_responses[i].async_callback(&g_mock_api_responses[i].server_response, g_mock_api_responses[i].async_callback_data); + free((void*)g_mock_api_responses[i].request_params); + g_mock_api_responses[i].request_params = NULL; + + while (g_num_mock_api_responses > 0 && g_mock_api_responses[g_num_mock_api_responses - 1].request_params == NULL) + --g_num_mock_api_responses; + return; + } + } + + ASSERT_FAIL("No pending API request for: %s", request_params); +} + +void async_api_response(const char* request_params, const char* response_body) +{ + _async_api_response(request_params, response_body, 200); +} + +void async_api_error(const char* request_params, const char* response_body, int http_status_code) +{ + _async_api_response(request_params, response_body, http_status_code); +} + +static void _assert_api_called(const char* request_params, int count) +{ + int i; + for (i = 0; i < g_num_mock_api_responses; i++) { + if (g_mock_api_responses[i].request_params && + strcmp(g_mock_api_responses[i].request_params, request_params) == 0) { + ASSERT_NUM_EQUALS(g_mock_api_responses[i].seen, count); + return; + } + } + + ASSERT_NUM_EQUALS(0, count); +} +#define assert_api_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 1), "assert_api_called") +#define assert_api_not_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_called") +#define assert_api_call_count(request_params, num) ASSERT_HELPER(_assert_api_called(request_params, num), "assert_api_call_count") +#define assert_api_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, -1), "assert_api_pending") +#define assert_api_not_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_pending") + +void reset_mock_api_handlers(void) +{ + g_num_mock_api_responses = 0; + memset(g_mock_api_responses, 0, sizeof(g_mock_api_responses)); +} + +void mock_api_response(const char* request_params, const char* response_body) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; + g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; + g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); + g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = 200; + g_num_mock_api_responses++; +} + +void mock_api_error(const char* request_params, const char* response_body, int http_status_code) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; + g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; + g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); + g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = http_status_code; + g_num_mock_api_responses++; +} + +static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_timeout(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); + ASSERT_STR_EQUALS(error_message, "Request has timed out."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_no_internet(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); + ASSERT_STR_EQUALS(error_message, "Internet not available."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_no_game_loaded(int result, const char* error_message, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); + ASSERT_STR_EQUALS(error_message, "No game loaded"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called."); +} + +static rc_client_t* mock_client_not_logged_in(void) +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + rc_client_set_event_handler(client, rc_client_event_handler); + rc_client_set_get_time_millisecs_function(client, rc_client_get_now_millisecs); + + mock_memory(NULL, 0); + rc_api_set_host(NULL); + reset_mock_api_handlers(); + g_now = 100000; + + return client; +} + +static rc_client_t* mock_client_not_logged_in_async(void) +{ + rc_client_t* client = mock_client_not_logged_in(); + client->callbacks.server_call = rc_client_server_call_async; + return client; +} + +static rc_client_t* mock_client_logged_in(void) +{ + rc_client_t* client = mock_client_not_logged_in(); + client->user.username = "Username"; + client->user.display_name = "DisplayName"; + client->user.token = "ApiToken"; + client->user.score = 12345; + client->user.avatar_url = "http://server/UserPic/Username.png"; + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + + return client; +} + +static rc_client_t* mock_client_logged_in_async(void) +{ + rc_client_t* client = mock_client_logged_in(); + client->callbacks.server_call = rc_client_server_call_async; + return client; +} + +static void mock_client_load_game(const char* patchdata, const char* unlocks) +{ + reset_mock_api_handlers(); + event_count = 0; + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlocks); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + if (!g_client->game) + ASSERT_MESSAGE("client->game is NULL"); +} + +static void mock_client_load_game_softcore(const char* patchdata, const char* unlocks) +{ + reset_mock_api_handlers(); + event_count = 0; + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=0&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlocks); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + if (!g_client->game) + ASSERT_MESSAGE("client->game is NULL"); +} + +static rc_client_t* mock_client_game_loaded(const char* patchdata, const char* unlocks) +{ + g_client = mock_client_logged_in(); + + mock_client_load_game(patchdata, unlocks); + + return g_client; +} + +/* ----- login ----- */ + +static void test_login_with_password(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void test_login_with_token(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login2&u=User&t=ApiToken", + "{\"Success\":true,\"User\":\"User\",\"AvatarUrl\":\"http://server/UserPic/USER.png\",\"Token\":\"ApiToken\",\"Score\":12345,\"Messages\":2}"); + + rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_STR_EQUALS(user->avatar_url, "http://server/UserPic/USER.png"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_username_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "username is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_password_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "password is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_token_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "token is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_required_fields(void) +{ + g_client = mock_client_not_logged_in(); + + rc_client_begin_login_with_password(g_client, "User", "", rc_client_callback_expect_password_required, g_callback_userdata); + rc_client_begin_login_with_password(g_client, "", "Pa$$word", rc_client_callback_expect_username_required, g_callback_userdata); + rc_client_begin_login_with_password(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); + + rc_client_begin_login_with_token(g_client, "User", "", rc_client_callback_expect_token_required, g_callback_userdata); + rc_client_begin_login_with_token(g_client, "", "ApiToken", rc_client_callback_expect_username_required, g_callback_userdata); + rc_client_begin_login_with_token(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); + + ASSERT_NUM_EQUALS(g_client->state.user, RC_CLIENT_USER_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_credentials_error(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_CREDENTIALS); + ASSERT_STR_EQUALS(error_message, "Invalid User/Password combination. Please try again"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_with_incorrect_password(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_error("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"," + "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_token_error(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_CREDENTIALS); + ASSERT_STR_EQUALS(error_message, "Invalid User/Token combination."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_with_incorrect_token(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_error("r=login2&u=User&t=TOKEN", + "{\"Success\":false,\"Error\":\"Invalid User/Token combination.\"," + "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); + + rc_client_begin_login_with_token(g_client, "User", "TOKEN", rc_client_callback_expect_token_error, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_expired_token(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_EXPIRED_TOKEN); + ASSERT_STR_EQUALS(error_message, "The access token has expired. Please log in again."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_with_expired_token(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_error("r=login2&u=User&t=EXPIRED", + "{\"Success\":false,\"Error\":\"The access token has expired. Please log in again.\"," + "\"Status\":401,\"Code\":\"expired_token\"}", 403); + + rc_client_begin_login_with_token(g_client, "User", "EXPIRED", rc_client_callback_expect_expired_token, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_access_denied(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ACCESS_DENIED); + ASSERT_STR_EQUALS(error_message, "Access denied."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_with_banned_account(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_error("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":false,\"Error\":\"Access denied.\"," + "\"Status\":403,\"Code\":\"access_denied\"}", 403); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_access_denied, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_missing_token(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_STR_EQUALS(error_message, "Token not found in response"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_incomplete_response(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login2&u=User&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\"}"); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_missing_token, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + async_api_response("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async_aborted(void) +{ + const rc_client_user_t* user; + rc_client_async_handle_t* handle; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", + rc_client_callback_expect_uncalled, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async_destroyed(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", + rc_client_callback_expect_uncalled, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_destroy(g_client); + + async_api_response("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); +} + +static void test_login_with_password_client_error(void) +{ + const rc_client_user_t* user; + rc_client_async_handle_t* handle; + + g_client = mock_client_not_logged_in(); + + mock_api_error("r=login2&u=User&p=Pa%24%24word", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", + rc_client_callback_expect_no_internet, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + ASSERT_PTR_NULL(handle); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_login_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); + ASSERT_STR_EQUALS(error_message, "Login required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_logged_in(); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + + rc_client_logout(g_client); + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + /* reference pointer should be NULLed out */ + ASSERT_PTR_NULL(user->display_name); + ASSERT_PTR_NULL(user->username); + ASSERT_PTR_NULL(user->token); + + /* attempt to load game should fail */ + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_logout_with_game_loaded(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); + + rc_client_logout(g_client); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_login_aborted(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "Login aborted"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout_during_login(void) +{ + g_client = mock_client_not_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_login_aborted, g_callback_userdata); + rc_client_logout(g_client); + + async_api_response("r=login2&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_no_longer_active(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "The requested game is no longer active"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout_during_fetch_game(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_no_longer_active, g_callback_userdata); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + + rc_client_logout(g_client); + + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void test_user_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + ASSERT_STR_EQUALS(g_client->user.avatar_url, "http://server/UserPic/Username.png"); + + ASSERT_NUM_EQUALS(rc_client_user_get_image_url(rc_client_get_user_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "http://server/UserPic/Username.png"); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive, unlock_6_8h_and_9); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_softcore(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive, unlock_6_8h_and_9); + rc_client_set_hardcore_enabled(g_client, 0); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 3); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 15); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_encore_mode(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_exhaustive); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlock_6_8h_and_9); + + rc_client_set_encore_mode_enabled(g_client, 1); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_with_unsupported_and_unofficial(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 7); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_with_unsupported_unlocks(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, unlock_5501_5502_and_5503); + + /* unlocked unsupported achievement should be counted in both unlocked and unsuppored buckets */ + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 2); + + ASSERT_NUM_EQUALS(summary.points_core, 7); + ASSERT_NUM_EQUALS(summary.points_unlocked, 7); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 1234567999); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_with_unofficial_off(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 0); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + /* unofficial achievements are not copied from the patch data to the runtime if unofficial is off */ + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 7); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_no_achievements(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_empty, no_unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 0); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_unknown_game(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); + ASSERT_STR_EQUALS(error_message, "Unknown game"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_get_user_game_summary_unknown_game(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 0); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_progress_incomplete(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive_typed, unlock_8); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_progress_progression_no_win(void) +{ + rc_client_user_game_summary_t summary; + /* 6 and 8 are progression, 9 is win condition */ + const char* unlock_5_6_and_8 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5,\"When\":1234568890}," + "{\"ID\":6,\"When\":1234567999}," + "{\"ID\":8,\"When\":1234567895}" + "]}"; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive_typed, unlock_5_6_and_8); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 3); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 15); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_progress_win_only(void) +{ + rc_client_user_game_summary_t summary; + /* 6 and 8 are progression, 9 is win condition */ + const char* unlock_5_and_9 = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5,\"When\":1234568890}," + "{\"ID\":9,\"When\":1234567999}" + "]}"; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive_typed, unlock_5_and_9); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 2); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 10); + + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_beat(void) +{ + rc_client_user_game_summary_t summary; + /* 6 and 8 are progression, 9 is win condition */ + const char* unlocks = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5,\"When\":1234568890}," + "{\"ID\":6,\"When\":1234567999}," + "{\"ID\":8,\"When\":1234567890}," + "{\"ID\":9,\"When\":1234568765}" + "]}"; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive_typed, unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 4); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 20); + + ASSERT_NUM_EQUALS(summary.beaten_time, 1234568765); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_mastery(void) +{ + rc_client_user_game_summary_t summary; + const char* unlocks = "{\"Success\":true,\"HardcoreUnlocks\":[" + "{\"ID\":5,\"When\":1234568890}," + "{\"ID\":6,\"When\":1234567999}," + "{\"ID\":7,\"When\":1234569123}," + "{\"ID\":8,\"When\":1234567890}," + "{\"ID\":9,\"When\":1234568765}," + "{\"ID\":70,\"When\":1234568901}," + "{\"ID\":71,\"When\":1234566789}" + "]}"; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive_typed, unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 7); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 35); + + ASSERT_NUM_EQUALS(summary.beaten_time, 1234568765); + ASSERT_NUM_EQUALS(summary.completed_time, 1234569123); + + rc_client_destroy(g_client); +} + +/* ----- load game ----- */ + +static void rc_client_callback_expect_hash_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "hash is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_required_fields(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_load_game(g_client, NULL, rc_client_callback_expect_hash_required, g_callback_userdata); + rc_client_begin_load_game(g_client, "", rc_client_callback_expect_hash_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_unknown_hash(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 0); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.id, 0); + ASSERT_STR_EQUALS(g_client->game->subsets->public_.title, ""); + ASSERT_NUM_EQUALS(g_client->game->subsets->active, 0); + } + rc_client_destroy(g_client); +} + +static void test_load_game_unknown_hash_repeated(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* first request should resolve the hash asynchronously */ + handle = rc_client_begin_load_game(g_client, + "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NOT_NULL(handle); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + + /* second request should use the hash cache and not need an asynchronous call */ + handle = rc_client_begin_load_game(g_client, + "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NULL(handle); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + + rc_client_destroy(g_client); +} + +static void test_load_game_unknown_hash_multiple(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* first request */ + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NOT_NULL(handle); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + + /* second request */ + handle = rc_client_begin_load_game(g_client, "FEDCBA9876543210", + rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NOT_NULL(handle); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=FEDCBA9876543210", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "FEDCBA9876543210"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + + rc_client_destroy(g_client); +} + +static void test_load_game_not_logged_in(void) +{ + g_client = mock_client_not_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 1); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, "http://server/Images/112233.png"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); + ASSERT_STR_EQUALS(achievement->public_.badge_url, "https://media.retroachievements.org/Badge/00234.png"); + ASSERT_STR_EQUALS(achievement->public_.badge_locked_url, "https://media.retroachievements.org/Badge/00234_lock.png"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); + ASSERT_STR_EQUALS(achievement->public_.badge_url, "https://media.retroachievements.org/Badge/00235.png"); + ASSERT_STR_EQUALS(achievement->public_.badge_locked_url, "https://media.retroachievements.org/Badge/00235_lock.png"); + ASSERT_NUM_EQUALS(achievement->public_.points, 2); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_load_different_game(void) +{ + static const char* patchdata_alternate = "{\"Success\":true," + "\"GameId\":2345,\"Title\":\"Other Game\",\"ConsoleId\":7," + "\"ImageIconUrl\":\"http://server/Images/555555.png\"," + "\"RichPresenceGameId\":2345,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":2222,\"GameId\":2345,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/555555.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("1", "0xH0000=5") "," + GENERIC_ACHIEVEMENT_JSON("2", "0xHFFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("3", "0xH10000=5") + "]," + "\"Leaderboards\":[]" + "}]}"; + + g_client = mock_client_logged_in_async(); + reset_mock_api_handlers(); + + /* start loading first game */ + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_no_longer_active, g_callback_userdata); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); + + /* receive data for first game, start session for first game */ + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); + + /* start loading second game*/ + rc_client_begin_load_game(g_client, "ABCDEF0123456789", rc_client_callback_expect_success, g_callback_userdata); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789"); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); + + /* session started for first game, should abort */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); + + /* receive data for second game, start session for second game */ + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789", patchdata_alternate); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); + + /* session started for second game, should succeed */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=2345&h=1&m=ABCDEF0123456789&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + + /* verify second game was loaded */ + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 2345); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 7); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Other Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "ABCDEF0123456789"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "555555"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 3); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN); + + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + /* login completion will trigger process to continue */ + async_api_response("r=login2&u=Username&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); + + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + + ASSERT_STR_EQUALS(g_client->user.username, "Username"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login_with_incorrect_password(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN); + + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + /* login failure will trigger process to continue */ + async_api_error("r=login2&u=Username&p=Pa%24%24word", + "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"," + "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + ASSERT_PTR_NULL(g_client->user.username); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login_logout(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_login_aborted, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_aborted, g_callback_userdata); + + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + /* logout will cancel login and allow game load to proceed with failure */ + rc_client_logout(g_client); + async_api_response("r=login2&u=Username&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + ASSERT_PTR_NULL(g_client->user.username); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + handle = rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_uncalled, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_aborted, g_callback_userdata); + + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + /* login abort will trigger game load process to continue */ + rc_client_abort_async(g_client, handle); + async_api_response("r=login2&u=Username&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + + ASSERT_PTR_NULL(g_client->user.username); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_too_many_requests(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + ASSERT_STR_EQUALS(error_message, "429 Too Many Requests"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_patch_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_error("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", response_429, 429); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_startsession_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, response_429, 429); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_startsession_timeout(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "", 504); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_timeout, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_startsession_custom_timeout(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, + "Request has timed out.", RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_timeout, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_patch_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + assert_api_not_called("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_startsession_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void test_load_game_while_spectating(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + rc_client_set_spectator_mode_enabled(g_client, 1); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + /* spectator mode should not start a session or fetch unlocks */ + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public_.points, 2); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + /* spectator mode cannot be disabled if it was enabled before loading the game */ + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + rc_client_unload_game(g_client); + + /* spectator mode can be disabled after unloading game */ + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + + rc_client_destroy(g_client); +} + +static int rc_client_callback_process_game_sets_called = 0; +static void rc_client_callback_process_game_sets(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_sets_response_t* game_sets_response, rc_client_t* client, void* userdata) +{ + ASSERT_STR_EQUALS(server_response->body, patchdata_2ach_1lbd); + ASSERT_NUM_EQUALS(game_sets_response->id, 1234); + ASSERT_NUM_EQUALS(game_sets_response->num_sets, 1); + ASSERT_NUM_EQUALS(game_sets_response->sets[0].num_achievements, 2); + ASSERT_NUM_EQUALS(game_sets_response->sets[0].num_leaderboards, 1); + rc_client_callback_process_game_sets_called = 1; +} + +static void test_load_game_process_game_sets(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + g_client->callbacks.post_process_game_sets_response = rc_client_callback_process_game_sets; + rc_client_callback_process_game_sets_called = 0; + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public_.points, 2); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + + ASSERT_NUM_NOT_EQUALS(rc_client_callback_process_game_sets_called, 0); + + rc_client_destroy(g_client); +} + +static void test_load_game_destroy_while_fetching_game_data(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_destroy(g_client); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); +} + +static void test_load_unknown_game(void) +{ + const char* hash = "0123456789ABCDEFFEDCBA9876543210"; + g_client = mock_client_logged_in(); + + rc_client_load_unknown_game(g_client, hash); + + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); + + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, hash); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 0); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.id, 0); + ASSERT_STR_EQUALS(g_client->game->subsets->public_.title, ""); + ASSERT_NUM_EQUALS(g_client->game->subsets->active, 0); + + rc_client_destroy(g_client); +} + +static void test_load_unknown_game_multihash(void) +{ + const char* hash = "0123456789ABCDEFFEDCBA9876543210,FEDCBA98765432100123456789ABCDEF"; + g_client = mock_client_logged_in(); + + rc_client_load_unknown_game(g_client, hash); + + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); + ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); + + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, hash); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 0); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.id, 0); + ASSERT_STR_EQUALS(g_client->game->subsets->public_.title, ""); + ASSERT_NUM_EQUALS(g_client->game->subsets->active, 0); + + rc_client_destroy(g_client); +} + +static void test_load_game_dispatched_read_memory(void) +{ + g_client = mock_client_logged_in(); + rc_client_set_allow_background_memory_reads(g_client, 0); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); + ASSERT_PTR_NOT_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_idle(g_client); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); +} + +/* ----- unload game ----- */ + +static void test_unload_game(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_achievement_info(g_client, 5501)); + ASSERT_PTR_NOT_NULL(rc_client_get_leaderboard_info(g_client, 4401)); + + event_count = 0; + rc_client_unload_game(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + ASSERT_PTR_NULL(g_client->game); + ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NULL(rc_client_get_achievement_info(g_client, 5501)); + ASSERT_PTR_NULL(rc_client_get_leaderboard_info(g_client, 4401)); + + rc_client_destroy(g_client); +} + +static void test_unload_game_hides_ui(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x01] = 1; /* show indicator */ + memory[0x06] = 3; /* progress tracker */ + memory[0x0B] = 1; /* start leaderboard */ + memory[0x0E] = 17; /* leaderboard value */ + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + rc_client_unload_game(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + rc_client_destroy(g_client); + ASSERT_NUM_EQUALS(event_count, 0); +} + +static void test_unload_game_while_fetching_game_data(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_unload_game(g_client); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_unload_game_while_starting_session(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + + rc_client_unload_game(g_client); + + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +#ifdef RC_CLIENT_SUPPORTS_HASH + +/* ----- identify and load game ----- */ + +static void rc_client_callback_expect_data_or_file_path_required(int result, const char* error_message, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "either data or file_path is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static void test_identify_and_load_game_required_fields(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, NULL, NULL, 0, + rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_identify_and_load_game_console_specified(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_console_not_specified(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +#ifndef RC_HASH_NO_ROM +uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size); + +static void test_identify_and_load_game_multiconsole_first(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* first hash lookup should be pending. inject a secondary console into the iterator */ + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 1); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ + iterator->consoles[iterator->index + 1] = 0; + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + assert_api_not_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* actual console ID returned from server */ + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multiconsole_second(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* first hash lookup should be pending. inject a secondary console into the iterator */ + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 1); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ + iterator->consoles[iterator->index + 1] = 0; + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e" ,patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=64b131c5c7fec32985d9c99700babb7e&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* actual console ID returned from server */ + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "64b131c5c7fec32985d9c99700babb7e"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +#endif /* RC_HASH_NO_ROM */ + +#ifndef RC_HASH_NO_DISC + +static void test_identify_and_load_game_from_disc(void) +{ + rc_hash_callbacks_t hash_callbacks; + + size_t image_size; + uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); + const char cue_single_track[] = + "FILE \"game.bin\" BINARY\n" + " TRACK 01 MODE2/2352\n" + " INDEX 01 00:00:00\n"; + + g_client = mock_client_logged_in(); + + memset(&hash_callbacks, 0, sizeof(hash_callbacks)); + get_mock_filereader(&hash_callbacks.filereader); + rc_client_set_hash_callbacks(g_client, &hash_callbacks); + + mock_file(0, "game.bin", image, image_size); + mock_file(1, "game.cue", (uint8_t*)cue_single_track, sizeof(cue_single_track)); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=db433fb038cde4fb15c144e8c7dea6e3", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=db433fb038cde4fb15c144e8c7dea6e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_PLAYSTATION, "game.cue", + NULL, 0, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "db433fb038cde4fb15c144e8c7dea6e3"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +#endif + +static void test_identify_and_load_game_unknown_hash(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_unknown_hash_repeated(void) +{ + rc_client_async_handle_t* handle; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* first request should resolve the hash asynchronously */ + handle = rc_client_begin_identify_and_load_game(g_client, + RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NOT_NULL(handle); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + + /* second request should use the hash cache and not need an asynchronous call */ + handle = rc_client_begin_identify_and_load_game(g_client, + RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NULL(handle); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_unknown_hash_multiple(void) +{ + rc_client_async_handle_t* handle; + + const size_t disk_size = 256 * 16 * 35; /* 140KB - Apple II disk size */ + uint8_t* disk = generate_generic_file(disk_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* first request */ + handle = rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_APPLE_II, + "disk1.dsk", disk, disk_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NOT_NULL(handle); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=88be638f4d78b4072109e55f13e8a0ac", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "88be638f4d78b4072109e55f13e8a0ac"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + + /* second request - modify file so new hash is generated */ + disk[16]++; + handle = rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_APPLE_II, + "disk1.dsk", disk, disk_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + ASSERT_PTR_NOT_NULL(handle); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=8e39c6077108cafd6193d1c649b5d695", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "8e39c6077108cafd6193d1c649b5d695"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); + + free(disk); + rc_client_destroy(g_client); +} + +#ifndef RC_HASH_NO_ROM + +static void test_identify_and_load_game_unknown_hash_multiconsole(void) +{ + rc_hash_iterator_t* iterator; + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + /* first hash lookup should be pending. inject a secondary console into the iterator */ + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 1); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ + iterator->consoles[iterator->index + 1] = 0; + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + /* when multiple hashes are tried, console will be unknown and hash will be a CSV */ + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857,64b131c5c7fec32985d9c99700babb7e"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +#endif + +static void test_identify_and_load_game_unknown_hash_console_specified(void) +{ + rc_hash_iterator_t* iterator; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* explicitly specify we only want the NES hash processed */ + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + /* first hash lookup should be pending. iterator should not have been initialized */ + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + iterator = rc_client_get_load_state_hash_iterator(g_client); + ASSERT_NUM_EQUALS(iterator->index, 0); + ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); + + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +static void assert_unknown_hash_parameters(uint32_t console_id, const char* hash, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(console_id, RC_CONSOLE_GAMEBOY); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static uint32_t rc_client_identify_unknown_hash(uint32_t console_id, const char* hash, rc_client_t* client, void* callback_data) +{ + assert_unknown_hash_parameters(console_id, hash, client, callback_data); + return 1234; +} + +static void test_identify_and_load_game_unknown_hash_client_provided(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.identify_unknown_hash = rc_client_identify_unknown_hash; + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* patchdata returns 17 */ + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash_unknown_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); + } + + /* same hash generated for all dsk consoles - only one server call should be made */ + assert_api_call_count("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", 1); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash_differ(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* modify the checksum so callback for first lookup will generate a new lookup */ + memset(&image[256], 0, 32); + + /* first lookup fails */ + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + /* second lookup should succeed */ + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +#endif /* RC_CLIENT_SUPPORTS_HASH */ + +/* ----- change media ----- */ + +static void rc_client_callback_expect_hardcore_disabled_undentified_media(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_HARDCORE_DISABLED); + ASSERT_STR_EQUALS(error_message, "Hardcore disabled. Unidentified media inserted."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +#ifdef RC_CLIENT_SUPPORTS_HASH + +static void test_change_media_required_fields(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + rc_client_begin_identify_and_change_media(g_client, NULL, NULL, 0, + rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_no_game_loaded(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_no_game_loaded, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_same_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + /* changing known discs within a game set is expected to succeed */ + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* resetting with a disc from the current game is allowed */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_known_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":5555}"); + + /* changing to a known disc from another game is allowed */ + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* resetting with a disc from another game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_unknown_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + ASSERT_TRUE(rc_client_get_hardcore_enabled(g_client)); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + /* changing to an unknown disc is not allowed - could be a hacked version of one of the game's discs */ + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_hardcore_disabled_undentified_media, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + ASSERT_FALSE(rc_client_get_hardcore_enabled(g_client)); + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_unhashable(void) +{ + const char* original_hash = NULL; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + original_hash = g_client->game->public_.hash; + + /* N64 hash will fail with Not a Nintendo 64 ROM */ + g_client->game->public_.console_id = RC_CONSOLE_NINTENDO_64; + + /* changing to a disc not supported by the system is allowed */ + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* switch back to the original media so we can detect a switch back to the unhashable media */ + rc_client_begin_change_media(g_client, original_hash, rc_client_callback_expect_success, g_callback_userdata); + + /* re-entrant call won't try to regenerate the hash */ + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_unhashable_without_generation(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + /* changing to a disc not supported by the system is allowed */ + rc_client_begin_change_media(g_client, "[NO HASH]", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_back_and_forth(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + uint8_t* image2 = generate_generic_file(image_size); + memset(&image2[256], 0, 32); /* force image2 to be different */ + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); + assert_api_call_count("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", 1); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image2); + free(image); +} + +static void test_change_media_while_loading(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + /* media request won't occur until patch data is received */ + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + /* secondary hash resolution does not occur until game is fully loaded or hash can't be compared to loaded game */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_while_loading_later(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + + /* get past fetching the patch data so there's a valid console for the change media call */ + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + + /* change_media should immediately attempt to resolve the new hash */ + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game - session will be started with the old hash because the new hash hasn't resolved yet */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_async_aborted(void) +{ + rc_client_async_handle_t* handle; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* changing known discs within a game set is expected to succeed */ + handle = rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + /* hash should still have been captured and lookup should succeed without having to call server again */ + reset_mock_api_handlers(); + + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_client_error(void) +{ + rc_client_async_handle_t* handle; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + mock_api_error("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, + rc_client_callback_expect_no_internet, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); + } + + ASSERT_PTR_NULL(handle); + + rc_client_destroy(g_client); + free(image); +} + +#endif + +static void test_change_media_from_hash_required_fields(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + rc_client_begin_change_media(g_client, NULL, + rc_client_callback_expect_hash_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + + rc_client_destroy(g_client); +} + + +static void test_change_media_from_hash_no_game_loaded(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_no_game_loaded, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_same_game(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + /* changing known discs within a game set is expected to succeed */ + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + + /* resetting with a disc from the current game is allowed */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_known_game(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":5555}"); + + /* changing to a known disc from another game is allowed */ + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + + /* resetting with a disc from another game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_unknown_game(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + ASSERT_TRUE(rc_client_get_hardcore_enabled(g_client)); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + /* changing to an unknown disc is not allowed - could be a hacked version of one of the game's discs */ + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_hardcore_disabled_undentified_media, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + + ASSERT_FALSE(rc_client_get_hardcore_enabled(g_client)); + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_back_and_forth(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + + assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); + assert_api_call_count("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", 1); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_while_loading(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + + /* media request won't occur until patch data is received */ + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + /* secondary hash resolution does not occur until game is fully loaded or hash can't be compared to loaded game */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_while_loading_later(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + + /* get past fetching the patch data so there's a valid console for the change media call */ + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + + /* change_media should immediately attempt to resolve the new hash */ + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game - session will be started with the old hash because the new hash hasn't resolved yet */ + async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_async_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* changing known discs within a game set is expected to succeed */ + handle = rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ + + /* hash should still have been captured and lookup should succeed without having to call server again */ + reset_mock_api_handlers(); + + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); +} + +static void test_change_media_from_hash_client_error(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + mock_api_error("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", + rc_client_callback_expect_no_internet, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ + + ASSERT_PTR_NULL(handle); + + rc_client_destroy(g_client); +} + +/* ----- get game image ----- */ + +static void test_game_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "http://server/Images/112233.png"); + + rc_client_destroy(g_client); +} + +/* ----- fetch hash library ----- */ + +static void test_fetch_hash_library_response(int result, const char* error_message, + rc_client_hash_library_t* list, rc_client_t* client, void* callback_userdata) +{ + rc_client_callback_expect_success(result, error_message, client, callback_userdata); + if (result != RC_OK) + return; + + ASSERT_NUM_EQUALS(list->num_entries, 3); + ASSERT_NUM_EQUALS(list->entries[0].game_id, 1); + ASSERT_STR_EQUALS(list->entries[0].hash, "aabbccddeeff00112233445566778899"); + ASSERT_NUM_EQUALS(list->entries[1].game_id, 1); + ASSERT_STR_EQUALS(list->entries[1].hash, "99aabbccddeeff001122334455667788"); + ASSERT_NUM_EQUALS(list->entries[2].game_id, 2); + ASSERT_STR_EQUALS(list->entries[2].hash, "8899aabbccddeeff0011223344556677"); + + rc_client_destroy_hash_library(list); +} + +static void test_fetch_hash_library(void) +{ + g_client = mock_client_not_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=hashlibrary&c=1", "{\"Success\":true,\"MD5List\":{" + "\"aabbccddeeff00112233445566778899\":1," + "\"99aabbccddeeff001122334455667788\":1," + "\"8899aabbccddeeff0011223344556677\":2" + "}}"); + + rc_client_begin_fetch_hash_library(g_client, 1, test_fetch_hash_library_response, g_callback_userdata); + rc_client_destroy(g_client); +} + +/* ----- fetch game titles ----- */ + +static void test_fetch_game_titles_response(int result, const char* error_message, + rc_client_game_title_list_t* list, rc_client_t* client, void* callback_userdata) +{ + rc_client_callback_expect_success(result, error_message, client, callback_userdata); + if (result != RC_OK) + return; + + ASSERT_NUM_EQUALS(list->num_entries, 3); + ASSERT_NUM_EQUALS(list->entries[0].game_id, 3); + ASSERT_STR_EQUALS(list->entries[0].title, "Game Name 3"); + ASSERT_STR_EQUALS(list->entries[0].badge_name, "010003"); + ASSERT_NUM_EQUALS(list->entries[1].game_id, 4); + ASSERT_STR_EQUALS(list->entries[1].title, "Game Name 4"); + ASSERT_STR_EQUALS(list->entries[1].badge_name, "010004"); + ASSERT_NUM_EQUALS(list->entries[2].game_id, 7); + ASSERT_STR_EQUALS(list->entries[2].title, "Game Name 7"); + ASSERT_STR_EQUALS(list->entries[2].badge_name, "010007"); + + rc_client_destroy_game_title_list(list); +} + +static void test_fetch_game_titles(void) +{ + const uint32_t game_ids[] = { 3, 4, 7 }; + g_client = mock_client_not_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameinfolist&g=3,4,7", "{\"Success\":true,\"Response\":[" + "{\"ID\": 3, \"Title\":\"Game Name 3\", \"ImageIcon\": \"/Images/010003.png\"}," + "{\"ID\": 4, \"Title\":\"Game Name 4\", \"ImageIcon\": \"/Images/010004.png\"}," + "{\"ID\": 7, \"Title\":\"Game Name 7\", \"ImageIcon\": \"/Images/010007.png\"}" + "]}"); + + rc_client_begin_fetch_game_titles(g_client, game_ids, 3, test_fetch_game_titles_response, g_callback_userdata); + rc_client_destroy(g_client); +} + +/* ----- all user progress ----- */ + +static void test_fetch_all_user_progress_response(int result, const char* error_message, + rc_client_all_user_progress_t* list, rc_client_t* client, void* callback_userdata) +{ + rc_client_callback_expect_success(result, error_message, client, callback_userdata); + if (result != RC_OK) + return; + + ASSERT_NUM_EQUALS(list->num_entries, 4); + ASSERT_NUM_EQUALS(list->entries[0].game_id, 10); + ASSERT_NUM_EQUALS(list->entries[0].num_achievements, 11); + ASSERT_NUM_EQUALS(list->entries[0].num_unlocked_achievements, 0); + ASSERT_NUM_EQUALS(list->entries[0].num_unlocked_achievements_hardcore, 0); + ASSERT_NUM_EQUALS(list->entries[1].game_id, 20); + ASSERT_NUM_EQUALS(list->entries[1].num_achievements, 21); + ASSERT_NUM_EQUALS(list->entries[1].num_unlocked_achievements, 22); + ASSERT_NUM_EQUALS(list->entries[1].num_unlocked_achievements_hardcore, 0); + ASSERT_NUM_EQUALS(list->entries[2].game_id, 30); + ASSERT_NUM_EQUALS(list->entries[2].num_achievements, 31); + ASSERT_NUM_EQUALS(list->entries[2].num_unlocked_achievements, 32); + ASSERT_NUM_EQUALS(list->entries[2].num_unlocked_achievements_hardcore, 33); + ASSERT_NUM_EQUALS(list->entries[3].game_id, 40); + ASSERT_NUM_EQUALS(list->entries[3].num_achievements, 41); + ASSERT_NUM_EQUALS(list->entries[3].num_unlocked_achievements, 0); + ASSERT_NUM_EQUALS(list->entries[3].num_unlocked_achievements_hardcore, 43); + + rc_client_destroy_all_user_progress(list); +} + +static void test_fetch_all_user_progress(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=allprogress&u=Username&t=ApiToken&c=1", "{\"Success\":true,\"Response\":{\"10\":{\"Achievements\":11}," + "\"20\":{\"Achievements\":21,\"Unlocked\":22}," + "\"30\":{\"Achievements\":31,\"Unlocked\":32,\"UnlockedHardcore\":33}," + "\"40\":{\"Achievements\":41,\"UnlockedHardcore\":43}}}"); + + rc_client_begin_fetch_all_user_progress(g_client, 1, test_fetch_all_user_progress_response, g_callback_userdata); + rc_client_destroy(g_client); +} + +/* ----- subset ----- */ + +static void test_load_subset(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + rc_client_subset_info_t* subset_info; + const rc_client_subset_t* subset; + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_subset); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); + + ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 7); + ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 7); + } + + subset = rc_client_get_subset_info(g_client, 2345); + ASSERT_PTR_NOT_NULL(subset); + if (subset) { + subset_info = g_client->game->subsets->next; + ASSERT_PTR_EQUALS(subset, &subset_info->public_); + + ASSERT_NUM_EQUALS(subset->id, 2345); + ASSERT_STR_EQUALS(subset->title, "Bonus"); + ASSERT_STR_EQUALS(subset->badge_name, "112234"); + ASSERT_STR_EQUALS(subset->badge_url, "http://server/Images/112234.png"); + ASSERT_NUM_EQUALS(subset->num_achievements, 3); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 2); + + achievement = &subset_info->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5501); + ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 5501"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc 5501"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "005501"); + ASSERT_STR_EQUALS(achievement->public_.badge_url, "https://media.retroachievements.org/Badge/005501.png"); + ASSERT_STR_EQUALS(achievement->public_.badge_locked_url, "https://media.retroachievements.org/Badge/005501_lock.png"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_STR_EQUALS(achievement->author, "User1"); + ASSERT_NUM_EQUALS(achievement->created_time, 1367266583); + ASSERT_NUM_EQUALS(achievement->updated_time, 1376929305); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &subset_info->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5502); + ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 5502"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc 5502"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "005502"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &subset_info->achievements[2]; + ASSERT_NUM_EQUALS(achievement->public_.id, 5503); + ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 5503"); + ASSERT_STR_EQUALS(achievement->public_.description, "Desc 5503"); + ASSERT_STR_EQUALS(achievement->public_.badge_name, "005503"); + ASSERT_NUM_EQUALS(achievement->public_.points, 5); + ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &subset_info->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 81); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard 81"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc 81"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + + leaderboard = &subset_info->leaderboards[1]; + ASSERT_NUM_EQUALS(leaderboard->public_.id, 82); + ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard 82"); + ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc 82"); + ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + rc_client_destroy(g_client); +} + +static void test_subset_list(void) +{ + rc_client_subset_list_t* list; + const rc_client_subset_t* subset; + + g_client = mock_client_game_loaded(patchdata_subset, no_unlocks); + + list = rc_client_create_subset_list(g_client); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_subsets, 2); + + subset = list->subsets[0]; + ASSERT_NUM_EQUALS(subset->id, 1111); + ASSERT_STR_EQUALS(subset->title, "Sample Game"); + ASSERT_STR_EQUALS(subset->badge_name, "112233"); + ASSERT_NUM_EQUALS(subset->num_achievements, 7); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 7); + ASSERT_STR_EQUALS(subset->badge_url, "http://server/Images/112233.png"); + + subset = list->subsets[1]; + ASSERT_NUM_EQUALS(subset->id, 2345); + ASSERT_STR_EQUALS(subset->title, "Bonus"); + ASSERT_STR_EQUALS(subset->badge_name, "112234"); + ASSERT_NUM_EQUALS(subset->num_achievements, 3); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 2); + ASSERT_STR_EQUALS(subset->badge_url, "http://server/Images/112234.png"); + + rc_client_destroy_subset_list(list); + } + + rc_client_unload_game(g_client); + ASSERT_PTR_NULL(g_client->game); + + list = rc_client_create_subset_list(g_client); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_subsets, 0); + rc_client_destroy_subset_list(list); + } + + rc_client_destroy(g_client); +} + +/* ----- achievement list ----- */ + +static void test_achievement_list_simple(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 0); + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unlocks(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in hardcore mode, 5501 should be unlocked, but 5502 will be locked */ + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + rc_client_destroy_achievement_list(list); + } + + rc_client_set_hardcore_enabled(g_client, 0); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in softcore mode, both should be unlocked */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unlocks_encore_mode(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in hardcore mode, 5501 should be unlocked, but both will appear locked due to encore mode */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_set_hardcore_enabled(g_client, 0); + mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=0&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlock_5501h_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in softcore mode, both should be unlocked, but will appear locked due to encore mode */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + /* unlock 5501, should appear unlocked again */ + mock_memory(memory, sizeof(memory)); + rc_client_do_frame(g_client); + memory[1] = 3; + memory[2] = 7; + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=0&m=0123456789ABCDEF&v=7f066800cd3962efeb1f479e5671b59c", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501)); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + + achievement = list->buckets[0].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + achievement = list->buckets[1].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + + achievement = list->buckets[0].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + achievement = list->buckets[1].achievements[0]; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unofficial_and_unsupported(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unofficial_off(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 0); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 0); + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_buckets(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* also check mapping to lock state */ + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 5); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 9); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 70); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 71); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievement no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); + ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_buckets_progress_sort(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + g_client->game->subsets->achievements[0].trigger->measured_target = 100; + g_client->game->subsets->achievements[0].trigger->measured_value = 86; + g_client->game->subsets->achievements[1].trigger->measured_target = 100; + g_client->game->subsets->achievements[1].trigger->measured_value = 85; + g_client->game->subsets->achievements[2].trigger->measured_target = 1000; + g_client->game->subsets->achievements[2].trigger->measured_value = 855; + g_client->game->subsets->achievements[3].trigger->measured_target = 100; + g_client->game->subsets->achievements[3].trigger->measured_value = 87; + g_client->game->subsets->achievements[4].trigger->measured_target = 100; + g_client->game->subsets->achievements[4].trigger->measured_value = 85; + g_client->game->subsets->achievements[5].trigger->measured_target = 100; + g_client->game->subsets->achievements[5].trigger->measured_value = 75; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 5); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 87.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "87/100"); + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 86.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "86/100"); + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.5f); + ASSERT_STR_EQUALS(achievement->measured_progress, "855/1,000"); + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); + ASSERT_NUM_EQUALS(achievement->id, 6); + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); + ASSERT_NUM_EQUALS(achievement->id, 9); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_FLOAT_EQUALS(list->buckets[1].achievements[0]->measured_percent, 75.0f); + ASSERT_STR_EQUALS(list->buckets[1].achievements[0]->measured_progress, "75/100"); + ASSERT_FLOAT_EQUALS(list->buckets[1].achievements[1]->measured_percent, 0.0f); + ASSERT_STR_EQUALS(list->buckets[1].achievements[1]->measured_progress, ""); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_buckets_progress_sort_big_ids(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_big_ids, no_unlocks); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + g_client->game->subsets->achievements[0].trigger->measured_target = 100; + g_client->game->subsets->achievements[0].trigger->measured_value = 86; + g_client->game->subsets->achievements[1].trigger->measured_target = 100; + g_client->game->subsets->achievements[1].trigger->measured_value = 85; + g_client->game->subsets->achievements[2].trigger->measured_target = 100; + g_client->game->subsets->achievements[2].trigger->measured_value = 85; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 3); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 86.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "86/100"); + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); + ASSERT_NUM_EQUALS(achievement->id, 6); + achievement = *iter++; + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); + ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); + ASSERT_NUM_EQUALS(achievement->id, 4294967295UL); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_FLOAT_EQUALS(list->buckets[1].achievements[0]->measured_percent, 0.0f); + ASSERT_STR_EQUALS(list->buckets[1].achievements[0]->measured_progress, ""); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_buckets_with_unsynced(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t* achievement; + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b"; + const char* unlock_request_params2 = "r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&o=2&v=3cca3f9a9cfd6c0d5b6a17b9bb539070"; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); + g_client->callbacks.server_call = rc_client_server_call_async; + mock_memory(memory, sizeof(memory)); + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + memory[5] = 5; /* trigger achievement 5 */ + memory[1] = 1; /* begin challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + /* first failure will immediately requeue the request */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + rc_client_idle(g_client); + + /* second failure will queue it */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + rc_client_idle(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) + { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* adjust unlock time on achievement 5 so it's no longer recent */ + achievement = (rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5); + achievement->unlock_time -= 15 * 60; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) + { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocks Not Synced to Server"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* allow unlock request to succeed */ + g_now += 2 * 1000; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params2); + async_api_response(unlock_request_params2, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) + { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_with_unofficial_and_unsupported(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_subset_unofficial_unsupported, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 6); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 9); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 70); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[5]->id, 71); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 5); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 6); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 9); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 70); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[5]->id, 71); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5501); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5502); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Bonus - Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_buckets(void) +{ + rc_client_achievement_list_t* list; + const rc_client_achievement_t** iter; + const rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_subset, unlock_8_and_5502); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + memory[0x17] = 7; /* trigger achievement 5501 */ + rc_client_do_frame(g_client); + event_count = 0; + + /* set the unlock time for achievement 5 back one second to ensure consistent sorting */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time--; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 6); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[3].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[5].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[5].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[5].label, "Bonus - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[5].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[5].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievements no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5501))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 5); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); + ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Bonus - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[1]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + rc_client_destroy(g_client); +} + +/* ----- leaderboards ----- */ + +static void test_leaderboard_list_simple(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_simple_with_unsupported(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_buckets(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_buckets_with_unsupported(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ + mock_client_load_game(patchdata_exhaustive, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0B] = 3; /* start 52 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 1); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 5); + + iter = list->buckets[2].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_subset(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_subset, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 2); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 81); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 82); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + memory[0x08] = 2; /* start 82 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 4); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 82); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 1); + + iter = list->buckets[2].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 81); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_hidden(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_leaderboards_hidden, no_unlocks); + + rc_client_do_frame(g_client); + + /* hidden leaderboards (45+48) should not appear in list */ + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static const char* lbinfo_4401_top_10 = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":78," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," + "{\"User\":\"DisplayName\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static const char* lbinfo_4401_top_10_no_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":78," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," + "{\"User\":\"PlayerJ\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static const char* lbinfo_4401_near_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":78," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":17,\"Index\":17,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":18,\"Index\":18,\"DateSubmitted\":1615634566}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":19,\"Index\":19,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":19,\"Index\":20,\"DateSubmitted\":1615623878}," + "{\"User\":\"DisplayName\",\"Score\":3811,\"Rank\":19,\"Index\":21,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":19,\"Index\":22,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":23,\"Index\":23,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":24,\"Index\":24,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":25,\"Index\":25,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":26,\"Index\":26,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static rc_client_leaderboard_entry_list_t* g_leaderboard_entries = NULL; +static void rc_client_callback_expect_leaderboard_entry_list(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + + ASSERT_PTR_NOT_NULL(list); + g_leaderboard_entries = list; +} + +static void test_fetch_leaderboard_entries(void) +{ + rc_client_leaderboard_entry_t* entry; + char url[256]; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); + + rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + ASSERT_NUM_EQUALS(g_leaderboard_entries->total_entries, 78); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ASSERT_STR_EQUALS(entry->display, "003524"); + ASSERT_NUM_EQUALS(entry->index, 1); + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerG.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ASSERT_STR_EQUALS(entry->display, "003645"); + ASSERT_NUM_EQUALS(entry->index, 2); + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_NUM_EQUALS(entry->submitted, 1615634566); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "DisplayName"); + ASSERT_STR_EQUALS(entry->display, "003754"); + ASSERT_NUM_EQUALS(entry->index, 3); + ASSERT_NUM_EQUALS(entry->rank, 3); + ASSERT_NUM_EQUALS(entry->submitted, 1615234553); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/DisplayName.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 4); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615653844); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615623878); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615653284); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerA.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ASSERT_STR_EQUALS(entry->display, "003902"); + ASSERT_NUM_EQUALS(entry->index, 7); + ASSERT_NUM_EQUALS(entry->rank, 7); + ASSERT_NUM_EQUALS(entry->submitted, 1615632174); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ASSERT_STR_EQUALS(entry->display, "003956"); + ASSERT_NUM_EQUALS(entry->index, 8); + ASSERT_NUM_EQUALS(entry->rank, 8); + ASSERT_NUM_EQUALS(entry->submitted, 1616384834); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ASSERT_STR_EQUALS(entry->display, "003985"); + ASSERT_NUM_EQUALS(entry->index, 9); + ASSERT_NUM_EQUALS(entry->rank, 9); + ASSERT_NUM_EQUALS(entry->submitted, 1615238383); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + ASSERT_STR_EQUALS(entry->display, "004012"); + ASSERT_NUM_EQUALS(entry->index, 10); + ASSERT_NUM_EQUALS(entry->rank, 10); + ASSERT_NUM_EQUALS(entry->submitted, 1615638984); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 2); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void test_fetch_leaderboard_entries_no_user(void) +{ + rc_client_leaderboard_entry_t* entry; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10_no_user); + + rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerJ"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, -1); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void test_fetch_leaderboard_entries_around_user(void) +{ + rc_client_leaderboard_entry_t* entry; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); + + rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + ASSERT_NUM_EQUALS(g_leaderboard_entries->total_entries, 78); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ASSERT_STR_EQUALS(entry->display, "003524"); + ASSERT_NUM_EQUALS(entry->index, 17); + ASSERT_NUM_EQUALS(entry->rank, 17); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ASSERT_STR_EQUALS(entry->display, "003645"); + ASSERT_NUM_EQUALS(entry->index, 18); + ASSERT_NUM_EQUALS(entry->rank, 18); + ASSERT_NUM_EQUALS(entry->submitted, 1615634566); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 19); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615653844); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 20); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615623878); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "DisplayName"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 21); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615234553); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 22); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615653284); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ASSERT_STR_EQUALS(entry->display, "003902"); + ASSERT_NUM_EQUALS(entry->index, 23); + ASSERT_NUM_EQUALS(entry->rank, 23); + ASSERT_NUM_EQUALS(entry->submitted, 1615632174); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ASSERT_STR_EQUALS(entry->display, "003956"); + ASSERT_NUM_EQUALS(entry->index, 24); + ASSERT_NUM_EQUALS(entry->rank, 24); + ASSERT_NUM_EQUALS(entry->submitted, 1616384834); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ASSERT_STR_EQUALS(entry->display, "003985"); + ASSERT_NUM_EQUALS(entry->index, 25); + ASSERT_NUM_EQUALS(entry->rank, 25); + ASSERT_NUM_EQUALS(entry->submitted, 1615238383); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + ASSERT_STR_EQUALS(entry->display, "004012"); + ASSERT_NUM_EQUALS(entry->index, 26); + ASSERT_NUM_EQUALS(entry->rank, 26); + ASSERT_NUM_EQUALS(entry->submitted, 1615638984); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 4); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_entry_list_login_required(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); + ASSERT_STR_EQUALS(error_message, "Login required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + ASSERT_PTR_NULL(list); +} + +static void test_fetch_leaderboard_entries_around_user_not_logged_in(void) +{ + g_client = mock_client_not_logged_in(); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); + + rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, + rc_client_callback_expect_leaderboard_entry_list_login_required, g_callback_userdata); + ASSERT_PTR_NULL(g_leaderboard_entries); + + assert_api_not_called("r=lbinfo&i=4401&u=Username&c=10"); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_uncalled(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called.") +} + +static void test_fetch_leaderboard_entries_async_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + g_leaderboard_entries = NULL; + + handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); + ASSERT_PTR_NULL(g_leaderboard_entries); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_internet_not_available(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); + ASSERT_STR_EQUALS(error_message, "Internet not available."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + ASSERT_PTR_NULL(list); +} + +static void test_fetch_leaderboard_entries_client_error(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + g_leaderboard_entries = NULL; + + mock_api_error("r=lbinfo&i=4401&c=10", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); + + handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_internet_not_available, g_callback_userdata); + + ASSERT_PTR_NULL(handle); + ASSERT_PTR_NULL(g_leaderboard_entries); + + rc_client_destroy(g_client); +} + +static void test_map_leaderboard_format(void) +{ + int i; + + for (i = 0; i < 30; ++i) { + switch (i) { + case RC_FORMAT_SECONDS: + case RC_FORMAT_CENTISECS: + case RC_FORMAT_MINUTES: + case RC_FORMAT_SECONDS_AS_MINUTES: + case RC_FORMAT_FRAMES: + ASSERT_NUM_EQUALS(rc_client_map_leaderboard_format(i), RC_CLIENT_LEADERBOARD_FORMAT_TIME); + break; + + case RC_FORMAT_SCORE: + ASSERT_NUM_EQUALS(rc_client_map_leaderboard_format(i), RC_CLIENT_LEADERBOARD_FORMAT_SCORE); + break; + + default: + ASSERT_NUM_EQUALS(rc_client_map_leaderboard_format(i), RC_CLIENT_LEADERBOARD_FORMAT_VALUE); + break; + } + } +} + +/* ----- do frame ----- */ + +static void test_do_frame_bounds_check_system(void) +{ + const uint32_t memory_size = 0x10010; /* provide more memory than system expects */ + uint8_t* memory = (uint8_t*)calloc(1, memory_size); + ASSERT_PTR_NOT_NULL(memory); + + g_client = mock_client_game_loaded(patchdata_bounds_check_system, no_unlocks); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":7,\"AchievementsRemaining\":4}"); + + ASSERT_PTR_NOT_NULL(g_client->game); + ASSERT_NUM_EQUALS(g_client->game->max_valid_address, 0xFFFF); + + assert_achievement_state(g_client, 1, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 2, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 3, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ + assert_achievement_state(g_client, 4, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* cannot read two bytes from 0xFFFF, but size isn't enforced until do_frame */ + assert_achievement_state(g_client, 6, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* verify that reading at the edge of the memory bounds fails */ + mock_memory(memory, 0x10000); + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* cannot read two bytes from 0xFFFF */ + + /* set up memory so achievement 7 would trigger if the pointed at address were valid */ + /* achievement should not trigger - invalid address should be ignored */ + memory[0x10000] = 5; + memory[0x00000] = 1; /* byte(0xFFFF + byte(0x0000)) == 5 */ + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* even if the extra memory is available, it shouldn't try to read beyond the system defined max address */ + mock_memory(memory, memory_size); + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* change max valid address so memory will be evaluated. achievement should trigger */ + g_client->game->max_valid_address = memory_size - 1; + rc_client_do_frame(g_client); + assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + + rc_client_destroy(g_client); + free(memory); +} + +static void test_do_frame_bounds_check_available(void) +{ + const rc_trigger_t* trigger; + uint8_t memory[8] = { 0,0,0,0,0,0,0,0 }; + g_client = mock_client_game_loaded(patchdata_bounds_check_8, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + /* all addresses are valid according to the system, so no achievements should be disabled yet. */ + assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + trigger = ((rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 808))->trigger; + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_WAITING); + + /* limit the memory that's actually exposed and try to process a frame */ + mock_memory(memory, sizeof(memory)); + rc_client_do_frame(g_client); + + assert_achievement_state(g_client, 408, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 508, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 608, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 708, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_DISABLED); + + assert_achievement_state(g_client, 416, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 516, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 616, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 716, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 816, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 424, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 524, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 24-bit read actually fetches 32-bits */ + assert_achievement_state(g_client, 624, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ + assert_achievement_state(g_client, 724, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 824, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 432, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 532, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only three bytes available */ + assert_achievement_state(g_client, 632, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ + assert_achievement_state(g_client, 732, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 832, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_already_awarded(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_server_error(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"Achievement not found\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + /* achievement still counts as triggered */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ + + /* but an error should have been reported */ + event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->server_error->api, "award_achievement"); + ASSERT_STR_EQUALS(event->server_error->error_message, "Achievement not found"); + ASSERT_NUM_EQUALS(event->server_error->result, RC_API_FAILURE); + ASSERT_NUM_EQUALS(event->server_error->related_id, 8); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_while_spectating(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"Achievement should not have been unlocked in spectating mode\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + /* achievement still counts as triggered */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ + + /* expect API not called */ + assert_api_not_called("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + } + + rc_client_destroy(g_client); +} + +static int rc_client_callback_deny_unlock(uint32_t achievement_id, rc_client_t* client) +{ + return 0; +} + +static int rc_client_callback_allow_unlock(uint32_t achievement_id, rc_client_t* client) +{ + return 1; +} + +static void test_do_frame_achievement_trigger_blocked(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + uint32_t num_active; + const char* api_call8 = "r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"; + const char* api_call9 = "r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6"; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.can_submit_achievement_unlock = rc_client_callback_deny_unlock; + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + num_active = g_client->game->runtime.trigger_count; + + mock_api_response(api_call8, + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + mock_api_response(api_call9, + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12350); /* 12345+5 - not updated by server response that didn't happen */ + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + assert_api_not_called(api_call8); + + g_client->callbacks.can_submit_achievement_unlock = rc_client_callback_allow_unlock; + + memory[9] = 9; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + assert_api_called(api_call9); + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_automatic_retry(const char* response, int status_code) +{ + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + const char* unlock_request_params1 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=1&v=4b4af09722aeb0e8a16c152f9a646281"; + const char* unlock_request_params3 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=3&v=d5bc287a030084aa77a911b6c2c5fc96"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_error(unlock_request_params, response, status_code); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second failure will queue it */ + async_api_error(unlock_request_params, response, status_code); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_DISCONNECTED, 0)); + event_count = 0; + + /* advance time so queued request gets processed */ + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 1 * 1000); + g_now += 1 * 1000; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params1); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it */ + async_api_error(unlock_request_params1, response, status_code); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 2 * 1000); + g_now += 2 * 1000; + + rc_client_idle(g_client); + assert_api_pending(unlock_request_params3); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(unlock_request_params3, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + /* reconnected event should be pending, watch for it */ + ASSERT_NUM_EQUALS(event_count, 0); + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RECONNECTED, 0)); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_automatic_retry_empty(void) +{ + test_do_frame_achievement_trigger_automatic_retry("", 200); +} + +static void test_do_frame_achievement_trigger_automatic_retry_429(void) +{ + test_do_frame_achievement_trigger_automatic_retry(response_429, 429); +} + +static void test_do_frame_achievement_trigger_automatic_retry_502(void) +{ + test_do_frame_achievement_trigger_automatic_retry(response_502, 502); +} + +static void test_do_frame_achievement_trigger_automatic_retry_503(void) +{ + test_do_frame_achievement_trigger_automatic_retry(response_503, 503); +} + +static void test_do_frame_achievement_trigger_automatic_retry_custom_timeout(void) +{ + test_do_frame_achievement_trigger_automatic_retry("Request has timed out.", RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR); +} + +static void test_do_frame_achievement_trigger_automatic_retry_generic_empty_response(void) +{ + test_do_frame_achievement_trigger_automatic_retry("", 0); +} + +static void test_do_frame_achievement_trigger_subset(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_subset, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + memory[0x17] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5437); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_rarity(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive_typed, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->type, RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION); + ASSERT_FLOAT_EQUALS(event->achievement->rarity, 86.0f); + ASSERT_FLOAT_EQUALS(event->achievement->rarity_hardcore, 73.1f); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_measured(void) +{ + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=70&h=1&m=0123456789ABCDEF&v=61e40027573e2cde88b49d27f6804879", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":70,\"AchievementsRemaining\":11}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=71&h=1&m=0123456789ABCDEF&v=3a8d55b81d391557d5111306599a2b0d", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":71,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x10] = 0x39; memory[0x11] = 0x30; /* 12345 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12,345/100,000"); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12%"); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increment measured value - raw counter will report progress change, percentage will not */ + memory[0x10] = 0x3A; /* 12346 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12,346/100,000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increment measured value - raw counter will report progress change, percentage will not */ + memory[0x11] = 0x33; /* 13114 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "13,114/100,000"); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "13%"); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* trigger measured achievements - progress becomes blank */ + memory[0x10] = 0xA0; memory[0x11] = 0x86; memory[0x12] = 0x01; /* 100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); /* two TRIGGERED events, and no PROGRESS_INDICATOR_SHOW events */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_measured_progress_event(void) +{ + rc_client_event_t* event; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=6&h=1&m=0123456789ABCDEF&v=65206f4290098ecd30c7845e895057d0", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":6,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x06] = 3; /* 3/6 */ + memory[0x11] = 0xC3; memory[0x10] = 0x4F; /* 49999/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + /* 3/6 = 50%, 49999/100000 = 49.999% */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); + + /* both achievements should have been updated, */ + achievement = rc_client_get_achievement_info(g_client, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "3/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 50.0); + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "49,999/100,000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 49.999); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* any change will trigger the popup - even dropping */ + memory[0x10] = 0x4E; /* 49998 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 70); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "49,998/100,000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* don't trigger popup when value changes to 0 as the measured_progress string will be blank */ + memory[0x06] = 0; /* 0 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + /* both at 50%, only report first */ + memory[0x06] = 3; /* 3/6 */ + memory[0x11] = 0xC3; memory[0x10] = 0x50; /* 50000/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second slightly ahead */ + memory[0x6] = 4; /* 4/6 */ + memory[0x12] = 1; memory[0x11] = 0x04; memory[0x10] = 0x6B; /* 66667/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 70); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "66,667/100,000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* don't show popup on trigger */ + memory[0x06] = 6; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, ""); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + ASSERT_PTR_NOT_NULL(g_client->game->progress_tracker.hide_callback); + g_now += 2 * 1000; + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_measured_progress_reshown(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x06] = 3; /* 3/6 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + /* should be two callbacks queued - hiding the progress indicator, and the rich presence update */ + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks, g_client->game->progress_tracker.hide_callback); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks->next); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); + + /* advance time to hide the progress indicator */ + g_now = g_client->game->progress_tracker.hide_callback->when; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + /* only the rich presence update should be scheduled */ + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next); + + /* advance time to just before the rich presence update */ + g_now = g_client->state.scheduled_callbacks->when - 10; + + /* reschedule the progress indicator */ + memory[0x06] = 4; /* 4/6 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + /* should be two callbacks queued - rich presence update, then hiding the progress indicator */ + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->next, g_client->game->progress_tracker.hide_callback); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_challenge_indicator(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 1; /* show indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 0; /* hide indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 1; /* show indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* trigger achievement - expect both hide and trigger events. both should have triggered achievement data */ + memory[7] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_challenge_indicator_primed_while_reset(void) +{ + static const char* patchdata = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":7,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_T:0xH0002=4_R:0xH0003=3\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}" + "]," + "\"Leaderboards\":[]" + "}]}"; + + rc_client_event_t* event; + rc_client_achievement_info_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata, no_unlocks); + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_ACTIVE); + + memory[1] = 3; /* primed */ + memory[3] = 3; /* but reset */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_ACTIVE); + + memory[3] = 0; /* disable reset */ + + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 1); /* show indicator event */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_PRIMED); + + memory[3] = 3; /* reset active */ + + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 1); /* hide indicator event */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_challenge_indicator_primed_while_reset_next(void) +{ + static const char* patchdata = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":7,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_T:0xH0002=4_Z:0xH0003=3_P:0xH0006=1.10.\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}" + "]," + "\"Leaderboards\":[]" + "}]}"; + + rc_client_event_t* event; + rc_client_achievement_info_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata, no_unlocks); + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); + + memory[6] = 1; /* pause condition will gain a hit, but not enough to pause it */ + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_ACTIVE); + + /* ResetNextIf will clear hits on condition 4, _and_ the trigger will be primed at the same time. + * The ResetNextIf will cause rc_evaluate_trigger to return RESET, but we still want to raise a + * PRIMED event out of rc_client. */ + memory[1] = 3; /* primed */ + memory[3] = 3; /* reset pause condition */ + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 1); /* show indicator event */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_PRIMED); + + memory[3] = 0; /* disable reset, pause counter increases */ + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[3] = 0; /* re-enable reset. don't expect any events */ + + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); + ASSERT_PTR_NOT_NULL(event); + + memory[9] = 9; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery_encore(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); + ASSERT_PTR_NOT_NULL(event); + + memory[9] = 9; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery_subset(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_subset, unlock_5501_and_5502); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x19] = 9; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5503); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5503)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=5503&h=1&m=0123456789ABCDEF&v=50486c3ea9e2e32bb3683017b1488f88", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5503,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_SUBSET_COMPLETED, 2345); + ASSERT_PTR_NOT_NULL(event); + ASSERT_PTR_EQUALS(event->subset, rc_client_get_subset_info(g_client, 2345)); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_started(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_update(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* update the leaderboard */ + memory[0x0E] = 18; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000018"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_failed(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel the leaderboard */ + memory[0x0C] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->leaderboard_id, 44); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->submitted_score, "000017"); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->best_score, "000023"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->new_rank, 2); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_entries, 2); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_top_entries, 2); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].username, "Player1"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[0].rank, 1); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].score, "000044"); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].username, "Username"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[1].rank, 2); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].score, "000023"); + ASSERT_PTR_NOT_NULL(event->leaderboard_scoreboard->top_entries); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_server_error(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":false,\"Error\":\"Leaderboard not found\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + /* an error should have also been reported */ + event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->server_error->api, "submit_lboard_entry"); + ASSERT_STR_EQUALS(event->server_error->error_message, "Leaderboard not found"); + ASSERT_NUM_EQUALS(event->server_error->result, RC_API_FAILURE); + ASSERT_NUM_EQUALS(event->server_error->related_id, 44); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_while_spectating(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":false,\"Error\":\"Leaderboard entry should not have been submitted in spectating mode\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* expect API not called */ + assert_api_not_called("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_immediate(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_leaderboard_immediate_submit, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard (it will immediately submit) */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); /* don't expect start or tracker events - only submit and scoreboard */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->leaderboard_id, 44); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->submitted_score, "000017"); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->best_score, "000023"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->new_rank, 2); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_entries, 2); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_top_entries, 2); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].username, "Player1"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[0].rank, 1); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].score, "000044"); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].username, "Username"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[1].rank, 2); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].score, "000023"); + ASSERT_PTR_NOT_NULL(event->leaderboard_scoreboard->top_entries); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_hidden(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_leaderboards_hidden, no_unlocks); + + /* hidden leaderboards should still start/track/submit normally. they just don't appear in list */ + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=48&s=17&m=0123456789ABCDEF&v=468a1f9e9475d8c4d862f48cc8806018", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0A] = 2; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->leaderboard_id, 48); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->submitted_score, "000017"); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->best_score, "000023"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->new_rank, 2); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_entries, 2); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_top_entries, 2); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].username, "Player1"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[0].rank, 1); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].score, "000044"); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].username, "Username"); + ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[1].rank, 2); + ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].score, "000023"); + ASSERT_PTR_NOT_NULL(event->leaderboard_scoreboard->top_entries); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_destroy(g_client); +} + +static int rc_client_callback_deny_leaderboard(uint32_t leaderboard_id, rc_client_t* client) +{ + return 0; +} + +static int rc_client_callback_allow_leaderboard(uint32_t leaderboard_id, rc_client_t* client) +{ + return 1; +} + +static void test_do_frame_leaderboard_submit_blocked(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + const char* api_call = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.can_submit_leaderboard_entry = rc_client_callback_deny_leaderboard; + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + + mock_api_response(api_call, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0B] = 0; + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + /* but make sure the server wasn't called */ + assert_api_not_called(api_call); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + g_client->callbacks.can_submit_leaderboard_entry = rc_client_callback_allow_leaderboard; + + /* restart the leaderboard - will immediately submit */ + memory[0x0B] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44)); + + /* make sure the server was called */ + assert_api_called(api_call); + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_softcore(void) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_event_t* event; + uint8_t memory[64]; + const char* api_call = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + rc_client_set_hardcore_enabled(g_client, 0); + + ASSERT_PTR_NOT_NULL(g_client->game); + mock_memory(memory, sizeof(memory)); + + mock_api_response(api_call, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard - will be ignored in softcore */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* allow leaderboards to be processed in softcore. have to manually activate as it wasn't activated on game load */ + g_client->state.allow_leaderboards_in_softcore = 1; + leaderboard = (rc_client_leaderboard_info_t*)rc_client_get_leaderboard_info(g_client, 44); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + leaderboard->lboard->state = RC_LBOARD_STATE_ACTIVE; + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0B] = 0; + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + /* but make sure the server wasn't called */ + assert_api_not_called(api_call); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + g_client->state.hardcore = 1; + + /* restart the leaderboard - will immediately submit */ + memory[0x0B] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44)); + + /* make sure the server was called */ + assert_api_called(api_call); + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_tracker_sharing(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start one leaderboard (one tracker) */ + memory[0x0B] = 1; + memory[0x0E] = 17; + memory[0x0F] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start additional leaderboards (45,46,47) - 45 and 46 should generate new trackers */ + memory[0x0A] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 5); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 45); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 45 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 46); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 46 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 47); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 44,47 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); /* 45 has different size */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 3); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); /* 46 has different format */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start additional leaderboard (48) - should share tracker with 44 */ + memory[0x0A] = 2; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 3); /* 44,47,48 */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 44 */ + memory[0x0C] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 47,48 */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 45 */ + memory[0x0C] = 2; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 45); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 46 */ + memory[0x0C] = 3; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 46); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 3); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel 47, start 51 */ + memory[0x0A] = 3; + memory[0x0B] = 0; + memory[0x0C] = 4; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 47); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 1); /* 48 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 51 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel 48 */ + memory[0x0C] = 5; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_tracker_sharing_hits(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start leaderboards 51,52 (share tracker) */ + memory[0x0A] = 3; + memory[0x0B] = 3; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 52); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 52)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); + + /* hit count ticks */ + memory[0x09] = 1; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); + + /* cancel leaderboard 51 */ + memory[0x0C] = 6; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "2"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "2"); + + /* hit count ticks */ + memory[0x0A] = 0; + memory[0x0C] = 0; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "3"); + + /* restart leaderboard 51 - hit count differs, can't share */ + memory[0x0A] = 3; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "1"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "4"); /* 52 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); /* 51 */ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_automatic_retry(void) +{ + const char* submit_entry_params = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; + const char* submit_entry_params1 = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&o=1&v=659685352e6d8e14923ea32da6f8c3e4"; + const char* submit_entry_params3 = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&o=3&v=91debb749e90c2beff4f21430716dac9"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_response(submit_entry_params, ""); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second failure will queue it for one second later */ + async_api_response(submit_entry_params, ""); + assert_api_not_pending(submit_entry_params); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + /* disconnected event should be raised after retry is queued */ + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_DISCONNECTED, 0)); + event_count = 0; + + /* advance time and process scheduled callbacks */ + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 1 * 1000); + g_now += 1 * 1000; + + rc_client_idle(g_client); + assert_api_pending(submit_entry_params1); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it for two seconds later */ + async_api_response(submit_entry_params1, ""); + assert_api_not_pending(submit_entry_params1); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 2 * 1000); + g_now += 2 * 1000; + + rc_client_idle(g_client); + assert_api_pending(submit_entry_params3); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(submit_entry_params3, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* reconnected event should be pending, watch for it */ + ASSERT_NUM_EQUALS(event_count, 0); + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RECONNECTED, 0)); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_multiple_automatic_retry(void) +{ + const char* unlock_5501_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + const char* unlock_5501_request_params1 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=1&v=4b4af09722aeb0e8a16c152f9a646281"; + const char* unlock_5501_request_params4 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=4&v=63646fa1b30797144cc87881f7d95932"; + const char* unlock_5502_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5502&h=1&m=0123456789ABCDEF&v=8d7405f5aacc9b4b334619a0dae62a56"; + const char* unlock_5502_request_params1 = "r=awardachievement&u=Username&t=ApiToken&a=5502&h=1&m=0123456789ABCDEF&o=1&v=46d77e89548126a88c0cdda1980c4dd4"; + const char* submit_entry_params = "r=submitlbentry&u=Username&t=ApiToken&i=4401&s=17&m=0123456789ABCDEF&v=13b4cdfe295a97783e78bb48c8910867"; + const char* submit_entry_params1 = "r=submitlbentry&u=Username&t=ApiToken&i=4401&s=17&m=0123456789ABCDEF&o=1&v=16fe62616f0e39d0a2620b50cc004928"; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + g_now = 100000; /* time 0 */ + memory[1] = 3; /* trigger 5501 */ + memory[2] = 7; /* trigger 5501 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501)); + event_count = 0; + + /* first failure will immediately requeue the request */ + async_api_response(unlock_5501_request_params, ""); + assert_api_pending(unlock_5501_request_params); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second failure will queue it for one second later (101000) */ + async_api_response(unlock_5501_request_params, ""); + assert_api_not_pending(unlock_5501_request_params); + + /* disconnected event should be raised after retry is queued */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_DISCONNECTED, 0)); + event_count = 0; + + /* advance time 500ms. trigger second achievement and start leaderboard */ + g_now += 500; /* 100500 */ + memory[1] = 2; /* trigger 5502 */ + memory[2] = 9; /* trigger 5502 */ + memory[0x0C] = 1; /* start leaderboard 1 */ + memory[0x0E] = 17; /* leaderboard 1 value */ + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + /* disconnected event should not be raised again */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5502)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 4401)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + event_count = 0; + + /* first failure will immediately requeue the request */ + async_api_response(unlock_5502_request_params, ""); + assert_api_pending(unlock_5502_request_params); + + /* second failure will queue it for one second later (101500) */ + async_api_response(unlock_5502_request_params, ""); + assert_api_not_pending(unlock_5502_request_params); + + /* advance time 250ms. submit leaderboard */ + g_now += 250; /* 100750 */ + memory[0x0D] = 2; /* submit leaderboard 1 */ + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + /* disconnected event should not be raised again */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 4401)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + event_count = 0; + + /* first failure will immediately requeue the request */ + async_api_response(submit_entry_params, ""); + assert_api_pending(submit_entry_params); + + /* second failure will queue it for one second later (101750) */ + async_api_response(submit_entry_params, ""); + assert_api_not_pending(submit_entry_params); + + /* advance time. first callback will fail again and be queued for two seconds later (103000) */ + g_now += 350; /* 101100 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + assert_api_pending(unlock_5501_request_params1); + async_api_response(unlock_5501_request_params1, ""); + assert_api_not_pending(unlock_5501_request_params1); + + /* advance time. second callback will succeed */ + g_now += 500; /* 101600 */ + rc_client_do_frame(g_client); + assert_api_pending(unlock_5502_request_params1); + async_api_response(unlock_5502_request_params1, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + assert_api_not_pending(unlock_5502_request_params1); + + /* reconnected event should not be raised until all pending requests are handled */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* advance time. third callback will succeed */ + g_now += 500; /* 102100 */ + rc_client_do_frame(g_client); + assert_api_pending(submit_entry_params1); + async_api_response(submit_entry_params1, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + assert_api_not_pending(submit_entry_params); + + /* reconnected event should not be raised until all pending requests are handled */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* advance time. first callback will finally succeed */ + g_now += 2000; /* 104100 */ + rc_client_do_frame(g_client); + assert_api_pending(unlock_5501_request_params4); + async_api_response(unlock_5501_request_params4, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + assert_api_not_pending(unlock_5501_request_params4); + + /* reconnected event should be pending, watch for it */ + ASSERT_NUM_EQUALS(event_count, 0); + rc_client_idle(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RECONNECTED, 0)); + + rc_client_destroy(g_client); +} + +static void test_do_frame_rich_presence_hitcount(void) +{ + char buffer[8]; + const char* patchdata_rich_presence_hit_count = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(M:1=1)\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]" + "}]}"; + g_client = mock_client_game_loaded(patchdata_rich_presence_hit_count, no_unlocks); + ASSERT_PTR_NOT_NULL(g_client->game); + + ASSERT_NUM_EQUALS(rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)), 1); + ASSERT_STR_EQUALS(buffer, "0"); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)), 1); + ASSERT_STR_EQUALS(buffer, "1"); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)), 1); + ASSERT_STR_EQUALS(buffer, "2"); + + rc_client_destroy(g_client); +} + +static void test_clock_get_now_millisecs(void) +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + rc_get_time_millisecs_func_t get_millisecs = client->callbacks.get_time_millisecs; + +#ifdef RC_NO_SLEEP + rc_clock_t time1; + ASSERT_PTR_NOT_NULL(get_millisecs); + time1 = get_millisecs(client); + ASSERT_NUM_NOT_EQUALS(time1, 0); +#else + rc_clock_t time1, time2, diff; + time_t first = time(NULL), now; + + do { + ASSERT_PTR_NOT_NULL(get_millisecs); + time1 = get_millisecs(client); + ASSERT_NUM_NOT_EQUALS(time1, 0); + +#if defined(_WIN32) + Sleep(50); +#else + usleep(50000); +#endif + + time2 = get_millisecs(client); + ASSERT_NUM_NOT_EQUALS(time2, 0); + diff = time2 - time1; + + ASSERT_NUM_GREATER(diff, 49); + if (diff < 100) + break; + + now = time(NULL); + if (now - first >= 3) { + ASSERT_FAIL("could not get a 50ms sleep interval within 3 seconds"); + break; + } + + } while (1); +#endif + + rc_client_destroy(client); +} + +/* ----- ping ----- */ + +static void test_idle_ping(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + + g_client->state.frames_processed++; /* ping won't get called if no frames have been processed */ + rc_client_idle(g_client); + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&h=1&x=0123456789ABCDEF"); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + } + + /* unloading game should unschedule ping */ + rc_client_unload_game(g_client); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + rc_client_destroy(g_client); +} + +static void test_do_frame_ping_rich_presence(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_memory(memory, sizeof(memory)); + memory[0x03] = 25; + + /* before rc_client_do_frame, memory will not have been read. all values will be 0 */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a0&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + + g_client->state.frames_processed++; /* ping won't get called if no frames have been processed */ + rc_client_idle(g_client); + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a0&h=1&x=0123456789ABCDEF"); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + /* rc_client_do_frame will update the memory, so the message will contain appropriate data */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); + + /* change the memory to make sure the rich presence gets updated */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + memory[0x03] = 75; + + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75&h=1&x=0123456789ABCDEF"); + + /* no change to rich presence strings. make sure the callback still gets called again */ + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + assert_api_call_count("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75&h=1&x=0123456789ABCDEF", 2); + } + + rc_client_destroy(g_client); +} + +static int rc_client_callback_rich_presence_override_allow(rc_client_t* client, char buffer[], size_t buffersize) +{ + memcpy(buffer, "Custom", 7); + return 0; +} + +static int rc_client_callback_rich_presence_override_replace(rc_client_t* client, char buffer[], size_t buffersize) +{ + memcpy(buffer, "Custom", 7); + return 6; +} + +static void test_do_frame_ping_rich_presence_override_allowed(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.rich_presence_override = rc_client_callback_rich_presence_override_allow; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + rc_client_scheduled_callback_t ping_callback; + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_memory(memory, sizeof(memory)); + memory[0x03] = 25; + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + + /* ping won't get called if no frames have been processed. do_frame will increment frames_processed */ + rc_client_do_frame(g_client); + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_ping_rich_presence_override_replaced(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + g_client->callbacks.rich_presence_override = rc_client_callback_rich_presence_override_replace; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) + { + rc_client_scheduled_callback_t ping_callback; + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_memory(memory, sizeof(memory)); + memory[0x03] = 25; + + /* before rc_client_do_frame, can_submit only ignores the m parameter. ping still occurs */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Custom&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + + /* ping won't get called if no frames have been processed. do_frame will increment frames_processed */ + rc_client_do_frame(g_client); + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Custom&h=1&x=0123456789ABCDEF"); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + /* ping still happens every two minutes, even if message not provided */ + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Custom&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + } + + rc_client_destroy(g_client); +} + +static void test_idle_ping_while_not_running(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + memory[0x03] = 25; + mock_memory(memory, sizeof(memory)); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ping_callback = g_client->state.scheduled_callbacks->callback; + + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); + g_now += 30 * 1000; + + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); + + /* if no frames have been processed since the last ping, a ping shouldn't be sent */ + g_client->state.frames_processed = g_client->state.frames_at_last_ping; + rc_client_idle(g_client); + + assert_api_not_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + g_now += 120 * 1000; + + /* do_frame will increment frames_processed, and ping will get called */ + + rc_client_do_frame(g_client); + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + g_now += 120 * 1000; + + /* no frames processed. ping shouldn't be sent, but still rescheduled */ + + rc_client_idle(g_client); + + assert_api_call_count("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", 1); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + } + + /* unloading game should unschedule ping */ + rc_client_unload_game(g_client); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + rc_client_destroy(g_client); +} + +/* ----- reset ----- */ + +static void test_reset_hides_widgets(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x06] = 3; /* progress indicator for achievement 6 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + rc_client_reset(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* non tracked achievements/leaderboards should also be reset to waiting */ + achievement = rc_client_get_achievement_info(g_client, 5); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + + leaderboard = rc_client_get_leaderboard_info(g_client, 46); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + rc_client_destroy(g_client); +} + +static void test_reset_detaches_hide_progress_indicator_event(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x06] = 3; /* progress indicator for achievement 6 */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + /* should be two callbacks queued - hiding the progress indicator, and the rich presence update */ + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks, g_client->game->progress_tracker.hide_callback); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks->next); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); + + /* advance time to hide the progress indicator */ + g_now = g_client->game->progress_tracker.hide_callback->when; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + /* only the rich presence update should be scheduled */ + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next); + + /* advance time to just before the rich presence update */ + g_now = g_client->state.scheduled_callbacks->when - 10; + + /* reschedule the progress indicator */ + memory[0x06] = 4; /* 4/6 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + /* should be two callbacks queued - rich presence update, then hiding the progress indicator */ + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->next, g_client->game->progress_tracker.hide_callback); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); + + rc_client_reset(g_client); + + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + + /* only the rich presence update should be scheduled */ + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next); + + rc_client_destroy(g_client); +} + +/* ----- pause ----- */ + +static void test_can_pause(void) +{ + uint16_t frames_needed, frames_needed2, frames_needed3, frames_needed4; + uint32_t frames_remaining; + int i; + + /* pause without client should be allowed */ + ASSERT_NUM_EQUALS(rc_client_can_pause(NULL, NULL), 1); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + + rc_client_do_frame(g_client); + + /* first pause should always be allowed */ + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed = g_client->state.unpaused_frame_decay; + + /* if no frames have been processed, the client is still paused, so pause is allowed */ + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed); + + /* do a few frames (not enough to allow pause) - pause should still not be allowed */ + for (i = 0; i < 10; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); + ASSERT_NUM_EQUALS(frames_remaining, 10); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed - 10); + + /* do enough frames to allow pause, but not clear out the decay value. + * pause should be allowed, and the decay value should be reset to a higher value. */ + for (i = 0; i < 20; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_GREATER(g_client->state.unpaused_frame_decay, 0); + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed2 = g_client->state.unpaused_frame_decay; + ASSERT_NUM_GREATER(frames_needed2, frames_needed); + + /* do enough frames to allow pause before - should not allow pause now */ + for (i = 0; i < 25; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); + ASSERT_NUM_EQUALS(frames_remaining, 15); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed2 - 25); + + /* do enough frames to allow pause, but not clear out the decay value. + * pause should be allowed, and the decay value should be reset to an even higher value. */ + for (i = 0; i < 35; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_GREATER(g_client->state.unpaused_frame_decay, 0); + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed3 = g_client->state.unpaused_frame_decay; + ASSERT_NUM_GREATER(frames_needed3, frames_needed2); + + /* completely clear out the decay. decay value should drop, but not all the way. */ + for (i = 0; i < frames_needed3; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + frames_needed4 = g_client->state.unpaused_frame_decay; + ASSERT_NUM_LESS(frames_needed4, frames_needed3); + ASSERT_NUM_GREATER(frames_needed4, frames_needed); + + /* completely clear out the decay. decay value should drop back to default + * have to do this twice to get through the decayed cycles */ + for (i = 0; i < frames_needed4 * 2; i++) + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed); + + /* disable hardcore. pause should be allowed immediately */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + + rc_client_destroy(g_client); +} + +/* ----- progress ----- */ + +static void test_deserialize_progress_updates_widgets(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + const rc_client_event_t* event; + uint8_t* serialized1; + uint8_t* serialized2; + size_t serialize_size; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* create an initial checkpoint */ + serialize_size = rc_client_progress_size(g_client); + serialized1 = (uint8_t*)malloc(serialize_size); + serialized2 = (uint8_t*)malloc(serialize_size); + ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized1), RC_OK); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x06] = 4; /* progress indicator for achievement 6*/ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* capture the state with the widgets visible */ + ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized2), RC_OK); + + /* deserialize current state. expect progress tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize original state. expect challenge indicator hide, tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized1), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_ACTIVE); + + /* deserialize second state. expect challenge indicator show, tracker show */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* update tracker value */ + memory[0x0E] = 30; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000030"); + + /* deserialize second state. expect challenge tracker update to old value */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 1); + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + free(serialized2); + free(serialized1); + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_null(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, NULL), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ + ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* advance frame, challenge indicator and leaderboard tracker should reappear */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_invalid(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, memory), RC_INVALID_STATE); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ + ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* advance frame, challenge indicator and leaderboard tracker should reappear */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_sized(void) +{ + uint8_t* serialized1; + uint8_t* serialized2; + size_t serialize_size; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* create an initial checkpoint */ + serialize_size = rc_client_progress_size(g_client); + serialized1 = (uint8_t*)malloc(serialize_size); + serialized2 = (uint8_t*)malloc(serialize_size); + ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, serialized1, serialize_size - 1), RC_INSUFFICIENT_BUFFER); + ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, serialized1, serialize_size), RC_OK); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x06] = 4; /* progress indicator for achievement 6*/ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 4); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); + event_count = 0; + + /* capture the state with the widgets visible */ + ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, serialized2, serialize_size), RC_OK); + + /* deserialize current state. expect progress tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, serialized2, serialize_size), RC_OK); + ASSERT_NUM_EQUALS(event_count, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); + event_count = 0; + + /* deserialize original state with incorrect size. expect challenge indicator hide, tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, serialized1, serialize_size - 1), RC_INSUFFICIENT_BUFFER); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + /* deserialize second state. expect challenge indicator show, tracker show */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, serialized2, serialize_size), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + free(serialized2); + free(serialized1); + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_unknown_game(void) +{ + uint8_t buffer[128]; + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_NUM_EQUALS(rc_client_progress_size(g_client), 0); + ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, buffer, sizeof(buffer)), RC_NO_GAME_LOADED); + ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, buffer, sizeof(buffer)), RC_NO_GAME_LOADED); + + rc_client_destroy(g_client); +} + +/* ----- processing required ----- */ + +static void test_processing_required(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_empty_game(void) +{ + g_client = mock_client_game_loaded(patchdata_empty, no_unlocks); + + ASSERT_FALSE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_rich_presence_only(void) +{ + g_client = mock_client_game_loaded(patchdata_rich_presence_only, no_unlocks); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_leaderboard_only(void) +{ + g_client = mock_client_game_loaded(patchdata_leaderboard_only, no_unlocks); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_after_mastery(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501_and_5502); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_after_mastery_no_leaderboards(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_0lbd, unlock_5501_and_5502); + + ASSERT_FALSE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +/* ----- settings ----- */ + +static void test_set_hardcore_disable(void) +{ + const rc_client_achievement_t* achievement; + const rc_client_leaderboard_t* leaderboard; + const rc_trigger_t* trigger; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + trigger = ((rc_client_achievement_info_t*)achievement)->trigger; + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + trigger = ((rc_client_achievement_info_t*)achievement)->trigger; + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_WAITING); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* only 5502 should be active*/ + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); + + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + trigger = ((rc_client_achievement_info_t*)achievement)->trigger; + + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_disable_active_tracker(void) +{ + const rc_client_leaderboard_t* leaderboard; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x0C] = 1; + memory[0x0E] = 25; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 4401); + ASSERT_PTR_NOT_NULL(event); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + ASSERT_NUM_EQUALS(event_count, 1); + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable(void) +{ + const rc_client_achievement_t* achievement; + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_logged_in(); + rc_client_set_hardcore_enabled(g_client, 0); + mock_client_load_game_softcore(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); + } + + /* when enabling hardcore, flag waiting_for_reset. this will prevent processing until rc_client_reset is called */ + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RESET, 0)); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* 5502 should be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); + } + + /* resetting clears waiting_for_reset */ + rc_client_reset(g_client); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + /* hardcore already enabled, attempting to set it again shouldn't flag waiting_for_reset */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable_no_game_loaded(void) +{ + g_client = mock_client_logged_in(); + rc_client_set_hardcore_enabled(g_client, 0); + + /* enabling hardcore before a game is loaded just toggles the flag */ + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable_encore_mode(void) +{ + const rc_client_achievement_t* achievement; + rc_client_achievement_info_t* achievement_info; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + g_client->game->runtime.triggers[1].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + /* trigger an achievement */ + achievement_info = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 5501); + achievement_info->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_TRIGGERED; + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* only one active now */ + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_PTR_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger, g_client->game->runtime.triggers[0].trigger); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); + + rc_client_destroy(g_client); +} + +static void test_set_encore_mode_enable(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + /* toggle encore mode with a game loaded has no effect */ + rc_client_set_encore_mode_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + rc_client_destroy(g_client); +} + +static void test_set_encore_mode_disable(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + rc_client_set_encore_mode_enabled(g_client, 0); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + /* toggle encore mode with a game loaded has no effect */ + rc_client_set_encore_mode_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + rc_client_destroy(g_client); +} + +static void test_get_user_agent_clause(void) +{ + char expected_clause[] = "rcheevos/" RCHEEVOS_VERSION_STRING; + char buffer[64]; + + g_client = mock_client_not_logged_in(); + ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, sizeof(buffer)), sizeof(expected_clause) - 1); + ASSERT_STR_EQUALS(buffer, expected_clause); + + /* snprintf will return the number of characters it wants, even if the buffer is too small, + * but will only fill as much of the buffer is available */ + ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, 8), sizeof(expected_clause) - 1); + ASSERT_STR_EQUALS(buffer, "rcheevo"); + + rc_client_destroy(g_client); +} + +static void test_version(void) +{ + ASSERT_STR_EQUALS(rc_version_string(), RCHEEVOS_VERSION_STRING); + ASSERT_NUM_EQUALS(rc_version(), RCHEEVOS_VERSION); +} + +/* ----- harness ----- */ + +void test_client(void) { + TEST_SUITE_BEGIN(); + + /* login */ + TEST(test_login_with_password); + TEST(test_login_with_token); + TEST(test_login_required_fields); + TEST(test_login_with_incorrect_password); + TEST(test_login_with_incorrect_token); + TEST(test_login_with_expired_token); + TEST(test_login_with_banned_account); + TEST(test_login_incomplete_response); + TEST(test_login_with_password_async); + TEST(test_login_with_password_async_aborted); + TEST(test_login_with_password_async_destroyed); + TEST(test_login_with_password_client_error); + + /* logout */ + TEST(test_logout); + TEST(test_logout_with_game_loaded); + TEST(test_logout_during_login); + TEST(test_logout_during_fetch_game); + + /* user */ + TEST(test_user_get_image_url); + + TEST(test_get_user_game_summary); + TEST(test_get_user_game_summary_softcore); + TEST(test_get_user_game_summary_encore_mode); + TEST(test_get_user_game_summary_with_unsupported_and_unofficial); + TEST(test_get_user_game_summary_with_unsupported_unlocks); + TEST(test_get_user_game_summary_with_unofficial_off); + TEST(test_get_user_game_summary_no_achievements); + TEST(test_get_user_game_summary_unknown_game); + TEST(test_get_user_game_summary_progress_incomplete); + TEST(test_get_user_game_summary_progress_progression_no_win); + TEST(test_get_user_game_summary_progress_win_only); + TEST(test_get_user_game_summary_beat); + TEST(test_get_user_game_summary_mastery); + + /* load game */ + TEST(test_load_game_required_fields); + TEST(test_load_game_unknown_hash); + TEST(test_load_game_unknown_hash_repeated); + TEST(test_load_game_unknown_hash_multiple); + TEST(test_load_game_not_logged_in); + TEST(test_load_game); + TEST(test_load_game_async_load_different_game); + TEST(test_load_game_async_login); + TEST(test_load_game_async_login_with_incorrect_password); + TEST(test_load_game_async_login_logout); + TEST(test_load_game_async_login_aborted); + TEST(test_load_game_patch_failure); + TEST(test_load_game_startsession_failure); + TEST(test_load_game_startsession_timeout); + TEST(test_load_game_startsession_custom_timeout); + TEST(test_load_game_patch_aborted); + TEST(test_load_game_startsession_aborted); + TEST(test_load_game_while_spectating); + TEST(test_load_game_process_game_sets); + TEST(test_load_game_destroy_while_fetching_game_data); + TEST(test_load_unknown_game); + TEST(test_load_unknown_game_multihash); + TEST(test_load_game_dispatched_read_memory); + + /* unload game */ + TEST(test_unload_game); + TEST(test_unload_game_hides_ui); + TEST(test_unload_game_while_fetching_game_data); + TEST(test_unload_game_while_starting_session); + +#ifdef RC_CLIENT_SUPPORTS_HASH + /* identify and load game */ + TEST(test_identify_and_load_game_required_fields); + TEST(test_identify_and_load_game_console_specified); + TEST(test_identify_and_load_game_console_not_specified); + #ifndef RC_HASH_NO_ROM + TEST(test_identify_and_load_game_multiconsole_first); + TEST(test_identify_and_load_game_multiconsole_second); + #endif + #ifndef RC_HASH_NO_DISC + TEST(test_identify_and_load_game_from_disc); + #endif + TEST(test_identify_and_load_game_unknown_hash); + TEST(test_identify_and_load_game_unknown_hash_repeated); + TEST(test_identify_and_load_game_unknown_hash_multiple); + #ifndef RC_HASH_NO_ROM + TEST(test_identify_and_load_game_unknown_hash_multiconsole); + #endif + TEST(test_identify_and_load_game_unknown_hash_console_specified); + TEST(test_identify_and_load_game_unknown_hash_client_provided); + TEST(test_identify_and_load_game_multihash); + TEST(test_identify_and_load_game_multihash_unknown_game); + TEST(test_identify_and_load_game_multihash_differ); + + /* change media */ + TEST(test_change_media_required_fields); + TEST(test_change_media_no_game_loaded); + TEST(test_change_media_same_game); + TEST(test_change_media_known_game); + TEST(test_change_media_unknown_game); + TEST(test_change_media_unhashable); + TEST(test_change_media_unhashable_without_generation); + TEST(test_change_media_back_and_forth); + TEST(test_change_media_while_loading); + TEST(test_change_media_while_loading_later); + TEST(test_change_media_async_aborted); + TEST(test_change_media_client_error); +#endif + + TEST(test_change_media_from_hash_required_fields); + TEST(test_change_media_from_hash_no_game_loaded); + TEST(test_change_media_from_hash_same_game); + TEST(test_change_media_from_hash_known_game); + TEST(test_change_media_from_hash_unknown_game); + TEST(test_change_media_from_hash_back_and_forth); + TEST(test_change_media_from_hash_while_loading); + TEST(test_change_media_from_hash_while_loading_later); + TEST(test_change_media_from_hash_async_aborted); + TEST(test_change_media_from_hash_client_error); + + /* game */ + TEST(test_game_get_image_url); + + /* hash library */ + TEST(test_fetch_hash_library); + + /* game titles */ + TEST(test_fetch_game_titles); + + /* all user progress */ + TEST(test_fetch_all_user_progress); + + /* subset */ + TEST(test_load_subset); + TEST(test_subset_list); + + /* achievements */ + TEST(test_achievement_list_simple); + TEST(test_achievement_list_simple_with_unlocks); + TEST(test_achievement_list_simple_with_unlocks_encore_mode); + TEST(test_achievement_list_simple_with_unofficial_and_unsupported); + TEST(test_achievement_list_simple_with_unofficial_off); + TEST(test_achievement_list_buckets); + TEST(test_achievement_list_buckets_progress_sort); + TEST(test_achievement_list_buckets_progress_sort_big_ids); + TEST(test_achievement_list_buckets_with_unsynced); + TEST(test_achievement_list_subset_with_unofficial_and_unsupported); + TEST(test_achievement_list_subset_buckets); + + TEST(test_achievement_get_image_url); + + /* leaderboards */ + TEST(test_leaderboard_list_simple); + TEST(test_leaderboard_list_simple_with_unsupported); + TEST(test_leaderboard_list_buckets); + TEST(test_leaderboard_list_buckets_with_unsupported); + TEST(test_leaderboard_list_subset); + TEST(test_leaderboard_list_hidden); + + TEST(test_fetch_leaderboard_entries); + TEST(test_fetch_leaderboard_entries_no_user); + TEST(test_fetch_leaderboard_entries_around_user); + TEST(test_fetch_leaderboard_entries_around_user_not_logged_in); + TEST(test_fetch_leaderboard_entries_async_aborted); + TEST(test_fetch_leaderboard_entries_client_error); + + TEST(test_map_leaderboard_format); + + /* do frame */ + TEST(test_do_frame_bounds_check_system); + TEST(test_do_frame_bounds_check_available); + TEST(test_do_frame_achievement_trigger); + TEST(test_do_frame_achievement_trigger_already_awarded); + TEST(test_do_frame_achievement_trigger_server_error); + TEST(test_do_frame_achievement_trigger_while_spectating); + TEST(test_do_frame_achievement_trigger_blocked); + TEST(test_do_frame_achievement_trigger_automatic_retry_empty); + TEST(test_do_frame_achievement_trigger_automatic_retry_429); + TEST(test_do_frame_achievement_trigger_automatic_retry_502); + TEST(test_do_frame_achievement_trigger_automatic_retry_503); + TEST(test_do_frame_achievement_trigger_automatic_retry_custom_timeout); + TEST(test_do_frame_achievement_trigger_automatic_retry_generic_empty_response); + TEST(test_do_frame_achievement_trigger_subset); + TEST(test_do_frame_achievement_trigger_rarity); + TEST(test_do_frame_achievement_measured); + TEST(test_do_frame_achievement_measured_progress_event); + TEST(test_do_frame_achievement_measured_progress_reshown); + TEST(test_do_frame_achievement_challenge_indicator); + TEST(test_do_frame_achievement_challenge_indicator_primed_while_reset); + TEST(test_do_frame_achievement_challenge_indicator_primed_while_reset_next); + TEST(test_do_frame_mastery); + TEST(test_do_frame_mastery_encore); + TEST(test_do_frame_mastery_subset); + TEST(test_do_frame_leaderboard_started); + TEST(test_do_frame_leaderboard_update); + TEST(test_do_frame_leaderboard_failed); + TEST(test_do_frame_leaderboard_submit); + TEST(test_do_frame_leaderboard_submit_server_error); + TEST(test_do_frame_leaderboard_submit_while_spectating); + TEST(test_do_frame_leaderboard_submit_immediate); + TEST(test_do_frame_leaderboard_submit_hidden); + TEST(test_do_frame_leaderboard_submit_blocked); + TEST(test_do_frame_leaderboard_submit_softcore); + TEST(test_do_frame_leaderboard_tracker_sharing); + TEST(test_do_frame_leaderboard_tracker_sharing_hits); + TEST(test_do_frame_leaderboard_submit_automatic_retry); + TEST(test_do_frame_multiple_automatic_retry); + TEST(test_do_frame_rich_presence_hitcount); + + TEST(test_clock_get_now_millisecs); + + /* ping */ + TEST(test_idle_ping); + TEST(test_do_frame_ping_rich_presence); + TEST(test_do_frame_ping_rich_presence_override_allowed); + TEST(test_do_frame_ping_rich_presence_override_replaced); + TEST(test_idle_ping_while_not_running); + + /* reset */ + TEST(test_reset_hides_widgets); + TEST(test_reset_detaches_hide_progress_indicator_event); + + /* pause */ + TEST(test_can_pause); + + /* deserialize_progress */ + TEST(test_deserialize_progress_updates_widgets); + TEST(test_deserialize_progress_null); + TEST(test_deserialize_progress_invalid); + TEST(test_deserialize_progress_sized); + TEST(test_deserialize_progress_unknown_game); + + /* processing required */ + TEST(test_processing_required); + TEST(test_processing_required_empty_game); + TEST(test_processing_required_rich_presence_only); + TEST(test_processing_required_leaderboard_only); + TEST(test_processing_required_after_mastery); + TEST(test_processing_required_after_mastery_no_leaderboards); + + /* settings */ + TEST(test_set_hardcore_disable); + TEST(test_set_hardcore_disable_active_tracker); + TEST(test_set_hardcore_enable); + TEST(test_set_hardcore_enable_no_game_loaded); + TEST(test_set_hardcore_enable_encore_mode); + TEST(test_set_encore_mode_enable); + TEST(test_set_encore_mode_disable); + + TEST(test_get_user_agent_clause); + TEST(test_version); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/test_rc_client_external.c b/src/rcheevos/test/test_rc_client_external.c new file mode 100644 index 0000000000..497b22fc0d --- /dev/null +++ b/src/rcheevos/test/test_rc_client_external.c @@ -0,0 +1,2170 @@ +#include "rc_client.h" + +#include "../src/rc_client_external_versions.h" +#include "../src/rc_client_internal.h" +#include "../src/rc_version.h" +#include "rc_consoles.h" +#include "rhash/data.h" + +#include "test_framework.h" + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + +static rc_client_t* g_client; +static const char* g_external_event; +static int g_external_int = 0; +static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ + +/* begin from test_rc_client.c */ + +extern void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); +extern void reset_mock_api_handlers(void); +extern void mock_api_response(const char* request_params, const char* response_body); +extern void mock_api_error(const char* request_params, const char* response_body, int http_status_code); + +/* end from test_rc_client.c */ + +#define RC_OFFSETOF(s,f) ((uint32_t)((uint8_t*)&(((s*)(0))->f) - ((uint8_t*)0))) + +#define ASSERT_FIELD_OFFSET(struct1, struct2, field) { \ + const uint32_t __o1 = RC_OFFSETOF(struct1, field); \ + const uint32_t __o2 = RC_OFFSETOF(struct2, field); \ + if (__o1 != __o2) { \ + ASSERT_FAIL("Expected: " #struct1 "." #field " at offset %u (%s:%d)\n Found: " #struct2 "." #field " at offset %u", \ + __o1, test_framework_basename(__FILE__), __LINE__, __o2); \ + } \ +} + +static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + return 0; +} + +static rc_client_t* mock_client_with_external() +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + client->state.external_client = (rc_client_external_t*) + rc_buffer_alloc(&client->state.buffer, sizeof(*client->state.external_client)); + memset(client->state.external_client, 0, sizeof(*client->state.external_client)); + + rc_api_set_host(NULL); + reset_mock_api_handlers(); + g_external_event = "none"; + g_external_int = 0; + + return client; +} + +static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_external_unload_game(void) +{ + g_external_event = "unload_game"; +} + +/* ----- settings ----- */ + +static int rc_client_external_get_int(void) +{ + return g_external_int; +} + +static void rc_client_external_set_int(int value) +{ + g_external_int = value; +} + +static void test_hardcore_enabled(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->get_hardcore_enabled = rc_client_external_get_int; + g_client->state.external_client->set_hardcore_enabled = rc_client_external_set_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(g_external_int, 0); + + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(g_external_int, 1); + + rc_client_destroy(g_client); +} + +static void test_unofficial_enabled(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->get_unofficial_enabled = rc_client_external_get_int; + g_client->state.external_client->set_unofficial_enabled = rc_client_external_set_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_get_unofficial_enabled(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_get_unofficial_enabled(g_client), 1); + + rc_client_set_unofficial_enabled(g_client, 0); + ASSERT_NUM_EQUALS(g_external_int, 0); + + rc_client_set_unofficial_enabled(g_client, 1); + ASSERT_NUM_EQUALS(g_external_int, 1); + + rc_client_destroy(g_client); +} + +static void test_encore_mode_enabled(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->get_encore_mode_enabled = rc_client_external_get_int; + g_client->state.external_client->set_encore_mode_enabled = rc_client_external_set_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + rc_client_set_encore_mode_enabled(g_client, 0); + ASSERT_NUM_EQUALS(g_external_int, 0); + + rc_client_set_encore_mode_enabled(g_client, 1); + ASSERT_NUM_EQUALS(g_external_int, 1); + + rc_client_destroy(g_client); +} + +static void test_spectator_mode_enabled(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->get_spectator_mode_enabled = rc_client_external_get_int; + g_client->state.external_client->set_spectator_mode_enabled = rc_client_external_set_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_get_spectator_mode_enabled(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_get_spectator_mode_enabled(g_client), 1); + + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_NUM_EQUALS(g_external_int, 0); + + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_NUM_EQUALS(g_external_int, 1); + + rc_client_destroy(g_client); +} + +static void rc_client_external_log_message(const char* message, const rc_client_t* client) +{ +} + +static void rc_client_external_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_NUM_EQUALS(level, RC_CLIENT_LOG_LEVEL_INFO); + ASSERT_PTR_EQUALS(callback, rc_client_external_log_message); + + g_external_event = "enable_logging"; +} + +static void test_enable_logging(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->enable_logging = rc_client_external_enable_logging; + + rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_INFO, rc_client_external_log_message); + + ASSERT_STR_EQUALS(g_external_event, "enable_logging"); + + rc_client_destroy(g_client); +} + +static void rc_client_external_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ +} + +static void rc_client_external_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(handler, rc_client_external_event_handler); + + g_external_event = "event_handler"; +} + +static void test_event_handler(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->set_event_handler = rc_client_external_set_event_handler; + + rc_client_set_event_handler(g_client, rc_client_external_event_handler); + + ASSERT_STR_EQUALS(g_external_event, "event_handler"); + + rc_client_destroy(g_client); +} + +static uint32_t rc_client_external_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + return 0; +} + +static void rc_client_external_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(handler, rc_client_external_read_memory); + + g_external_event = "read_memory"; +} + +static void test_read_memory(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->set_read_memory = rc_client_external_set_read_memory_function; + + rc_client_set_read_memory_function(g_client, rc_client_external_read_memory); + + ASSERT_STR_EQUALS(g_external_event, "read_memory"); + + rc_client_destroy(g_client); +} + +static rc_clock_t rc_client_external_now_millisecs(const rc_client_t* client) +{ + return (rc_clock_t)12345678; +} + +static void rc_client_external_set_get_time_millisecs(rc_client_t* client, rc_get_time_millisecs_func_t handler) +{ + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(handler, rc_client_external_now_millisecs); + + g_external_event = "set_milli"; +} + +static void test_get_time_millisecs(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->set_get_time_millisecs = rc_client_external_set_get_time_millisecs; + + rc_client_set_get_time_millisecs_function(g_client, rc_client_external_now_millisecs); + + ASSERT_STR_EQUALS(g_external_event, "set_milli"); + + rc_client_destroy(g_client); +} + +static void rc_client_external_set_host(const char* hostname) +{ + ASSERT_STR_EQUALS(hostname, "localhost"); + + g_external_event = "set_host"; +} + +static void test_set_host(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->set_host = rc_client_external_set_host; + + rc_client_set_host(g_client, "localhost"); + + ASSERT_STR_EQUALS(g_external_event, "set_host"); + + rc_api_set_host(NULL); + rc_api_set_image_host(NULL); + + rc_client_destroy(g_client); +} + +static size_t rc_client_external_get_user_agent_clause(char buffer[], size_t buffer_size) +{ + return snprintf(buffer, buffer_size, "external/2.1"); +} + +static void test_get_user_agent_clause(void) +{ + char expected_clause[] = "external/2.1 rc_client/" RCHEEVOS_VERSION_STRING; + char buffer[64]; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_user_agent_clause = rc_client_external_get_user_agent_clause; + + ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, sizeof(buffer)), sizeof(expected_clause) - 1); + ASSERT_STR_EQUALS(buffer, expected_clause); + + /* snprintf will return the number of characters it wants, even if the buffer is too small, + * but will only fill as much of the buffer is available */ + ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, 8), sizeof(expected_clause) - 1); + ASSERT_STR_EQUALS(buffer, "externa"); + + ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, 20), sizeof(expected_clause) - 1); + ASSERT_STR_EQUALS(buffer, "external/2.1 rc_cli"); + + rc_client_destroy(g_client); +} + +static void rc_client_external_add_game_hash(const char* hash, uint32_t game_id) +{ + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(game_id, 1234); + + g_external_event = "add_game_hash"; + g_external_int = game_id; +} + +static void test_add_game_hash(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->add_game_hash = rc_client_external_add_game_hash; + + rc_client_add_game_hash(g_client, "6a2305a2b6675a97ff792709be1ca857", 1234); + + /* hash should be loaded in both local client and external client */ + ASSERT_PTR_NOT_NULL(g_client->hashes); + ASSERT_STR_EQUALS(g_client->hashes->hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_NUM_EQUALS(g_client->hashes->game_id, 1234); + + ASSERT_STR_EQUALS(g_external_event, "add_game_hash"); + + rc_client_destroy(g_client); +} + +static void test_set_allow_background_memory_reads(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->set_allow_background_memory_reads = rc_client_external_set_int; + + rc_client_set_allow_background_memory_reads(g_client, 0); + ASSERT_NUM_EQUALS(g_external_int, 0); + + rc_client_set_allow_background_memory_reads(g_client, 1); + ASSERT_NUM_EQUALS(g_external_int, 1); + + rc_client_destroy(g_client); +} + +/* ----- login ----- */ + +static void test_v1_user_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, display_name); + ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, username); + ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, token); + ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, score); + ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, score_softcore); + ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, num_unread_messages); +} + +static void test_v3_user_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, display_name); + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, username); + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, token); + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, score); + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, score_softcore); + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, num_unread_messages); + ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, avatar_url); +} + +static void assert_login_with_password(rc_client_t* client, const char* username, const char* password) +{ + ASSERT_PTR_EQUALS(client, g_client); + + ASSERT_STR_EQUALS(username, "User"); + ASSERT_STR_EQUALS(password, "Pa$$word"); +} + +static rc_client_async_handle_t* rc_client_external_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + assert_login_with_password(client, username, password); + + g_external_event = "login"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static const rc_client_user_t* rc_client_external_get_user_info_v1(void) +{ + v1_rc_client_user_t* user = (v1_rc_client_user_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_user_t)); + + memset(user, 0, sizeof(*user)); + user->display_name = "User"; + user->username = "User"; + user->token = "ApiToken"; + user->score = 12345; + user->score_softcore = 123; + user->num_unread_messages = 2; + + return (rc_client_user_t*)user; +} + +static const rc_client_user_t* rc_client_external_get_user_info_v3(void) +{ + v3_rc_client_user_t* user = (v3_rc_client_user_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_user_t)); + + memset(user, 0, sizeof(*user)); + user->display_name = "User"; + user->username = "User"; + user->token = "ApiToken"; + user->score = 12345; + user->score_softcore = 123; + user->num_unread_messages = 2; + user->avatar_url = "/UserPic/User.png"; + + return (rc_client_user_t*)user; +} + +static void test_login_with_password(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_login_with_password = rc_client_external_login_with_password; + g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1; + g_client->state.external_client->get_user_info_v3 = rc_client_external_get_user_info_v3; + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "login"); + + /* user data should come from external client. validate structure */ + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + ASSERT_STR_EQUALS(user->avatar_url, "/UserPic/User.png"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->user.username); + + rc_client_destroy(g_client); +} + +static void assert_login_with_token(rc_client_t* client, const char* username, const char* token) +{ + ASSERT_PTR_EQUALS(client, g_client); + + ASSERT_STR_EQUALS(username, "User"); + ASSERT_STR_EQUALS(token, "ApiToken"); +} + +static rc_client_async_handle_t* rc_client_external_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + assert_login_with_token(client, username, token); + + g_external_event = "login"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static void test_login_with_token_v1(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token; + g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1; + + rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "login"); + + /* user data should come from external client. validate structure */ + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + ASSERT_STR_EQUALS(user->avatar_url, "https://media.retroachievements.org/UserPic/User.png"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->user.username); + + rc_client_destroy(g_client); +} + +static const rc_client_user_t* rc_client_external_get_user_info_v1_long_name(void) +{ + v1_rc_client_user_t* user = (v1_rc_client_user_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_user_t)); + + memset(user, 0, sizeof(*user)); + user->display_name = "TwentyCharUserNameXX"; + user->username = "TwentyCharUserNameXX"; + user->token = "ApiToken"; + user->score = 12345; + user->score_softcore = 123; + user->num_unread_messages = 2; + + return (rc_client_user_t*)user; +} + +static rc_client_async_handle_t* rc_client_external_login_with_token_long_name(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + g_external_event = "login"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static void test_login_with_token_v1_long_username(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token_long_name; + g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1_long_name; + + rc_client_begin_login_with_token(g_client, "TwentyCharUserNameXX", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "login"); + + /* user data should come from external client. validate structure */ + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "TwentyCharUserNameXX"); + ASSERT_STR_EQUALS(user->display_name, "TwentyCharUserNameXX"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + ASSERT_STR_EQUALS(user->avatar_url, "https://media.retroachievements.org/UserPic/TwentyCharUserNameXX.png"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->user.username); + + rc_client_destroy(g_client); +} + +static const rc_client_user_t* rc_client_external_get_user_info_v1_too_long_name(void) +{ + v1_rc_client_user_t* user = (v1_rc_client_user_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_user_t)); + + memset(user, 0, sizeof(*user)); + user->display_name = "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"; + user->username = "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"; + user->token = "ApiToken"; + user->score = 12345; + user->score_softcore = 123; + user->num_unread_messages = 2; + + return (rc_client_user_t*)user; +} + +static void test_login_with_token_v1_too_long_username(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token_long_name; + g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1_too_long_name; + + rc_client_begin_login_with_token(g_client, "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "login"); + + /* user data should come from external client. validate structure */ + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"); + ASSERT_STR_EQUALS(user->display_name, "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + /* overly long URL will be truncated, but should not cause an exception. + * test_login_with_token_v1_long_username validates the longest allowed username, so this shouldn't occur anyway */ + ASSERT_STR_EQUALS(user->avatar_url, "https://media.retroachievements.org/UserPic/ThisUserNameIsTooLongToFitIntoTheUs"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->user.username); + + rc_client_destroy(g_client); +} + +static void test_login_with_token(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token; + g_client->state.external_client->get_user_info_v3 = rc_client_external_get_user_info_v3; + + rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "login"); + + /* user data should come from external client. validate structure */ + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + ASSERT_STR_EQUALS(user->avatar_url, "/UserPic/User.png"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->user.username); + + rc_client_destroy(g_client); +} + +static void rc_client_external_logout(void) +{ + g_external_event = "logout"; +} + +static void test_logout(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->logout = rc_client_external_logout; + + /* external client should maintain its own state, but use the singular instance*/ + g_client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + + rc_client_logout(g_client); + ASSERT_STR_EQUALS(g_external_event, "logout"); + + /* ensure non-external client user was not modified */ + ASSERT_NUM_EQUALS(g_client->state.user, RC_CLIENT_USER_STATE_LOGGED_IN); + + rc_client_destroy(g_client); +} + +/* ----- load game ----- */ + +static void test_v1_game_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, id); + ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, console_id); + ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, title); + ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, hash); + ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, badge_name); +} + +static void test_v3_game_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, id); + ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, console_id); + ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, title); + ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, hash); + ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, badge_name); + ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, badge_url); +} + +static const rc_client_game_t* rc_client_external_get_game_info_v1(void) +{ + v1_rc_client_game_t* game = (v1_rc_client_game_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_game_t)); + + memset(game, 0, sizeof(*game)); + game->id = 1234; + game->console_id = RC_CONSOLE_PLAYSTATION; + game->title = "Game Title"; + game->hash = "GAME_HASH"; + game->badge_name = "BDG001"; + + return (const rc_client_game_t*)game; +} + +static const rc_client_game_t* rc_client_external_get_game_info_v1_not_found(void) +{ + return NULL; +} + +static const rc_client_game_t* rc_client_external_get_game_info_v3(void) +{ + v3_rc_client_game_t* game = (v3_rc_client_game_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_game_t)); + + memset(game, 0, sizeof(*game)); + game->id = 1234; + game->console_id = RC_CONSOLE_PLAYSTATION; + game->title = "Game Title"; + game->hash = "GAME_HASH"; + game->badge_name = "BDG001"; + game->badge_url = "/Badge/BDG001.png"; + + return (const rc_client_game_t*)game; +} + +#ifdef RC_CLIENT_SUPPORTS_HASH + +static void assert_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size) +{ + ASSERT_PTR_EQUALS(client, g_client); + + ASSERT_NUM_EQUALS(console_id, RC_CONSOLE_GAMEBOY); + ASSERT_STR_EQUALS(file_path, "foo.zip#foo.gb"); + ASSERT_PTR_NOT_NULL(data); + ASSERT_NUM_EQUALS(32768, data_size); +} + +static rc_client_async_handle_t* rc_client_external_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) +{ + assert_identify_and_load_game(client, console_id, file_path, data, data_size); + + g_external_event = "load_game"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static void test_identify_and_load_game_v1(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_identify_and_load_game = rc_client_external_identify_and_load_game; + g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "https://media.retroachievements.org/Images/BDG001.png"); + + /* ensure non-external client game was not initialized */ + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_identify_and_load_game = rc_client_external_identify_and_load_game; + g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; + g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + /* ensure non-external client game was not initialized */ + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_reload_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_identify_and_load_game = rc_client_external_identify_and_load_game; + g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; + g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; + g_client->state.external_client->unload_game = rc_client_external_unload_game; + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + /* ensure non-external client game was not initialized */ + ASSERT_PTR_NULL(g_client->game); + + rc_client_unload_game(g_client); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + /* ensure non-external client game was not initialized */ + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +#endif /* RC_CLIENT_SUPPORTS_HASH */ + +static void assert_load_game(rc_client_t* client, const char* hash) +{ + ASSERT_PTR_EQUALS(client, g_client); + + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); +} + +static rc_client_async_handle_t* rc_client_external_load_game(rc_client_t* client, + const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + assert_load_game(client, hash); + + g_external_event = "load_game"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static void test_load_game_v1(void) +{ + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_load_game = rc_client_external_load_game; + g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; + + rc_client_begin_load_game(g_client, "6a2305a2b6675a97ff792709be1ca857", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); + + /* game data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "https://media.retroachievements.org/Images/BDG001.png"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game(void) +{ + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_load_game = rc_client_external_load_game; + g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; + g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; + + rc_client_begin_load_game(g_client, "6a2305a2b6675a97ff792709be1ca857", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); + + /* game data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + + /* ensure non-external client user was not initialized */ + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_get_game_info_v1_no_game_loaded(void) +{ + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1_not_found; + + /* game data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NULL(game); + + rc_client_destroy(g_client); +} + +static void test_v1_user_game_summary_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_core_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_unofficial_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_unlocked_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_unsupported_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, points_core); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, points_unlocked); +} + +static void test_v5_user_game_summary_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_core_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_unofficial_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_unlocked_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_unsupported_achievements); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, points_core); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, points_unlocked); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, beaten_time); + ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, completed_time); +} + +static void rc_client_external_get_user_game_summary(rc_client_user_game_summary_t* summary) +{ + summary->num_core_achievements = 20; + summary->num_unlocked_achievements = 6; + summary->num_unofficial_achievements = 3; + summary->num_unsupported_achievements = 1; + summary->points_core = 100; + summary->points_unlocked = 23; +} + +static void rc_client_external_get_user_game_summary_v5(rc_client_user_game_summary_t* summary) +{ + summary->num_core_achievements = 20; + summary->num_unlocked_achievements = 6; + summary->num_unofficial_achievements = 3; + summary->num_unsupported_achievements = 1; + summary->points_core = 100; + summary->points_unlocked = 23; + summary->beaten_time = 1234567890; + summary->completed_time = 1234598760; +} + +static void test_get_user_game_summary(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_user_game_summary = rc_client_external_get_user_game_summary; + + rc_client_get_user_game_summary(g_client, &summary); + + ASSERT_NUM_EQUALS(summary.num_core_achievements, 20); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 6); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 3); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.points_core, 100); + ASSERT_NUM_EQUALS(summary.points_unlocked, 23); + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_v5(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_user_game_summary_v5 = rc_client_external_get_user_game_summary_v5; + + rc_client_get_user_game_summary(g_client, &summary); + + ASSERT_NUM_EQUALS(summary.num_core_achievements, 20); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 6); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 3); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.points_core, 100); + ASSERT_NUM_EQUALS(summary.points_unlocked, 23); + ASSERT_NUM_EQUALS(summary.beaten_time, 1234567890); + ASSERT_NUM_EQUALS(summary.completed_time, 1234598760); + + rc_client_destroy(g_client); +} + +static void rc_client_external_get_user_subset_summary(uint32_t subset_id, rc_client_user_game_summary_t* summary) +{ + if (subset_id == 6) { + summary->num_core_achievements = 20; + summary->num_unlocked_achievements = 6; + summary->num_unofficial_achievements = 3; + summary->num_unsupported_achievements = 1; + summary->points_core = 100; + summary->points_unlocked = 23; + summary->beaten_time = 1234567890; + summary->completed_time = 1234598760; + } +} + +static void test_get_user_subset_summary(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_user_subset_summary = rc_client_external_get_user_subset_summary; + + rc_client_get_user_subset_summary(g_client, 1, &summary); + + ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.points_core, 0); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + ASSERT_NUM_EQUALS(summary.beaten_time, 0); + ASSERT_NUM_EQUALS(summary.completed_time, 0); + + rc_client_get_user_subset_summary(g_client, 6, &summary); + + ASSERT_NUM_EQUALS(summary.num_core_achievements, 20); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 6); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 3); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.points_core, 100); + ASSERT_NUM_EQUALS(summary.points_unlocked, 23); + ASSERT_NUM_EQUALS(summary.beaten_time, 1234567890); + ASSERT_NUM_EQUALS(summary.completed_time, 1234598760); + + rc_client_destroy(g_client); +} + +#ifdef RC_CLIENT_SUPPORTS_HASH + +static void test_identify_and_load_game_external_hash(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->add_game_hash = rc_client_external_add_game_hash; + g_client->state.external_client->begin_load_game = rc_client_external_load_game; + g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + g_external_int = 0; + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); /* begin_load_game called */ + ASSERT_NUM_EQUALS(g_external_int, 1234); /* add_game_hash called */ + ASSERT_PTR_NULL(g_client->state.load); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + + /* ensure internal client game was initialized to hold media hashes */ + ASSERT_PTR_NOT_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_reload_game_external_hash(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + const rc_client_game_t* game; + + g_client = mock_client_with_external(); + g_client->state.external_client->add_game_hash = rc_client_external_add_game_hash; + g_client->state.external_client->begin_load_game = rc_client_external_load_game; + g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; + g_client->state.external_client->unload_game = rc_client_external_unload_game; + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + g_external_int = 0; + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); /* begin_load_game called */ + ASSERT_NUM_EQUALS(g_external_int, 1234); /* add_game_hash called */ + ASSERT_PTR_NULL(g_client->state.load); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + + /* ensure internal client game was initialized to hold media hashes */ + ASSERT_PTR_NOT_NULL(g_client->game); + + rc_client_unload_game(g_client); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "load_game"); /* begin_load_game called */ + ASSERT_NUM_EQUALS(g_external_int, 1234); /* add_game_hash called */ + ASSERT_PTR_NULL(g_client->state.load); + + /* user data should come from external client. validate structure */ + game = rc_client_get_game_info(g_client); + ASSERT_PTR_NOT_NULL(game); + ASSERT_NUM_EQUALS(game->id, 1234); + ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); + ASSERT_STR_EQUALS(game->title, "Game Title"); + ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); + ASSERT_STR_EQUALS(game->badge_name, "BDG001"); + ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); + + /* ensure internal client game was initialized to hold media hashes */ + ASSERT_PTR_NOT_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void assert_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size) +{ + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_STR_EQUALS(file_path, "foo.zip#foo.gb"); + ASSERT_PTR_NOT_NULL(data); + ASSERT_NUM_EQUALS(data_size, 32768); +} + +static rc_client_async_handle_t* rc_client_external_begin_identify_and_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) +{ + assert_change_media(client, file_path, data, data_size); + + g_external_event = "change_media"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static void test_change_media(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_with_external(); + g_client->state.external_client->begin_identify_and_change_media = rc_client_external_begin_identify_and_change_media; + + rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "change_media"); + + rc_client_destroy(g_client); + free(image); +} + +#endif + +static void assert_change_media_from_hash(rc_client_t* client, const char* hash) +{ + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); +} + +static rc_client_async_handle_t* rc_client_external_begin_change_media(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata) +{ + assert_change_media_from_hash(client, hash); + + g_external_event = "change_media_from_hash"; + + callback(RC_OK, NULL, client, callback_userdata); + return NULL; +} + +static void test_change_media_from_hash(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->begin_change_media = rc_client_external_begin_change_media; + + rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_external_event, "change_media_from_hash"); + + rc_client_destroy(g_client); +} + +static const rc_client_subset_t* rc_client_external_get_subset_info_v1(uint32_t subset_id) +{ + v1_rc_client_subset_t* subset = (v1_rc_client_subset_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_subset_t)); + + memset(subset, 0, sizeof(*subset)); + subset->id = subset_id; + subset->title = "Subset Title"; + snprintf(subset->badge_name, sizeof(subset->badge_name), "%s", "BDG001"); + subset->num_achievements = 2; + subset->num_leaderboards = 1; + + return (const rc_client_subset_t*)subset; +} + +static const rc_client_subset_t* rc_client_external_get_subset_info_v3(uint32_t subset_id) +{ + v3_rc_client_subset_t* subset = (v3_rc_client_subset_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_subset_t)); + + memset(subset, 0, sizeof(*subset)); + subset->id = subset_id; + subset->title = "Subset Title"; + snprintf(subset->badge_name, sizeof(subset->badge_name), "%s", "BDG001"); + subset->num_achievements = 2; + subset->num_leaderboards = 1; + subset->badge_url = "/Badge/BDG001.png"; + + return (const rc_client_subset_t*)subset; +} + +static void test_v1_subset_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, id); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, title); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, badge_name); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, num_achievements); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, num_leaderboards); +} + +static void test_v3_subset_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, id); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, title); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, badge_name); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, num_achievements); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, num_leaderboards); + ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, badge_url); +} + +static void test_get_subset_info_v1(void) +{ + const rc_client_subset_t* subset; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_subset_info = rc_client_external_get_subset_info_v1; + + /* subset data should come from external client. validate structure */ + subset = rc_client_get_subset_info(g_client, 1234); + ASSERT_PTR_NOT_NULL(subset); + ASSERT_NUM_EQUALS(subset->id, 1234); + ASSERT_STR_EQUALS(subset->title, "Subset Title"); + ASSERT_STR_EQUALS(subset->badge_name, "BDG001"); + ASSERT_NUM_EQUALS(subset->num_achievements, 2); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 1); + ASSERT_STR_EQUALS(subset->badge_url, "https://media.retroachievements.org/Images/BDG001.png"); + + rc_client_destroy(g_client); +} + +static void test_get_subset_info(void) +{ + const rc_client_subset_t* subset; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_subset_info_v3 = rc_client_external_get_subset_info_v3; + + /* subset data should come from external client. validate structure */ + subset = rc_client_get_subset_info(g_client, 1234); + ASSERT_PTR_NOT_NULL(subset); + ASSERT_NUM_EQUALS(subset->id, 1234); + ASSERT_STR_EQUALS(subset->title, "Subset Title"); + ASSERT_STR_EQUALS(subset->badge_name, "BDG001"); + ASSERT_NUM_EQUALS(subset->num_achievements, 2); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 1); + ASSERT_STR_EQUALS(subset->badge_url, "/Badge/BDG001.png"); + + rc_client_destroy(g_client); +} + +static void rc_client_external_destroy_subset_list(rc_client_subset_list_info_t* list) +{ + g_external_event = "destroyed"; + free(list); +} + +static rc_client_subset_list_info_t* rc_client_external_create_subset_list_v6() +{ + rc_client_subset_list_info_t* list; + + list = (rc_client_subset_list_info_t*)calloc(1, sizeof(*list) + sizeof(rc_client_subset_t*) * 2); + if (list) { + const rc_client_subset_t** subset; + rc_client_subset_t* mutable_subset; + list->public_.num_subsets = 2; + list->public_.subsets = subset = (const rc_client_subset_t**)((uint8_t*)list + sizeof(*list)); + *subset++ = rc_client_external_get_subset_info_v3(1111); + *subset = rc_client_external_get_subset_info_v3(2345); + mutable_subset = (rc_client_subset_t*)*subset; + mutable_subset->title = "Bonus"; + mutable_subset->num_achievements = 1; + mutable_subset->num_leaderboards = 0; + + list->destroy_func = rc_client_external_destroy_subset_list; + } + + return list; +} + +static void test_create_subset_list(void) +{ + rc_client_subset_list_t* list; + + g_client = mock_client_with_external(); + g_client->state.external_client->create_subset_list = rc_client_external_create_subset_list_v6; + + list = rc_client_create_subset_list(g_client); + ASSERT_PTR_NOT_NULL(list); + ASSERT_NUM_EQUALS(list->num_subsets, 2); + ASSERT_PTR_NOT_NULL(list->subsets); + ASSERT_NUM_EQUALS(list->subsets[0]->id, 1111); + ASSERT_STR_EQUALS(list->subsets[0]->title, "Subset Title"); + ASSERT_STR_EQUALS(list->subsets[0]->badge_name, "BDG001"); + ASSERT_NUM_EQUALS(list->subsets[0]->num_achievements, 2); + ASSERT_NUM_EQUALS(list->subsets[0]->num_leaderboards, 1); + ASSERT_STR_EQUALS(list->subsets[0]->badge_url, "/Badge/BDG001.png"); + ASSERT_NUM_EQUALS(list->subsets[1]->id, 2345); + ASSERT_STR_EQUALS(list->subsets[1]->title, "Bonus"); + ASSERT_STR_EQUALS(list->subsets[1]->badge_name, "BDG001"); + ASSERT_NUM_EQUALS(list->subsets[1]->num_achievements, 1); + ASSERT_NUM_EQUALS(list->subsets[1]->num_leaderboards, 0); + ASSERT_STR_EQUALS(list->subsets[1]->badge_url, "/Badge/BDG001.png"); + + rc_client_destroy_subset_list(list); + + ASSERT_STR_EQUALS(g_external_event, "destroyed"); + + rc_client_destroy(g_client); +} + +static void test_unload_game(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->unload_game = rc_client_external_unload_game; + + rc_client_unload_game(g_client); + + ASSERT_STR_EQUALS(g_external_event, "unload_game"); + + rc_client_destroy(g_client); +} + +/* ----- achievements ----- */ + +static void test_v1_achievement_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, id); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, description); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, badge_name); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, measured_progress); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, measured_percent); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, id); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, points); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, unlock_time); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, state); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, category); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, bucket); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, unlocked); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, rarity); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, rarity_hardcore); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, type); +} + +static void test_v3_achievement_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, id); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, description); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_name); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, measured_progress); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, measured_percent); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, id); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, points); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, unlock_time); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, state); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, category); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, bucket); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, unlocked); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, rarity); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, rarity_hardcore); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, type); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_url); + ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_locked_url); +} + +static const rc_client_achievement_t* rc_client_external_get_achievement_info_v1(uint32_t id) +{ + v1_rc_client_achievement_t* achievement = (v1_rc_client_achievement_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_achievement_t)); + + memset(achievement, 0, sizeof(*achievement)); + achievement->id = id; + achievement->title = "Achievement Title"; + achievement->description = "Do something cool"; + memcpy(achievement->badge_name, "BDG1234", 8); + achievement->measured_percent = 33.5; + achievement->points = 5; + achievement->state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + achievement->category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + achievement->bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + achievement->unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; + achievement->rarity = 75.0f; + achievement->rarity_hardcore = 66.66f; + achievement->type = RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE; + + return (const rc_client_achievement_t*)achievement; +} + +static const rc_client_achievement_t* rc_client_external_get_achievement_info_v1_not_found(uint32_t id) +{ + return NULL; +} + +static const rc_client_achievement_t* rc_client_external_get_achievement_info_v3(uint32_t id) +{ + v3_rc_client_achievement_t* achievement = (v3_rc_client_achievement_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_achievement_t)); + + memset(achievement, 0, sizeof(*achievement)); + achievement->id = id; + achievement->title = "Achievement Title"; + achievement->description = "Do something cool"; + memcpy(achievement->badge_name, "BDG1234", 8); + achievement->measured_percent = 33.5; + achievement->points = 5; + achievement->state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + achievement->category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + achievement->bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + achievement->unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; + achievement->rarity = 75.0f; + achievement->rarity_hardcore = 66.66f; + achievement->type = RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE; + achievement->badge_url = "/Badge/000234.png"; + achievement->badge_locked_url = "/Badge/000234_locked.png"; + return (const rc_client_achievement_t*)achievement; +} + +static void test_has_achievements(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->has_achievements = rc_client_external_get_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_has_achievements(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_has_achievements(g_client), 1); + + rc_client_destroy(g_client); +} + +static void test_get_achievement_info_v1(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_achievement_info = rc_client_external_get_achievement_info_v1; + + achievement = rc_client_get_achievement_info(g_client, 4); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->id, 4); + ASSERT_STR_EQUALS(achievement->title, "Achievement Title"); + ASSERT_STR_EQUALS(achievement->description, "Do something cool"); + ASSERT_STR_EQUALS(achievement->badge_name, "BDG1234"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.5); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 75.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.66f); + ASSERT_NUM_EQUALS(achievement->type, RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/BDG1234.png"); + ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/BDG1234_lock.png"); + + rc_client_destroy(g_client); +} + +static void test_get_achievement_info_v1_not_found(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_achievement_info = rc_client_external_get_achievement_info_v1_not_found; + + achievement = rc_client_get_achievement_info(g_client, 4); + ASSERT_PTR_NULL(achievement); + + rc_client_destroy(g_client); +} + +static void test_get_achievement_info(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_achievement_info = rc_client_external_get_achievement_info_v1; + g_client->state.external_client->get_achievement_info_v3 = rc_client_external_get_achievement_info_v3; + + achievement = rc_client_get_achievement_info(g_client, 4); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->id, 4); + ASSERT_STR_EQUALS(achievement->title, "Achievement Title"); + ASSERT_STR_EQUALS(achievement->description, "Do something cool"); + ASSERT_STR_EQUALS(achievement->badge_name, "BDG1234"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.5); + ASSERT_NUM_EQUALS(achievement->points, 5); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_FLOAT_EQUALS(achievement->rarity, 75.0f); + ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.66f); + ASSERT_NUM_EQUALS(achievement->type, RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE); + ASSERT_STR_EQUALS(achievement->badge_url, "/Badge/000234.png"); + ASSERT_STR_EQUALS(achievement->badge_locked_url, "/Badge/000234_locked.png"); + + rc_client_destroy(g_client); +} + +static void test_v1_achievement_list_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v1_rc_client_achievement_list_info_t, public_); + ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v1_rc_client_achievement_list_info_t, destroy_func); + + ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v1_rc_client_achievement_list_t, buckets); + ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v1_rc_client_achievement_list_t, num_buckets); + + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, achievements); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, num_achievements); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, label); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, subset_id); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, bucket_type); +} + +static void test_v3_achievement_list_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v3_rc_client_achievement_list_info_t, public_); + ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v3_rc_client_achievement_list_info_t, destroy_func); + + ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v3_rc_client_achievement_list_t, buckets); + ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v3_rc_client_achievement_list_t, num_buckets); + + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, achievements); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, num_achievements); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, label); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, subset_id); + ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, bucket_type); +} + +static void assert_achievement_list_category_grouping(int category, int grouping) +{ + ASSERT_NUM_EQUALS(category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_NUM_EQUALS(grouping, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); +} + +static void rc_client_external_destroy_achievement_list(rc_client_achievement_list_info_t* list) +{ + g_external_event = "destroyed"; + free(list); +} + +static rc_client_achievement_list_info_t* rc_client_external_create_achievement_list_v1(int category, int grouping) +{ + v1_rc_client_achievement_list_info_t* list; + + assert_achievement_list_category_grouping(category, grouping); + + list = (v1_rc_client_achievement_list_info_t*)calloc(1, sizeof(*list) + sizeof(v1_rc_client_achievement_bucket_t) + sizeof(v1_rc_client_achievement_t*) * 2); + if (list) { + list->public_.num_buckets = 1; + list->public_.buckets = (v1_rc_client_achievement_bucket_t*)((uint8_t*)list + sizeof(*list)); + list->public_.buckets[0].achievements = (v1_rc_client_achievement_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); + list->public_.buckets[0].achievements[0] = (v1_rc_client_achievement_t*)rc_client_external_get_achievement_info_v1(1234); + list->public_.buckets[0].achievements[1] = (v1_rc_client_achievement_t*)rc_client_external_get_achievement_info_v1(1235); + list->public_.buckets[0].num_achievements = 2; + list->public_.buckets[0].bucket_type = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + list->public_.buckets[0].label = "Locked"; + list->public_.buckets[0].subset_id = 1234; + + list->destroy_func = rc_client_external_destroy_achievement_list; + } + + return (rc_client_achievement_list_info_t*)list; +} + +static rc_client_achievement_list_info_t* rc_client_external_create_achievement_list_v3(int category, int grouping) +{ + v3_rc_client_achievement_list_info_t* list; + + assert_achievement_list_category_grouping(category, grouping); + + list = (v3_rc_client_achievement_list_info_t*)calloc(1, sizeof(*list) + sizeof(v3_rc_client_achievement_bucket_t) + sizeof(v3_rc_client_achievement_t*) * 2); + if (list) { + v3_rc_client_achievement_bucket_t* bucket; + list->public_.num_buckets = 1; + list->public_.buckets = bucket = (v3_rc_client_achievement_bucket_t*)((uint8_t*)list + sizeof(*list)); + bucket->achievements = (const v3_rc_client_achievement_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); + bucket->achievements[0] = (const v3_rc_client_achievement_t*)rc_client_external_get_achievement_info_v3(1234); + bucket->achievements[1] = (const v3_rc_client_achievement_t*)rc_client_external_get_achievement_info_v3(1235); + bucket->num_achievements = 2; + bucket->bucket_type = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + bucket->label = "Locked"; + bucket->subset_id = 1234; + + list->destroy_func = rc_client_external_destroy_achievement_list; + } + + return (rc_client_achievement_list_info_t*)list; +} + +static void test_create_achievement_list_v1(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_with_external(); + g_client->state.external_client->create_achievement_list = rc_client_external_create_achievement_list_v1; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_PTR_NOT_NULL(list->buckets); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 1234); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 1235); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_url, "https://media.retroachievements.org/Badge/BDG1234.png"); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_locked_url, "https://media.retroachievements.org/Badge/BDG1234_lock.png"); + + rc_client_destroy_achievement_list(list); + + ASSERT_STR_EQUALS(g_external_event, "destroyed"); + + rc_client_destroy(g_client); +} + +static void test_create_achievement_list(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_with_external(); + g_client->state.external_client->create_achievement_list = rc_client_external_create_achievement_list_v1; + g_client->state.external_client->create_achievement_list_v3 = rc_client_external_create_achievement_list_v3; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_PTR_NOT_NULL(list->buckets); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 1234); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 1235); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + + // only difference between v1 and v3 create_achievement_list is the badge_url/badge_unlocked_url fields on each achievement + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_url, "/Badge/000234.png"); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_locked_url, "/Badge/000234_locked.png"); + + rc_client_destroy_achievement_list(list); + + ASSERT_STR_EQUALS(g_external_event, "destroyed"); + + rc_client_destroy(g_client); +} + +/* ----- leaderboards ----- */ + +typedef struct v1_rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t format; + uint8_t lower_is_better; +} v1_rc_client_leaderboard_t; + +static const rc_client_leaderboard_t* rc_client_external_get_leaderboard_info(uint32_t id) +{ + v1_rc_client_leaderboard_t* leaderboard = (v1_rc_client_leaderboard_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_leaderboard_t)); + + memset(leaderboard, 0, sizeof(*leaderboard)); + leaderboard->id = 1234; + leaderboard->title = "Leaderboard Title"; + leaderboard->description = "Do something cool"; + leaderboard->tracker_value = "000250"; + leaderboard->state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->format = RC_CLIENT_LEADERBOARD_FORMAT_SCORE; + leaderboard->lower_is_better = 1; + + return (const rc_client_leaderboard_t*)leaderboard; +} + +typedef struct v1_rc_client_leaderboard_bucket_t { + rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v1_rc_client_leaderboard_bucket_t; + +typedef struct v1_rc_client_leaderboard_list_t { + v1_rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} v1_rc_client_leaderboard_list_t; + +typedef struct v1_rc_client_leaderboard_list_info_t { + v1_rc_client_leaderboard_list_t public_; + rc_client_destroy_leaderboard_list_func_t destroy_func; +} v1_rc_client_leaderboard_list_info_t; + +static void assert_leaderboard_list_grouping(int grouping) +{ + ASSERT_NUM_EQUALS(grouping, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); +} + +static void rc_client_external_destroy_leaderboard_list(rc_client_leaderboard_list_info_t* list) +{ + g_external_event = "destroyed"; + free(list); +} + +static rc_client_leaderboard_list_info_t* rc_client_external_create_leaderboard_list(int grouping) +{ + v1_rc_client_leaderboard_list_info_t* list; + + assert_leaderboard_list_grouping(grouping); + + list = (v1_rc_client_leaderboard_list_info_t*)calloc(1, sizeof(*list) + sizeof(v1_rc_client_leaderboard_bucket_t)); + if (list) { + list->public_.num_buckets = 1; + list->public_.buckets = (v1_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); + list->public_.buckets[0].num_leaderboards = 2; /* didn't actually allocate these */ + list->public_.buckets[0].bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + list->public_.buckets[0].label = "Inactive"; + list->public_.buckets[0].subset_id = 1234; + + list->destroy_func = rc_client_external_destroy_leaderboard_list; + } + + return (rc_client_leaderboard_list_info_t*)list; +} + +static void test_create_leaderboard_list(void) +{ + rc_client_leaderboard_list_t* list; + + g_client = mock_client_with_external(); + g_client->state.external_client->create_leaderboard_list = rc_client_external_create_leaderboard_list; + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_PTR_NOT_NULL(list->buckets); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + + rc_client_destroy_leaderboard_list(list); + + rc_client_destroy(g_client); +} + +static void test_has_leaderboards(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->has_leaderboards = rc_client_external_get_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 1); + + rc_client_destroy(g_client); +} + +static void test_get_leaderboard_info(void) +{ + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info; + + leaderboard = rc_client_get_leaderboard_info(g_client, 4); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->id, 1234); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); + ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); + ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + + rc_client_destroy(g_client); +} + +/* ----- rich presence ----- */ + +static size_t rc_client_external_get_rich_presence_message(char buffer[], size_t buffer_size) +{ + size_t result = snprintf(buffer, buffer_size, "Playing Game Title"); + if (result >= buffer_size) + return (buffer_size - 1); + return result; +} + +static void test_get_rich_presence_message(void) +{ + char buffer[16]; + size_t result; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_rich_presence_message = rc_client_external_get_rich_presence_message; + + result = rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)); + + ASSERT_STR_EQUALS(buffer, "Playing Game Ti"); + ASSERT_NUM_EQUALS(result, 15); + + rc_client_destroy(g_client); +} + +static void test_has_rich_presence(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->has_rich_presence = rc_client_external_get_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_has_rich_presence(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_has_rich_presence(g_client), 1); + + rc_client_destroy(g_client); +} + +/* ----- processing ----- */ + +static void test_is_processing_required(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->is_processing_required = rc_client_external_get_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_is_processing_required(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_is_processing_required(g_client), 1); + + rc_client_destroy(g_client); +} + +static void rc_client_external_do_frame(void) +{ + g_external_event = "do_frame"; +} + +static void test_do_frame(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->do_frame = rc_client_external_do_frame; + + rc_client_do_frame(g_client); + + ASSERT_STR_EQUALS(g_external_event, "do_frame"); + + rc_client_destroy(g_client); +} + +static void rc_client_external_idle(void) +{ + g_external_event = "idle"; +} + +static void test_idle(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->idle = rc_client_external_idle; + + rc_client_idle(g_client); + + ASSERT_STR_EQUALS(g_external_event, "idle"); + + rc_client_destroy(g_client); +} + +static void rc_client_external_reset(void) +{ + g_external_event = "reset"; +} + +static int rc_client_external_can_pause(uint32_t* frames_remaining) +{ + *frames_remaining = g_external_int ? 0 : 10; + + return g_external_int; +} + +static void test_can_pause(void) +{ + uint32_t frames_remaining; + g_client = mock_client_with_external(); + g_client->state.external_client->can_pause = rc_client_external_can_pause; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); + ASSERT_NUM_EQUALS(frames_remaining, 10); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); + ASSERT_NUM_EQUALS(frames_remaining, 0); + + rc_client_destroy(g_client); +} + +static void test_reset(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->reset = rc_client_external_reset; + + rc_client_reset(g_client); + + ASSERT_STR_EQUALS(g_external_event, "reset"); + + rc_client_destroy(g_client); +} + +/* ----- progress ----- */ + +static size_t rc_client_external_progress_size(void) +{ + return 12345678; +} + +static void test_progress_size(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->progress_size = rc_client_external_progress_size; + + ASSERT_NUM_EQUALS(rc_client_progress_size(g_client), 12345678); + + rc_client_destroy(g_client); +} + +static int rc_client_external_serialize_progress(uint8_t* buffer, size_t size) +{ + memcpy(buffer, "SAVED", 6); + + g_external_event = "serialize_progress"; + + return RC_OK; +} + +static void test_serialize_progress(void) +{ + int result; + uint8_t buffer[8] = { 0 }; + + g_client = mock_client_with_external(); + g_client->state.external_client->serialize_progress = rc_client_external_serialize_progress; + + result = rc_client_serialize_progress(g_client, buffer); + + ASSERT_STR_EQUALS(g_external_event, "serialize_progress"); + ASSERT_STR_EQUALS(buffer, "SAVED"); + ASSERT_NUM_EQUALS(result, RC_OK); + + rc_client_destroy(g_client); +} + +static int rc_client_external_deserialize_progress(const uint8_t* buffer, size_t size) +{ + if (memcmp(buffer, "SAVE", 5) == 0) + g_external_event = "deserialize_progress"; + + return RC_OK; +} + +static void test_deserialize_progress(void) +{ + int result; + uint8_t buffer[8] = {'S', 'A', 'V', 'E'}; + + g_client = mock_client_with_external(); + g_client->state.external_client->deserialize_progress = rc_client_external_deserialize_progress; + + result = rc_client_deserialize_progress(g_client, buffer); + + ASSERT_STR_EQUALS(g_external_event, "deserialize_progress"); + ASSERT_NUM_EQUALS(result, RC_OK); + + rc_client_destroy(g_client); +} + +/* ----- harness ----- */ + +void test_client_external(void) { + TEST_SUITE_BEGIN(); + + /* settings */ + TEST(test_hardcore_enabled); + TEST(test_unofficial_enabled); + TEST(test_encore_mode_enabled); + TEST(test_spectator_mode_enabled); + TEST(test_enable_logging); + TEST(test_event_handler); + TEST(test_read_memory); + TEST(test_get_time_millisecs); + TEST(test_set_host); + TEST(test_get_user_agent_clause); + TEST(test_set_allow_background_memory_reads); + + /* login */ + TEST(test_v1_user_field_offsets); + TEST(test_v3_user_field_offsets); + TEST(test_login_with_password); + TEST(test_login_with_token_v1); + TEST(test_login_with_token_v1_long_username); + TEST(test_login_with_token_v1_too_long_username); + TEST(test_login_with_token); + + TEST(test_logout); + + /* load game */ + TEST(test_v1_game_field_offsets); + TEST(test_v3_game_field_offsets); + +#ifdef RC_CLIENT_SUPPORTS_HASH + TEST(test_identify_and_load_game_v1); + TEST(test_identify_and_load_game); + TEST(test_identify_and_reload_game); +#endif + TEST(test_load_game_v1); + TEST(test_load_game); + TEST(test_get_game_info_v1_no_game_loaded); + TEST(test_v1_user_game_summary_field_offsets); + TEST(test_v5_user_game_summary_field_offsets); + TEST(test_get_user_game_summary); + TEST(test_get_user_game_summary_v5); + TEST(test_get_user_subset_summary); +#ifdef RC_CLIENT_SUPPORTS_HASH + TEST(test_identify_and_load_game_external_hash); + TEST(test_identify_and_reload_game_external_hash); + TEST(test_change_media); +#endif + TEST(test_change_media_from_hash); + TEST(test_add_game_hash); + + TEST(test_unload_game); + + /* subsets */ + TEST(test_v1_subset_field_offsets); + TEST(test_v3_subset_field_offsets); + TEST(test_get_subset_info_v1); + TEST(test_get_subset_info); + TEST(test_create_subset_list); + + /* achievements */ + TEST(test_v1_achievement_field_offsets); + TEST(test_v3_achievement_field_offsets); + TEST(test_has_achievements); + TEST(test_get_achievement_info_v1); + TEST(test_get_achievement_info_v1_not_found); + TEST(test_get_achievement_info); + + TEST(test_v1_achievement_list_field_offsets); + TEST(test_v3_achievement_list_field_offsets); + TEST(test_create_achievement_list_v1); + TEST(test_create_achievement_list); + + /* leaderboards */ + TEST(test_create_leaderboard_list); + TEST(test_has_leaderboards); + TEST(test_get_leaderboard_info); + + /* rich presence */ + TEST(test_get_rich_presence_message); + TEST(test_has_rich_presence); + + /* processing */ + TEST(test_is_processing_required); + TEST(test_do_frame); + TEST(test_idle); + TEST(test_can_pause); + TEST(test_reset); + + /* progress */ + TEST(test_progress_size); + TEST(test_serialize_progress); + TEST(test_deserialize_progress); + + TEST_SUITE_END(); +} + +#endif /* RC_CLIENT_SUPPORTS_EXTERNAL */ diff --git a/src/rcheevos/test/test_rc_client_raintegration.c b/src/rcheevos/test/test_rc_client_raintegration.c new file mode 100644 index 0000000000..8308c32a36 --- /dev/null +++ b/src/rcheevos/test/test_rc_client_raintegration.c @@ -0,0 +1,441 @@ +#include "rc_client.h" + +#include "rc_consoles.h" +#include "rc_hash.h" +#include "rc_internal.h" +#include "rc_api_runtime.h" + +#include "../src/rc_client_internal.h" +#include "../src/rc_version.h" + +#include "rhash/data.h" +#include "test_framework.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +static rc_client_t* g_client; +static const char* g_integration_event; +static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ + +/* begin from test_rc_client.c */ + +extern void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); +extern void rc_client_server_call_async(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); +extern void reset_mock_api_handlers(void); +extern void mock_api_response(const char* request_params, const char* response_body); +extern void mock_api_error(const char* request_params, const char* response_body, int http_status_code); +extern void async_api_response(const char* request_params, const char* response_body); +extern void async_api_error(const char* request_params, const char* response_body, int http_status_code); + +/* end from test_rc_client.c */ + +static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + return 0; +} + +static rc_client_t* mock_client_with_integration() +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + client->state.raintegration = (rc_client_raintegration_t*) + rc_buffer_alloc(&client->state.buffer, sizeof(*client->state.raintegration)); + memset(client->state.raintegration, 0, sizeof(*client->state.raintegration)); + + rc_api_set_host(NULL); + reset_mock_api_handlers(); + g_integration_event = "none"; + + return client; +} + +static rc_client_t* mock_client_with_integration_async() +{ + rc_client_t* client = mock_client_with_integration(); + client->callbacks.server_call = rc_client_server_call_async; + return client; +} + +static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called."); +} + +static uint32_t g_uint32; + +static void rc_client_integration_set_uint(uint32_t value) +{ + g_uint32 = value; +} + +/* ----- version ----- */ + +extern int rc_client_version_less(const char* left, const char* right); + +static void test_version_less(const char* left, const char* right, int expected) +{ + if (expected == 0) { + ASSERT_FALSE(rc_client_version_less(left, right)); + } + else { + ASSERT_TRUE(rc_client_version_less(left, right)); + } +} + +/* ----- login ----- */ + +static void assert_init_params(HWND hWnd, const char* client_name, const char* client_version) +{ + ASSERT_PTR_NOT_NULL((void*)hWnd); + ASSERT_STR_EQUALS(client_name, "TestClient"); + ASSERT_STR_EQUALS(client_version, "1.0.1"); +} + +static int rc_client_integration_init(HWND hWnd, const char* client_name, const char* client_version) +{ + assert_init_params(hWnd, client_name, client_version); + + g_integration_event = "init"; + return 1; +} + +static int rc_client_get_external_client(rc_client_external_t* client, int nVersion) +{ + if (strcmp(g_integration_event, "init") == 0) + g_integration_event = "init2"; + + return 1; +} + +static const char* rc_client_integration_get_version(void) +{ + return "1.3.0"; +} + +static const char* rc_client_integration_get_host_url_offline(void) +{ + return "OFFLINE"; +} + +static void test_load_raintegration(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->get_version = rc_client_integration_get_version; + g_client->state.raintegration->init_client = rc_client_integration_init; + g_client->state.raintegration->get_external_client = rc_client_get_external_client; + + mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.0\"}"); + + rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_integration_event, "init2"); + + rc_client_destroy(g_client); +} + +static void test_load_raintegration_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_with_integration_async(); + g_client->state.raintegration->get_version = rc_client_integration_get_version; + g_client->state.raintegration->init_client = rc_client_integration_init; + g_client->state.raintegration->get_external_client = rc_client_get_external_client; + + handle = rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.0\"}"); + + ASSERT_STR_EQUALS(g_integration_event, "none"); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_outdated_version(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "RA_Integration version 1.3.0 is lower than minimum version 1.3.1"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_raintegration_outdated_version(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->get_version = rc_client_integration_get_version; + g_client->state.raintegration->init_client = rc_client_integration_init; + g_client->state.raintegration->get_external_client = rc_client_get_external_client; + + mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.1\"}"); + + rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", + rc_client_callback_expect_outdated_version, g_callback_userdata); + + ASSERT_STR_EQUALS(g_integration_event, "none"); + + rc_client_destroy(g_client); +} + +static void test_load_raintegration_supported_version(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->get_version = rc_client_integration_get_version; + g_client->state.raintegration->init_client = rc_client_integration_init; + g_client->state.raintegration->get_external_client = rc_client_get_external_client; + + mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.2.1\"}"); + + rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_integration_event, "init2"); + + rc_client_destroy(g_client); +} + +static void test_load_raintegration_offline(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->get_host_url = rc_client_integration_get_host_url_offline; + g_client->state.raintegration->get_version = rc_client_integration_get_version; + g_client->state.raintegration->init_client_offline = rc_client_integration_init; + g_client->state.raintegration->get_external_client = rc_client_get_external_client; + + mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.2.1\"}"); + + rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_integration_event, "init2"); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_after_login_failure(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "Cannot initialize RAIntegration after login"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_raintegration_after_login(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->get_version = rc_client_integration_get_version; + g_client->state.raintegration->init_client = rc_client_integration_init; + g_client->state.raintegration->get_external_client = rc_client_get_external_client; + g_client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.0\"}"); + + rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", + rc_client_callback_expect_after_login_failure, g_callback_userdata); + + ASSERT_STR_EQUALS(g_integration_event, "none"); + + rc_client_destroy(g_client); +} + +/* ----- windows ----- */ + +static HWND g_hWnd; + +static void rc_client_integration_update_main_window_handle(HWND hWnd) +{ + g_hWnd = hWnd; +} + +static void test_update_main_window_handle(void) +{ + HWND hWnd = (HWND)0x12345678; + g_hWnd = NULL; + + g_client = mock_client_with_integration(); + g_client->state.raintegration->update_main_window_handle = rc_client_integration_update_main_window_handle; + + /* does nothing if raintegration hasn't been initialized */ + ASSERT_NUM_EQUALS(g_client->state.raintegration->bIsInited, 0); + rc_client_raintegration_update_main_window_handle(g_client, hWnd); + ASSERT_PTR_NULL(g_hWnd); + + g_client->state.raintegration->bIsInited = 1; + rc_client_raintegration_update_main_window_handle(g_client, hWnd); + ASSERT_PTR_EQUALS(g_hWnd, hWnd); + + g_hWnd = NULL; +} + +/* ----- menu ----- */ + +static rc_client_raintegration_menu_t* g_menu; + +static const rc_client_raintegration_menu_t* rc_client_integration_get_menu(void) +{ + return g_menu; +} + +static void test_get_menu(void) +{ + const rc_client_raintegration_menu_t* menu; + rc_client_raintegration_menu_t local_menu; + rc_client_raintegration_menu_item_t local_menu_items[3]; + + memset(&local_menu_items, 0, sizeof(local_menu_items)); + local_menu_items[0].id = 1234; + local_menu_items[0].label = "Label 1"; + local_menu_items[0].checked = 1; + local_menu_items[0].enabled = 1; + local_menu_items[2].id = 2345; + local_menu_items[2].label = "Label 2"; + + local_menu.num_items = sizeof(local_menu_items) / sizeof(local_menu_items[0]); + local_menu.items = local_menu_items; + g_menu = &local_menu; + + g_client = mock_client_with_integration(); + g_client->state.raintegration->get_menu = rc_client_integration_get_menu; + + /* returns null if raintegration hasn't been initialized */ + ASSERT_NUM_EQUALS(g_client->state.raintegration->bIsInited, 0); + menu = rc_client_raintegration_get_menu(g_client); + ASSERT_PTR_NULL(menu); + + g_client->state.raintegration->bIsInited = 1; + menu = rc_client_raintegration_get_menu(g_client); + ASSERT_PTR_NOT_NULL(menu); + + ASSERT_NUM_EQUALS(menu->num_items, 3); + ASSERT_NUM_EQUALS(menu->items[0].id, 1234); + ASSERT_STR_EQUALS(menu->items[0].label, "Label 1"); + ASSERT_NUM_EQUALS(menu->items[0].checked, 1); + ASSERT_NUM_EQUALS(menu->items[0].enabled, 1); + ASSERT_NUM_EQUALS(menu->items[1].id, 0); + ASSERT_PTR_NULL(menu->items[1].label); + ASSERT_NUM_EQUALS(menu->items[1].checked, 0); + ASSERT_NUM_EQUALS(menu->items[1].enabled, 0); + ASSERT_NUM_EQUALS(menu->items[2].id, 2345); + ASSERT_STR_EQUALS(menu->items[2].label, "Label 2"); + ASSERT_NUM_EQUALS(menu->items[2].checked, 0); + ASSERT_NUM_EQUALS(menu->items[2].enabled, 0); + + g_menu = NULL; +} + +static int rc_client_integration_activate_menu_item(uint32_t id) +{ + if (id < 1700) + return 0; + + g_uint32 = id; + return 1; +} + +static void test_activate_menu_item(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->activate_menu_item = rc_client_integration_activate_menu_item; + + g_uint32 = 0; + ASSERT_NUM_EQUALS(rc_client_raintegration_activate_menu_item(g_client, 1600), 0); + ASSERT_NUM_EQUALS(g_uint32, 0); + + ASSERT_NUM_EQUALS(rc_client_raintegration_activate_menu_item(g_client, 1704), 1); + ASSERT_NUM_EQUALS(g_uint32, 1704); +} + +static void rc_client_integration_set_console_id(int console_id) +{ + g_uint32 = console_id; +} + +static void test_set_console_id(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->set_console_id = rc_client_integration_set_console_id; + + g_uint32 = 0; + rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_NINTENDO); + ASSERT_NUM_EQUALS(g_uint32, RC_CONSOLE_NINTENDO); + + rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_PLAYSTATION); + ASSERT_NUM_EQUALS(g_uint32, RC_CONSOLE_PLAYSTATION); + + rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_MEGA_DRIVE); + ASSERT_NUM_EQUALS(g_uint32, RC_CONSOLE_MEGA_DRIVE); +} + +static int rc_client_integration_has_modifications(void) +{ + return (int)g_uint32; +} + +static void test_has_modifications(void) +{ + g_client = mock_client_with_integration(); + g_client->state.raintegration->has_modifications = rc_client_integration_has_modifications; + + g_uint32 = 0; + ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 0); + + g_uint32 = 1; + ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 0); + + g_client->state.raintegration->bIsInited = 1; + ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 1); + + g_uint32 = 0; + ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 0); +} + +/* ----- harness ----- */ + +void test_client_raintegration(void) { + TEST_SUITE_BEGIN(); + + /* version */ + TEST_PARAMS3(test_version_less, "0.0.0", "1.0.0", 1); + TEST_PARAMS3(test_version_less, "1.0.0", "0.0.0", 0); + TEST_PARAMS3(test_version_less, "1.2.0", "1.2.0", 0); + TEST_PARAMS3(test_version_less, "1.2.0", "1.1.0", 0); + TEST_PARAMS3(test_version_less, "1.2.0", "1.3.0", 1); + TEST_PARAMS3(test_version_less, "1.2.0", "1.10.0", 1); + TEST_PARAMS3(test_version_less, "1.2.0", "2.1.0", 1); + TEST_PARAMS3(test_version_less, "2.1.0", "1.2.0", 0); + + /* login */ + TEST(test_load_raintegration); + TEST(test_load_raintegration_aborted); + TEST(test_load_raintegration_outdated_version); + TEST(test_load_raintegration_supported_version); + TEST(test_load_raintegration_offline); + TEST(test_load_raintegration_after_login); + + /* windows */ + TEST(test_update_main_window_handle); + + /* menu */ + TEST(test_get_menu); + TEST(test_activate_menu_item); + + /* set_console_id */ + TEST(test_set_console_id); + + /* has_modifications */ + TEST(test_has_modifications); + + TEST_SUITE_END(); +} + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ diff --git a/src/rcheevos/test/test_rc_libretro.c b/src/rcheevos/test/test_rc_libretro.c new file mode 100644 index 0000000000..f750f574c5 --- /dev/null +++ b/src/rcheevos/test/test_rc_libretro.c @@ -0,0 +1,952 @@ +#include "../rc_libretro.h" + +#include "../rc_compat.h" +#include "rc_consoles.h" + +#include "test_framework.h" +#include "rhash/mock_filereader.h" + +static void* retro_memory_data[4] = { NULL, NULL, NULL, NULL }; +static size_t retro_memory_size[4] = { 0, 0, 0, 0 }; + +static void libretro_get_core_memory_info(uint32_t id, rc_libretro_core_memory_info_t* info) +{ + info->data = retro_memory_data[id]; + info->size = retro_memory_size[id]; +} + +static int libretro_get_image_path(uint32_t index, char* buffer, size_t buffer_size) +{ + if (index < 0 || index > 9) + return 0; + + snprintf(buffer, buffer_size, "save%d.dsk", index); + return 1; +} + +static void test_allowed_setting(const char* library_name, const char* setting, const char* value) { + const rc_disallowed_setting_t* settings = rc_libretro_get_disallowed_settings(library_name); + if (!settings) + return; + + ASSERT_TRUE(rc_libretro_is_setting_allowed(settings, setting, value)); +} + +static void test_disallowed_setting(const char* library_name, const char* setting, const char* value) { + const rc_disallowed_setting_t* settings = rc_libretro_get_disallowed_settings(library_name); + ASSERT_PTR_NOT_NULL(settings); + ASSERT_FALSE(rc_libretro_is_setting_allowed(settings, setting, value)); +} + +static void test_allowed_system(const char* library_name, uint32_t console_id) { + ASSERT_TRUE(rc_libretro_is_system_allowed(library_name, console_id)); +} + +static void test_disallowed_system(const char* library_name, uint32_t console_id) { + ASSERT_FALSE(rc_libretro_is_system_allowed(library_name, console_id)); +} + +static void test_memory_init_without_regions() { + rc_libretro_memory_regions_t regions; + uint32_t avail; + #define BUFFER1_SIZE 16 + #define BUFFER2_SIZE 8 + /* put explicit gap between buffer1 and buffer2 to prevent them from being merged when building regions */ + uint8_t buffer[BUFFER1_SIZE + 4 + BUFFER2_SIZE]; + uint8_t* buffer1 = &buffer[0]; + uint8_t* buffer2 = &buffer[20]; + uint8_t buffer3[4]; + uint32_t i; + + for (i = 0; i < BUFFER1_SIZE; ++i) + buffer1[i] = i; + for (i = 0; i < BUFFER2_SIZE; ++i) + buffer2[i] = i; + for (i = 0; i < sizeof(buffer3); ++i) + buffer3[i] = i; + + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = BUFFER1_SIZE; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = BUFFER2_SIZE; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, BUFFER1_SIZE + BUFFER2_SIZE); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, BUFFER1_SIZE + 2), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, BUFFER1_SIZE + BUFFER2_SIZE + 2)); + + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 2, &avail), &buffer1[2]); + ASSERT_NUM_EQUALS(avail, BUFFER1_SIZE - 2); + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, BUFFER1_SIZE - 1, &avail), &buffer1[BUFFER1_SIZE - 1]); + ASSERT_NUM_EQUALS(avail, 1); + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, BUFFER1_SIZE + 2, &avail), &buffer2[2]); + ASSERT_NUM_EQUALS(avail, BUFFER2_SIZE - 2); + ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, BUFFER1_SIZE + BUFFER2_SIZE + 2, &avail)); + ASSERT_NUM_EQUALS(avail, 0); + + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 2, buffer3, 1), 1); + ASSERT_TRUE(memcmp(buffer3, &buffer1[2], 1) == 0); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 7, buffer3, 4), 4); + ASSERT_TRUE(memcmp(buffer3, &buffer1[7], 4) == 0); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, BUFFER1_SIZE - 2, buffer3, 3), 3); /* read across boundary */ + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, BUFFER1_SIZE + BUFFER2_SIZE + 2, buffer3, 1), 0); + + #undef BUFFER2_SIZE + #undef BUFFER1_SIZE +} + +static void test_memory_init_without_regions_system_ram_only() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[16]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer1)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer1) + 2)); +} + +static void test_memory_init_without_regions_save_ram_only() { + rc_libretro_memory_regions_t regions; + uint8_t buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer2) + 2)); +} + +static void test_memory_init_without_regions_no_ram() { + rc_libretro_memory_regions_t regions; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); + + ASSERT_NUM_EQUALS(regions.count, 0); + ASSERT_NUM_EQUALS(regions.total_size, 0); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 2)); +} + +static void test_memory_init_from_unmapped_memory() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8], buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0x10000; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_unmapped_memory_null_filler() { + rc_libretro_memory_regions_t regions; + uint32_t avail; + uint8_t buffer1[16], buffer2[8]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 4); /* two valid regions and two null fillers */ + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00012)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x1000A)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); + + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 0x00002, &avail), &buffer1[2]); + ASSERT_NUM_EQUALS(avail, sizeof(buffer1) - 2); + ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, 0x00012, &avail)); + ASSERT_NUM_EQUALS(avail, 0); + ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 0x10002, &avail), &buffer2[2]); + ASSERT_NUM_EQUALS(avail, sizeof(buffer2) - 2); + ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, 0x1000A, &avail)); + ASSERT_NUM_EQUALS(avail, 0); +} + +static void test_memory_init_from_unmapped_memory_no_save_ram() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[16]; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_unmapped_memory_merge_neighbors() { + rc_libretro_memory_regions_t regions; + uint8_t* buffer1 = (uint8_t*)malloc(0x10000); /* have to malloc to prevent array-bounds compiler warnings */ + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_ATARI_LYNX)); + + ASSERT_NUM_EQUALS(regions.count, 1); /* all regions are adjacent, so should be merged */ + ASSERT_NUM_EQUALS(regions.total_size, 0x10000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer1[0x0002]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer1[0x0102]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0xFFFF), &buffer1[0xFFFF]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10000)); + + free(buffer1); +} + +static void test_memory_init_from_unmapped_memory_no_ram() { + rc_libretro_memory_regions_t regions; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + /* init returns false */ + ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + /* but one null-filled region is still generated */ + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_unmapped_memory_ds() { + rc_libretro_memory_regions_t regions; + uint8_t* buffer1 = (uint8_t*)malloc(0x02000000); /* have to malloc to prevent array-bounds compiler warnings */ + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x02000000; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + /* init returns true */ + ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_NINTENDO_DS)); + + /* primary region should be generated, but TCM won't be available */ + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x1004000); + ASSERT_PTR_NOT_NULL(rc_libretro_memory_find(®ions, 0x00000002)); + ASSERT_PTR_NOT_NULL(rc_libretro_memory_find(®ions, 0x003FFFFF)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00400000)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x01000002)); + + free(buffer1); +} + +static void test_memory_init_from_unmapped_memory_wii() { + rc_libretro_memory_regions_t regions; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; + retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; + retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; + + /* init returns false */ + ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_WII)); + + /* but one null-filled region is still generated */ + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, 0x14000000); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00000002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x14000002)); +} + +static void test_memory_init_from_memory_map() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_null_filler() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_no_save_ram() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_merge_neighbors() { + rc_libretro_memory_regions_t regions; + uint8_t* buffer1 = (uint8_t*)malloc(0x10000); /* have to malloc to prevent array-bounds compiler warnings */ + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0x0000], 0, 0x0000U, 0, 0, 0xFC00, "RAM" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0xFC00], 0, 0xFC00U, 0, 0, 0x0400, "Hardware controllers" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_ATARI_LYNX)); + + ASSERT_NUM_EQUALS(regions.count, 1); /* all regions are adjacent, so should be merged */ + ASSERT_NUM_EQUALS(regions.total_size, 0x10000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer1[0x0002]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer1[0x0102]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0xFFFF), &buffer1[0xFFFF]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10000)); + + free(buffer1); +} + +static void test_memory_init_from_memory_map_no_ram() { + rc_libretro_memory_regions_t regions; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, NULL, 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, NULL, 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + /* init returns false */ + ASSERT_FALSE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + /* but one null-filled region is still generated */ + ASSERT_NUM_EQUALS(regions.count, 1); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_splice() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8], buffer2[8], buffer3[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x08000, "RAM1" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer2[0], 0, 0xFF8000U, 0, 0, 0x08000, "RAM2" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer3[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 3); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer2[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer3[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_mirrored() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0xFF0000U, 0x00C000U, 0x04000, "RAM" }, + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0x000000U, 0x000000U, 0x10000, "SRAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + /* select of 0xFF0000 should mirror the 0x4000 bytes at 0xFF0000 into 0xFF4000, 0xFF8000, and 0xFFC000 */ + ASSERT_NUM_EQUALS(regions.count, 5); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x04002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0C002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_out_of_order() { + rc_libretro_memory_regions_t regions; + uint8_t buffer1[8], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); + + ASSERT_NUM_EQUALS(regions.count, 2); + ASSERT_NUM_EQUALS(regions.total_size, 0x20000); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); +} + +static void test_memory_init_from_memory_map_disconnect_gaps() { + rc_libretro_memory_regions_t regions; + uint8_t buffer[256]; + /* the disconnect bit is smaller than the region size, so only parts of the memory map + * will be filled by the region. in this case, 00-1F will be buffer[00-1F], but + * buffer[20-3F] will be associated to addresses 40-5F! */ + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer[0], 0, 0x0000, 0xFC20, 0x0020, sizeof(buffer), "RAM" }, + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MAGNAVOX_ODYSSEY2)); + + ASSERT_NUM_EQUALS(regions.count, 10); + ASSERT_NUM_EQUALS(regions.total_size, 0x140); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer[0x02]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0022)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0042), &buffer[0x22]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0062)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0082), &buffer[0x42]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00A2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00C2), &buffer[0x62]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00E2)); + ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer[0x82]); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0122)); + ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0142)); +} + + +static void test_memory_read() +{ + rc_libretro_memory_regions_t regions; + /* intentionally put buffer3 between buffer1 and buffer2 for the read that spans buffers */ + uint8_t buffer1[8], buffer3[4], buffer2[8]; + const struct retro_memory_descriptor mmap_desc[] = { + { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0x000000U, 0, 0, 0x000008, "RAM A" }, + { RETRO_MEMDESC_SYSTEM_RAM, &buffer2[0], 0, 0x000008U, 0, 0, 0x000008, "RAM B" } + }; + const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; + int i; + + for (i = 0; i < 8; ++i) { + buffer1[i] = i; + buffer2[i] = i + 8; + } + + ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_NINTENDO)); + + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 0, buffer3, 4), 4); + ASSERT_NUM_EQUALS(buffer3[0], 0); + ASSERT_NUM_EQUALS(buffer3[1], 1); + ASSERT_NUM_EQUALS(buffer3[2], 2); + ASSERT_NUM_EQUALS(buffer3[3], 3); + + /* only requesting two bytes, other two remain unmodified */ + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 3, buffer3, 2), 2); + ASSERT_NUM_EQUALS(buffer3[0], 3); + ASSERT_NUM_EQUALS(buffer3[1], 4); + ASSERT_NUM_EQUALS(buffer3[2], 2); + ASSERT_NUM_EQUALS(buffer3[3], 3); + + /* one two bytes are available in buffer1, the rest have to be read from buffer2 */ + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 6, buffer3, 4), 4); + ASSERT_NUM_EQUALS(buffer3[0], 6); + ASSERT_NUM_EQUALS(buffer3[1], 7); + ASSERT_NUM_EQUALS(buffer3[2], 8); /* this comes from buffer2 */ + ASSERT_NUM_EQUALS(buffer3[3], 9); + + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 8, buffer3, 4), 4); + ASSERT_NUM_EQUALS(buffer3[0], 8); + ASSERT_NUM_EQUALS(buffer3[1], 9); + ASSERT_NUM_EQUALS(buffer3[2], 10); + ASSERT_NUM_EQUALS(buffer3[3], 11); + + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 11, buffer3, 4), 4); + ASSERT_NUM_EQUALS(buffer3[0], 11); + ASSERT_NUM_EQUALS(buffer3[1], 12); + ASSERT_NUM_EQUALS(buffer3[2], 13); + ASSERT_NUM_EQUALS(buffer3[3], 14); + + /* only requesting 1 byte. other three remain unmodified */ + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 13, buffer3, 1), 1); + ASSERT_NUM_EQUALS(buffer3[0], 13); + ASSERT_NUM_EQUALS(buffer3[1], 12); + ASSERT_NUM_EQUALS(buffer3[2], 13); + ASSERT_NUM_EQUALS(buffer3[3], 14); + + /* only two bytes are available at address 14 */ + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 14, buffer3, 3), 2); + ASSERT_NUM_EQUALS(buffer3[0], 14); + ASSERT_NUM_EQUALS(buffer3[1], 15); + ASSERT_NUM_EQUALS(buffer3[2], 13); + ASSERT_NUM_EQUALS(buffer3[3], 14); + + /* no bytes are available at invalid address */ + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 16, buffer3, 4), 0); + ASSERT_NUM_EQUALS(buffer3[0], 14); + ASSERT_NUM_EQUALS(buffer3[1], 15); + ASSERT_NUM_EQUALS(buffer3[2], 13); + ASSERT_NUM_EQUALS(buffer3[3], 14); +} + +static void test_hash_set_add_single() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char hash[] = "ABCDEF01234567899876543210ABCDEF"; + + get_mock_filereader(&file_reader); + + rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path, &file_reader); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.rom")); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + + rc_libretro_hash_set_add(&hash_set, "file.rom", 1234, hash); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 1234); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_update_single() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char hash[] = "ABCDEF01234567899876543210ABCDEF"; + const char hash2[] = "0123456789ABCDEF0123456789ABCDEF"; + + get_mock_filereader(&file_reader); + + rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path, &file_reader); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.rom")); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + + rc_libretro_hash_set_add(&hash_set, "file.rom", 99, hash); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 99); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 0); + + rc_libretro_hash_set_add(&hash_set, "file.rom", 1234, hash2); + ASSERT_NUM_EQUALS(hash_set.entries_count, 1); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash2); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 1234); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_add_many() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char hash1[] = "ABCDEF01234567899876543210ABCDE1"; + const char hash2[] = "ABCDEF01234567899876543210ABCDE2"; + const char hash3[] = "ABCDEF01234567899876543210ABCDE3"; + const char hash4[] = "ABCDEF01234567899876543210ABCDE4"; + const char hash5[] = "ABCDEF01234567899876543210ABCDE5"; + const char hash6[] = "ABCDEF01234567899876543210ABCDE6"; + const char hash7[] = "ABCDEF01234567899876543210ABCDE7"; + const char hash8[] = "ABCDEF01234567899876543210ABCDE8"; + + get_mock_filereader(&file_reader); + + rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path, &file_reader); + + rc_libretro_hash_set_add(&hash_set, "file1.rom", 1, hash1); + rc_libretro_hash_set_add(&hash_set, "file2.rom", 2, hash2); + rc_libretro_hash_set_add(&hash_set, "file3.rom", 3, hash3); + rc_libretro_hash_set_add(&hash_set, "file4.rom", 4, hash4); + rc_libretro_hash_set_add(&hash_set, "file5.rom", 5, hash5); + rc_libretro_hash_set_add(&hash_set, "file6.rom", 6, hash6); + rc_libretro_hash_set_add(&hash_set, "file7.rom", 7, hash7); + rc_libretro_hash_set_add(&hash_set, "file8.rom", 8, hash8); + + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file1.rom"), hash1); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash1), 1); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file2.rom"), hash2); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 2); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file3.rom"), hash3); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash3), 3); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file4.rom"), hash4); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash4), 4); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file5.rom"), hash5); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash5), 5); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file6.rom"), hash6); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash6), 6); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file7.rom"), hash7); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash7), 7); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file8.rom"), hash8); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash8), 8); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_single() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char hash[] = "ABCDEF01234567899876543210ABCDEF"; + const char* m3u_contents = "file.dsk"; + + get_mock_filereader(&file_reader); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk")); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_savedisk() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char* m3u_contents = "file.dsk\n#SAVEDISK:"; + + get_mock_filereader(&file_reader); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_savedisk_volume_label() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char* m3u_contents = "file.dsk\n#SAVEDISK:DSAVE"; + + get_mock_filereader(&file_reader); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static void test_hash_set_m3u_savedisk_multiple_with_comments_and_whitespace() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char* m3u_contents = + "#EXTM3U\n" + "file.dsk\n" /* index 0 */ + "\n" + "#Save disk in the middle, because why not?\n" + "#SAVEDISK:\n" /* index 1 */ + " \r\n" + "\tfile2.dsk|File 2\n" /* index 2 */ + "#SAVEDISK:DSAVE\n" /* index 3 */ + "\t\r\n" + "#LABEL:My Custom Disk Label\n" + "file3.dsk" /* index 4 */ + "\r\n" + "#SAVEDISK:|No Custom Label for Save Disk"; /* index 5 */ + + get_mock_filereader(&file_reader); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); + + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file2.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save3.dsk"), "[SAVE DISK]"); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file3.dsk")); + ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save5.dsk"), "[SAVE DISK]"); + + rc_libretro_hash_set_destroy(&hash_set); +} + +static int libretro_get_image_path_no_core_support(uint32_t index, char* buffer, size_t buffer_size) +{ + if (index < 0 || index > 1) + return 0; + + snprintf(buffer, buffer_size, "file%d.dsk", index); + return 1; +} + +static void test_hash_set_m3u_savedisk_no_core_support() { + rc_libretro_hash_set_t hash_set; + rc_hash_filereader_t file_reader; + const char* m3u_contents = "file1.dsk\n#SAVEDISK:\nfile2.dsk"; + + get_mock_filereader(&file_reader); + mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); + + rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path_no_core_support, &file_reader); + + ASSERT_NUM_EQUALS(hash_set.entries_count, 0); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file1.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file2.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file3.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save2.dsk")); + ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save3.dsk")); + + rc_libretro_hash_set_destroy(&hash_set); +} + + +void test_rc_libretro(void) { + TEST_SUITE_BEGIN(); + + /* rc_libretro_disallowed_settings */ + TEST_PARAMS3(test_allowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "750%"); + TEST_PARAMS3(test_allowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "100%(native)"); + TEST_PARAMS3(test_disallowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "99%"); + TEST_PARAMS3(test_disallowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "50%"); + + TEST_PARAMS3(test_allowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "750%"); + TEST_PARAMS3(test_allowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "100%(native)"); + TEST_PARAMS3(test_disallowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "99%"); + TEST_PARAMS3(test_disallowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "50%"); + + TEST_PARAMS3(test_allowed_setting, "bsnes-mercury", "bsnes_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "bsnes-mercury", "bsnes_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "bsnes-mercury", "bsnes_region", "PAL"); + + TEST_PARAMS3(test_allowed_setting, "cap32", "cap32_autorun", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "cap32", "cap32_autorun", "disabled"); + + TEST_PARAMS3(test_allowed_setting, "dolphin-emu", "dolphin_cheats_enabled", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "dolphin-emu", "dolphin_cheats_enabled", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "DOSBox-pure", "dosbox_pure_strict_mode", "true"); + TEST_PARAMS3(test_disallowed_setting, "DOSBox-pure", "dosbox_pure_strict_mode", "false"); + + TEST_PARAMS3(test_allowed_setting, "DuckStation", "duckstation_CDROM.LoadImagePatches", "false"); + TEST_PARAMS3(test_disallowed_setting, "DuckStation", "duckstation_CDROM.LoadImagePatches", "true"); + + TEST_PARAMS3(test_allowed_setting, "ecwolf", "ecwolf-invulnerability", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "ecwolf", "ecwolf-invulnerability", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "PAL"); + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "pal"); /* case insensitive */ + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "Dendy"); + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_palette", "default"); /* setting we don't care about */ + TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_game_genie", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_game_genie", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "Flycast", "reicast_sh4clock", "500"); + TEST_PARAMS3(test_allowed_setting, "Flycast", "reicast_sh4clock", "200"); + TEST_PARAMS3(test_disallowed_setting, "Flycast", "reicast_sh4clock", "190"); + TEST_PARAMS3(test_disallowed_setting, "Flycast", "reicast_sh4clock", "50"); + + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-allow-patched-romsets", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-allow-patched-romsets", "enabled"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "disabled"); /* wildcard key match */ + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "enabled"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "0 - Disabled"); /* multi-not value match */ + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "1 - Enabled"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "25%"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "50%"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "75%"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "90%"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "95%"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "99%"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "100%"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "150%"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "200%"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "400%"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-dipswitch-mslug-BIOS", "MVS Asia/Europe ver. 6 (1 slot)"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-dipswitch-mslug-BIOS", "Universe BIOS ver. 2.3 (alt)"); + TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-neogeo-mode", "DIPSWITCH"); + TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-neogeo-mode", "UNIBIOS"); + + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "action replay (pro)"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "game genie"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "sonic & knuckles"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "NTSC-U"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "PAL"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "NTSC-J"); + + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "action replay (pro)"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "game genie"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "sonic & knuckles"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "NTSC-U"); + TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "PAL"); + TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "NTSC-J"); + + TEST_PARAMS3(test_allowed_setting, "Mesen", "mesen_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Mesen", "mesen_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "Mesen", "mesen_region", "PAL"); + TEST_PARAMS3(test_disallowed_setting, "Mesen", "mesen_region", "Dendy"); + + TEST_PARAMS3(test_allowed_setting, "Mesen-S", "mesen-s_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Mesen-S", "mesen-s_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "Mesen-S", "mesen-s_region", "PAL"); + + TEST_PARAMS3(test_allowed_setting, "NeoCD", "neocd_bios", "neocd.bin (CDZ)"); + TEST_PARAMS3(test_disallowed_setting, "NeoCD", "neocd_bios", "uni-bioscd.rom (CDZ, Universe 3.3)"); + + TEST_PARAMS3(test_allowed_setting, "PPSSPP", "ppsspp_cheats", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "PPSSPP", "ppsspp_cheats", "enabled"); + + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "PAL"); + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "auto"); + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "100"); + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "57"); + TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "55"); + TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "54"); + TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "30"); + + TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "US"); + TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "Japan NTSC"); + TEST_PARAMS3(test_disallowed_setting, "PicoDrive", "picodrive_region", "Europe"); + TEST_PARAMS3(test_disallowed_setting, "PicoDrive", "picodrive_region", "Japan PAL"); + + TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "16"); + TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "8"); + TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "4"); + TEST_PARAMS3(test_disallowed_setting, "QUASI88", "q88_cpu_clock", "2"); + TEST_PARAMS3(test_disallowed_setting, "QUASI88", "q88_cpu_clock", "1"); + + TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "auto"); + TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "ntsc-u"); + TEST_PARAMS3(test_disallowed_setting, "SMS Plus GX", "smsplus_region", "pal"); + TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "ntsc-j"); + + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_region", "Auto"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_region", "NTSC"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_region", "PAL"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_gfx_clip", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_gfx_clip", "disabled"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_gfx_transp", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_gfx_transp", "disabled"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_layer_1", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_layer_1", "disabled"); + TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_layer_5", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_layer_5", "disabled"); + + TEST_PARAMS3(test_allowed_setting, "SwanStation", "swanstation_CPU_Overclock", "1000"); + TEST_PARAMS3(test_allowed_setting, "SwanStation", "swanstation_CPU_Overclock", "100"); + TEST_PARAMS3(test_disallowed_setting, "SwanStation", "swanstation_CPU_Overclock", "99"); + TEST_PARAMS3(test_disallowed_setting, "SwanStation", "swanstation_CPU_Overclock", "50"); + + TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_autostart", "enabled"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_autostart", "disabled"); + TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_autostart", "warp"); + TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_reset", "autostart"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "soft"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "hard"); + TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "freeze"); + + TEST_PARAMS3(test_allowed_setting, "Virtual Jaguar", "virtualjaguar_pal", "disabled"); + TEST_PARAMS3(test_disallowed_setting, "Virtual Jaguar", "virtualjaguar_pal", "enabled"); + + /* rc_libretro_is_system_allowed */ + TEST_PARAMS2(test_allowed_system, "FCEUmm", RC_CONSOLE_NINTENDO); + + TEST_PARAMS2(test_allowed_system, "Mesen-S", RC_CONSOLE_SUPER_NINTENDO); + TEST_PARAMS2(test_disallowed_system, "Mesen-S", RC_CONSOLE_GAMEBOY); + TEST_PARAMS2(test_disallowed_system, "Mesen-S", RC_CONSOLE_GAMEBOY_COLOR); + + /* rc_libretro_memory_init */ + TEST(test_memory_init_without_regions); + TEST(test_memory_init_without_regions_system_ram_only); + TEST(test_memory_init_without_regions_save_ram_only); + TEST(test_memory_init_without_regions_no_ram); + + TEST(test_memory_init_from_unmapped_memory); + TEST(test_memory_init_from_unmapped_memory_null_filler); + TEST(test_memory_init_from_unmapped_memory_no_save_ram); + TEST(test_memory_init_from_unmapped_memory_merge_neighbors); + TEST(test_memory_init_from_unmapped_memory_no_ram); + TEST(test_memory_init_from_unmapped_memory_ds); + TEST(test_memory_init_from_unmapped_memory_wii); + + TEST(test_memory_init_from_memory_map); + TEST(test_memory_init_from_memory_map_null_filler); + TEST(test_memory_init_from_memory_map_no_save_ram); + TEST(test_memory_init_from_memory_map_merge_neighbors); + TEST(test_memory_init_from_memory_map_no_ram); + TEST(test_memory_init_from_memory_map_splice); + TEST(test_memory_init_from_memory_map_mirrored); + TEST(test_memory_init_from_memory_map_out_of_order); + TEST(test_memory_init_from_memory_map_disconnect_gaps); + + /* rc_libretro_memory_read */ + TEST(test_memory_read) + + /* rc_libretro_hash_set_t */ + TEST(test_hash_set_add_single); + TEST(test_hash_set_update_single); + TEST(test_hash_set_add_many); + + TEST(test_hash_set_m3u_single); + TEST(test_hash_set_m3u_savedisk); + TEST(test_hash_set_m3u_savedisk_volume_label); + TEST(test_hash_set_m3u_savedisk_multiple_with_comments_and_whitespace); + TEST(test_hash_set_m3u_savedisk_no_core_support); + + TEST_SUITE_END(); +} diff --git a/src/rcheevos/test/test_types.natvis b/src/rcheevos/test/test_types.natvis new file mode 100644 index 0000000000..2721f39985 --- /dev/null +++ b/src/rcheevos/test/test_types.natvis @@ -0,0 +1,9 @@ + + + + + ((__rc_memref_list_t*)&memrefs) + ((__rc_value_list_t*)&variables) + + + diff --git a/src/rcheevos/validator/Makefile b/src/rcheevos/validator/Makefile new file mode 100644 index 0000000000..22d6e06461 --- /dev/null +++ b/src/rcheevos/validator/Makefile @@ -0,0 +1,25 @@ +RC_SRC=../src +RC_CHEEVOS_SRC=$(RC_SRC)/rcheevos +RC_HASH_SRC=$(RC_SRC)/rhash +RC_API_SRC=$(RC_SRC)/rapi + +OBJ=$(RC_CHEEVOS_SRC)/alloc.o $(RC_CHEEVOS_SRC)/condition.o $(RC_CHEEVOS_SRC)/condset.o \ + $(RC_CHEEVOS_SRC)/consoleinfo.o $(RC_CHEEVOS_SRC)/format.o $(RC_CHEEVOS_SRC)/lboard.o \ + $(RC_CHEEVOS_SRC)/memref.o $(RC_CHEEVOS_SRC)/operand.o $(RC_CHEEVOS_SRC)/rc_validate.o \ + $(RC_CHEEVOS_SRC)/richpresence.o $(RC_CHEEVOS_SRC)/runtime.o $(RC_CHEEVOS_SRC)/trigger.o \ + $(RC_CHEEVOS_SRC)/value.o \ + $(RC_SRC)/rc_compat.o $(RC_SRC)/rc_util.o \ + $(RC_HASH_SRC)/md5.o \ + $(RC_API_SRC)/rc_api_common.o $(RC_API_SRC)/rc_api_runtime.o + validator.o + +all: validator + +%.o: %.c + gcc -Wall -O0 -g -std=c89 -ansi -Wno-long-long -I../include -I$(RC_CHEEVOS_SRC) -c $< -o $@ + +validator: $(OBJ) + gcc -o $@ $+ -lm + +clean: + rm -f test $(OBJ) diff --git a/src/rcheevos/validator/validator.c b/src/rcheevos/validator/validator.c new file mode 100644 index 0000000000..c984533595 --- /dev/null +++ b/src/rcheevos/validator/validator.c @@ -0,0 +1,658 @@ +#include "rc_internal.h" +#include "rc_api_runtime.h" +#include "rc_consoles.h" +#include "rc_validate.h" + +#include +#include +#include +#include +#include /* memset */ +#include + +#ifdef _MSC_VER /* windows build */ + #define WIN32_LEAN_AND_MEAN + #include +#else + #include + #include + #define stricmp strcasecmp +#endif + +/* usage exmaple: + * + * ./validator.exe d "E:\RetroAchievements\dump" | sort > results.txt + * grep -v ": OK" results.txt | grep -v "File: " | grep . + */ + +static const char* flag_string(char type) { + switch (type) { + case RC_CONDITION_STANDARD: return ""; + case RC_CONDITION_PAUSE_IF: return "PauseIf "; + case RC_CONDITION_RESET_IF: return "ResetIf "; + case RC_CONDITION_MEASURED_IF: return "MeasuredIf "; + case RC_CONDITION_TRIGGER: return "Trigger "; + case RC_CONDITION_MEASURED: return "Measured "; + case RC_CONDITION_ADD_SOURCE: return "AddSource "; + case RC_CONDITION_SUB_SOURCE: return "SubSource "; + case RC_CONDITION_REMEMBER: return "Remember "; + case RC_CONDITION_ADD_ADDRESS: return "AddAddress "; + case RC_CONDITION_ADD_HITS: return "AddHits "; + case RC_CONDITION_SUB_HITS: return "SubHits "; + case RC_CONDITION_RESET_NEXT_IF: return "ResetNextIf "; + case RC_CONDITION_AND_NEXT: return "AndNext "; + case RC_CONDITION_OR_NEXT: return "OrNext "; + default: return "Unknown"; + } +} + +static const char* type_string(char type) { + switch (type) { + case RC_OPERAND_ADDRESS: return "Mem"; + case RC_OPERAND_DELTA: return "Delta"; + case RC_OPERAND_CONST: return "Value"; + case RC_OPERAND_FP: return "Float"; + case RC_OPERAND_FUNC: return "Func"; + case RC_OPERAND_PRIOR: return "Prior"; + case RC_OPERAND_BCD: return "BCD"; + case RC_OPERAND_INVERTED: return "Inverted"; + case RC_OPERAND_RECALL: return "Recall"; + default: return "Unknown"; + } +} + +static const char* size_string(char size) { + switch (size) { + case RC_MEMSIZE_8_BITS: return "8-bit"; + case RC_MEMSIZE_16_BITS: return "16-bit"; + case RC_MEMSIZE_24_BITS: return "24-bit"; + case RC_MEMSIZE_32_BITS: return "32-bit"; + case RC_MEMSIZE_LOW: return "Lower4"; + case RC_MEMSIZE_HIGH: return "Upper4"; + case RC_MEMSIZE_BIT_0: return "Bit0"; + case RC_MEMSIZE_BIT_1: return "Bit1"; + case RC_MEMSIZE_BIT_2: return "Bit2"; + case RC_MEMSIZE_BIT_3: return "Bit3"; + case RC_MEMSIZE_BIT_4: return "Bit4"; + case RC_MEMSIZE_BIT_5: return "Bit5"; + case RC_MEMSIZE_BIT_6: return "Bit6"; + case RC_MEMSIZE_BIT_7: return "Bit7"; + case RC_MEMSIZE_BITCOUNT: return "BitCount"; + case RC_MEMSIZE_16_BITS_BE: return "16-bit BE"; + case RC_MEMSIZE_24_BITS_BE: return "24-bit BE"; + case RC_MEMSIZE_32_BITS_BE: return "32-bit BE"; + case RC_MEMSIZE_VARIABLE: return "Variable"; + default: return "Unknown"; + } +} + +static const char* operator_string(char oper) { + switch (oper) { + case RC_OPERATOR_NONE: return ""; + case RC_OPERATOR_AND: return "&"; + case RC_OPERATOR_XOR: return "^"; + case RC_OPERATOR_MULT: return "*"; + case RC_OPERATOR_DIV: return "/"; + case RC_OPERATOR_MOD: return "%"; + case RC_OPERATOR_ADD: return "+"; + case RC_OPERATOR_SUB: return "-"; + case RC_OPERATOR_EQ: return "="; + case RC_OPERATOR_NE: return "!="; + case RC_OPERATOR_GE: return ">="; + case RC_OPERATOR_GT: return ">"; + case RC_OPERATOR_LE: return "<="; + case RC_OPERATOR_LT: return "<"; + default: return "?"; + } +} + +static void append_condition(char result[], size_t result_size, const rc_condition_t* cond) { + const char* flag = flag_string(cond->type); + const char* src_type = type_string(cond->operand1.type); + const char* tgt_type = type_string(cond->operand2.type); + const char* cmp = operator_string(cond->oper); + char val1[32], val2[32]; + + if (rc_operand_is_memref(&cond->operand1)) + snprintf(val1, sizeof(val1), "%s 0x%06x", size_string(cond->operand1.size), cond->operand1.value.memref->address); + else + snprintf(val1, sizeof(val1), "0x%06x", cond->operand1.value.num); + + if (rc_operand_is_memref(&cond->operand2)) + snprintf(val2, sizeof(val2), "%s 0x%06x", size_string(cond->operand2.size), cond->operand2.value.memref->address); + else + snprintf(val2, sizeof(val2), "0x%06x", cond->operand2.value.num); + + const size_t message_len = strlen(result); + result += message_len; + result_size -= message_len; + + if (cond->oper == RC_OPERATOR_NONE) + snprintf(result, result_size, ": %s%s %s", flag, src_type, val1); + else + snprintf(result, result_size, ": %s%s %s %s %s %s", flag, src_type, val1, cmp, tgt_type, val2); +} + +static void append_invalid_condition(char result[], size_t result_size, const rc_condset_t* condset) { + if (strncmp(result, "Condition ", 10) == 0) { + int index = atoi(&result[10]); + const rc_condition_t* cond; + for (cond = condset->conditions; cond; cond = cond->next) { + if (--index == 0) { + const size_t error_length = strlen(result); + append_condition(result + error_length, result_size - error_length, cond); + return; + } + } + } +} + +static void append_invalid_trigger_condition(char result[], size_t result_size, const rc_trigger_t* trigger) { + if (strncmp(result, "Alt", 3) == 0) { + int index = atoi(&result[3]); + const rc_condset_t* condset; + for (condset = trigger->alternative; condset; condset = condset->next) { + if (--index == 0) { + result += 4; + result_size -= 4; + while (isdigit(*result)) { + ++result; + --result_size; + } + ++result; + --result_size; + append_invalid_condition(result, result_size, condset); + return; + } + } + } + else if (strncmp(result, "Core ", 5) == 0) { + append_invalid_condition(result + 5, result_size - 5, trigger->requirement); + } + else { + append_invalid_condition(result, result_size, trigger->requirement); + } +} + +static int validate_trigger(const char* trigger, char result[], size_t result_size, uint32_t console_id) { + char* buffer; + rc_trigger_t* compiled; + int success = 0; + + int ret = rc_trigger_size(trigger); + if (ret < 0) { + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_trigger(buffer, trigger, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { + snprintf(result, result_size, "%d OK", ret); + success = 1; + } + else { + append_invalid_trigger_condition(result, result_size, compiled); + } + + free(buffer); + return success; +} + +static int validate_leaderboard(const char* leaderboard, char result[], const size_t result_size, uint32_t console_id) +{ + char* buffer; + rc_lboard_t* compiled; + int success = 0; + + int ret = rc_lboard_size(leaderboard); + if (ret < 0) { + /* generic problem parsing the leaderboard, attempt to report where */ + const char* start = leaderboard; + char part[4] = { 0,0,0,0 }; + do { + char* next = strstr(start, "::"); + part[0] = toupper((int)start[0]); + part[1] = toupper((int)start[1]); + part[2] = toupper((int)start[2]); + start += 4; + + if (strcmp(part, "VAL") == 0) { + int ret2 = rc_value_size(start); + if (ret2 == ret) { + snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); + return 0; + } + } + else { + int ret2 = rc_trigger_size(start); + if (ret2 == ret) { + snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); + return 0; + } + } + + if (!next) + break; + + start = next + 2; + } while (1); + + snprintf(result, result_size, "%s", rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_lboard(buffer, leaderboard, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else { + snprintf(result, result_size, "STA: "); + success = rc_validate_trigger_for_console(&compiled->start, result + 5, result_size - 5, console_id); + if (!success) { + append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->start); + } + else { + snprintf(result, result_size, "SUB: "); + success = rc_validate_trigger_for_console(&compiled->submit, result + 5, result_size - 5, console_id); + if (!success) { + append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->submit); + } + else { + snprintf(result, result_size, "CAN: "); + success = rc_validate_trigger_for_console(&compiled->cancel, result + 5, result_size - 5, console_id); + + if (!success) { + append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->cancel); + } + else { + snprintf(result, result_size, "%d OK", ret); + } + } + } + } + + free(buffer); + return success; +} + +static int validate_macros(const rc_richpresence_t* richpresence, const char* script, char result[], const size_t result_size) +{ + const uint16_t RC_FORMAT_UNKNOWN_MACRO = 103; /* enum not exposed by header */ + + rc_richpresence_display_t* display = richpresence->first_display; + while (display != NULL) { + rc_richpresence_display_part_t* part = display->display; + while (part != NULL) { + if (part->display_type == RC_FORMAT_UNKNOWN_MACRO) { + /* include opening parenthesis to prevent partial match */ + size_t macro_len = strchr(part->text, '(') - part->text + 1; + + /* find the display portion of the script */ + const char* ptr = script; + int line = 1; + while (strncmp(ptr, "Display:", 8) != 0) { + while (*ptr != '\n') + ++ptr; + + ++line; + ++ptr; + } + + /* find the first matching reference to the unknown macro */ + do { + while (*ptr != '@') { + if (*ptr == '\n') + ++line; + + if (*ptr == '\0') { + /* unexpected, but prevent potential infinite loop */ + snprintf(result, result_size, "Unknown macro \"%.*s\"", (int)(macro_len - 1), part->text); + return 0; + } + + ++ptr; + } + ++ptr; + + if (strncmp(ptr, part->text, macro_len) == 0) { + snprintf(result, result_size, "Line %d: Unknown macro \"%.*s\"", line, (int)(macro_len - 1), part->text); + return 0; + } + } while (1); + } + + part = part->next; + } + + display = display->next; + } + + return 1; +} + +static int validate_richpresence(const char* script, char result[], const size_t result_size, uint32_t console_id) +{ + char* buffer; + rc_richpresence_t* compiled; + int lines; + int success = 0; + + int ret = rc_richpresence_size_lines(script, &lines); + if (ret < 0) { + snprintf(result, result_size, "Line %d: %s", lines, rc_error_str(ret)); + return 0; + } + + buffer = (char*)malloc(ret + 4); + if (!buffer) { + snprintf(result, result_size, "malloc failed"); + return 0; + } + + memset(buffer + ret, 0xCD, 4); + compiled = rc_parse_richpresence(buffer, script, NULL, 0); + if (compiled == NULL) { + snprintf(result, result_size, "parse failed"); + } + else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { + snprintf(result, result_size, "write past end of buffer"); + } + else { + const rc_richpresence_display_t* display; + int index = 1; + for (display = compiled->first_display; display; display = display->next) { + const size_t prefix_length = snprintf(result, result_size, "Display%d: ", index++); + success = rc_validate_trigger_for_console(&display->trigger, result + prefix_length, result_size - prefix_length, console_id); + if (!success) + break; + } + + if (success) + success = rc_validate_memrefs_for_console(rc_richpresence_get_memrefs(compiled), result, result_size, console_id); + if (success) + success = validate_macros(compiled, script, result, result_size); + if (success) + snprintf(result, result_size, "%d OK", ret); + } + + free(buffer); + return success; +} + +static void validate_richpresence_file(const char* richpresence_file, char result[], const size_t result_size) +{ + char* file_contents; + size_t file_size; + FILE* file; + + fopen_s(&file, richpresence_file, "rb"); + if (!file) { + snprintf(result, result_size, "could not open file"); + return; + } + + fseek(file, 0, SEEK_END); + file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + file_contents = (char*)malloc(file_size + 1); + if (!file_contents) { + snprintf(result, result_size, "malloc failed"); + return; + } + + fread(file_contents, 1, file_size, file); + file_contents[file_size] = '\0'; + fclose(file); + + validate_richpresence(file_contents, result, result_size, 0); + + free(file_contents); +} + +static int validate_patchdata_file(const char* patchdata_file, const char* filename, int errors_only) { + char* file_contents; + size_t file_size; + FILE* file; + rc_api_fetch_game_data_response_t fetch_game_data_response; + int result; + size_t i; + char file_title[256]; + char buffer[256]; + int success = 1; + + fopen_s(&file, patchdata_file, "rb"); + if (!file) { + printf("File: %s: could not open file\n", filename); + return 0; + } + + fseek(file, 0, SEEK_END); + file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + file_contents = (char*)malloc(file_size + 1); + fread(file_contents, 1, file_size, file); + file_contents[file_size] = '\0'; + fclose(file); + + /* rc_api_process_fetch_game_data_response expects the PatchData to be + * a subobject, but the DLL strips that when writing to the RACache. + * if it looks like the nested object, wrap it again */ + if (strncmp(file_contents, "{\"ID\":", 6) == 0) { + char* expanded_contents = (char*)malloc(file_size + 15); + memcpy(expanded_contents, "{\"PatchData\":", 13); + memcpy(&expanded_contents[13], file_contents, file_size); + expanded_contents[file_size + 13] = '}'; + expanded_contents[file_size + 14] = '\0'; + + free(file_contents); + file_contents = expanded_contents; + } + + result = rc_api_process_fetch_game_data_response(&fetch_game_data_response, file_contents); + if (result != RC_OK) { + if (fetch_game_data_response.response.error_message) + printf("File: %s: %s\n", filename, fetch_game_data_response.response.error_message); + else + printf("File: %s: %s\n", filename, rc_error_str(result)); + return 0; + } + + free(file_contents); + + snprintf(file_title, sizeof(file_title), "File: %s: %s\n", filename, fetch_game_data_response.title); + + if (fetch_game_data_response.rich_presence_script && *fetch_game_data_response.rich_presence_script) { + result = validate_richpresence(fetch_game_data_response.rich_presence_script, + buffer, sizeof(buffer), fetch_game_data_response.console_id); + success &= result; + + if (!result || !errors_only) { + printf("%s", file_title); + file_title[0] = '\0'; + + printf(" rich presence %d: %s\n", fetch_game_data_response.id, buffer); + } + } + + for (i = 0; i < fetch_game_data_response.num_achievements; ++i) { + const char* trigger = fetch_game_data_response.achievements[i].definition; + result = validate_trigger(trigger, buffer, sizeof(buffer), fetch_game_data_response.console_id); + success &= result; + + if (!result || !errors_only) { + if (file_title[0]) { + printf("%s", file_title); + file_title[0] = '\0'; + } + + printf(" achievement %d%s: %s\n", fetch_game_data_response.achievements[i].id, + (fetch_game_data_response.achievements[i].category == 3) ? "" : " (Unofficial)", buffer); + } + } + + for (i = 0; i < fetch_game_data_response.num_leaderboards; ++i) { + result = validate_leaderboard(fetch_game_data_response.leaderboards[i].definition, + buffer, sizeof(buffer), fetch_game_data_response.console_id); + success &= result; + + if (!result || !errors_only) { + if (file_title[0]) { + printf("%s", file_title); + file_title[0] = '\0'; + } + + printf(" leaderboard %d: %s\n", fetch_game_data_response.leaderboards[i].id, buffer); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); + + return success; +} + +#ifdef _MSC_VER +static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { + WIN32_FIND_DATA fdFile; + HANDLE hFind = NULL; + int need_newline = 0; + + char filename[MAX_PATH]; + snprintf(filename, sizeof(filename), "%s\\*.json", patchdata_directory); + + if ((hFind = FindFirstFile(filename, &fdFile)) == INVALID_HANDLE_VALUE) { + printf("failed to open directory"); + return; + } + + do + { + if (!(fdFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + if (need_newline) { + printf("\n"); + need_newline = 0; + } + + snprintf(filename, sizeof(filename), "%s\\%s", patchdata_directory, fdFile.cFileName); + if (!validate_patchdata_file(filename, fdFile.cFileName, errors_only) || !errors_only) + need_newline = 1; + } + } while(FindNextFile(hFind, &fdFile)); + + FindClose(hFind); +} +#else +static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { + struct dirent* entry; + char* filename; + size_t filename_len; + char path[2048]; + int need_newline = 0; + + DIR* dir = opendir(patchdata_directory); + if (!dir) { + printf("failed to open directory"); + return; + } + + while ((entry = readdir(dir)) != NULL) { + filename = entry->d_name; + filename_len = strlen(filename); + if (filename_len > 5 && stricmp(&filename[filename_len - 5], ".json") == 0) { + if (need_newline) { + printf("\n"); + need_newline = 0; + } + + sprintf(path, "%s/%s", patchdata_directory, filename); + if (!validate_patchdata_file(path, filename, errors_only) || !errors_only) + need_newline = 1; + } + } + + closedir(dir); +} +#endif + +static int usage() { + printf("validator [type] [data]\n" + "\n" + "where [type] is one of the following:\n" + " a achievement, [data] = trigger definition\n" + " l leaderboard, [data] = leaderboard definition\n" + " r rich presence, [data] = path to rich presence script\n" + " f patchdata file, [data] = path to patchdata json file\n" + " d patchdata directory, [data] = path to directory containing one or more patchdata json files\n" + " e same as 'd', but only reports errors\n" + ); + + return 0; +} + +int main(int argc, char* argv[]) { + char buffer[256]; + + if (argc < 3) + return usage(); + + switch (argv[1][0]) + { + case 'a': + validate_trigger(argv[2], buffer, sizeof(buffer), 0); + printf("Achievement: %s\n", buffer); + break; + + case 'l': + validate_leaderboard(argv[2], buffer, sizeof(buffer), 0); + printf("Leaderboard: %s\n", buffer); + break; + + case 'r': + validate_richpresence_file(argv[2], buffer, sizeof(buffer)); + printf("Rich Presence: %s\n", buffer); + break; + + case 'f': + validate_patchdata_file(argv[2], argv[2], 0); + break; + + case 'd': + printf("Directory: %s:\n", argv[2]); + validate_patchdata_directory(argv[2], 0); + break; + + case 'e': + printf("Directory: %s:\n", argv[2]); + validate_patchdata_directory(argv[2], 1); + break; + + default: + return usage(); + } + + return 0; +} diff --git a/src/rcheevos/validator/validator.vcxproj b/src/rcheevos/validator/validator.vcxproj new file mode 100644 index 0000000000..c108bea51a --- /dev/null +++ b/src/rcheevos/validator/validator.vcxproj @@ -0,0 +1,152 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {16FABFA7-A2EC-4CD0-9E04-50315A2BB613} + rcheevostest + Application + MultiByte + v143 + + + v142 + + + v141 + + + + true + + + false + true + + + true + + + false + true + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + + + Console + + + + + Level3 + Disabled + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + + + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + + + true + true + Console + + + + + Level3 + MaxSpeed + true + true + true + true + $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/rcheevos/validator/validator.vcxproj.filters b/src/rcheevos/validator/validator.vcxproj.filters new file mode 100644 index 0000000000..52206787d6 --- /dev/null +++ b/src/rcheevos/validator/validator.vcxproj.filters @@ -0,0 +1,82 @@ + + + + + {125bb837-6e5b-4a66-a607-e2250b31f108} + + + {DD82F9EB-1066-40C6-9568-D5C7EEB2A49B} + + + {efdc689a-471b-432e-8ecb-dde3a3202949} + + + {8570c19e-e882-409f-84d8-b87887371c26} + + + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + src\rcheevos + + + + src\rhash + + + src\rapi + + + src\rapi + + + src\rcheevos + + + src\rcheevos + + + src + + + src + + + + + src\rcheevos + + + src\rcheevos + + + \ No newline at end of file From b5dd6261366b4ec8fe49b24d9ee1dbfc02bac474 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:15:13 +0100 Subject: [PATCH 29/41] Rcheevos v2 --- src/rcheevos/src/rcheevos/alloc.c | 312 ++++ src/rcheevos/src/rcheevos/condition.c | 754 +++++++++ src/rcheevos/src/rcheevos/condset.c | 770 +++++++++ src/rcheevos/src/rcheevos/consoleinfo.c | 1215 ++++++++++++++ src/rcheevos/src/rcheevos/format.c | 330 ++++ src/rcheevos/src/rcheevos/lboard.c | 287 ++++ src/rcheevos/src/rcheevos/memref.c | 805 ++++++++++ src/rcheevos/src/rcheevos/operand.c | 607 +++++++ src/rcheevos/src/rcheevos/rc_internal.h | 390 +++++ .../src/rcheevos/rc_runtime_types.natvis | 541 +++++++ src/rcheevos/src/rcheevos/rc_validate.c | 1370 ++++++++++++++++ src/rcheevos/src/rcheevos/rc_validate.h | 18 + src/rcheevos/src/rcheevos/richpresence.c | 922 +++++++++++ src/rcheevos/src/rcheevos/runtime.c | 852 ++++++++++ src/rcheevos/src/rcheevos/runtime_progress.c | 1073 +++++++++++++ src/rcheevos/src/rcheevos/trigger.c | 344 ++++ src/rcheevos/src/rcheevos/value.c | 933 +++++++++++ src/rcheevos/src/rhash/aes.c | 480 ++++++ src/rcheevos/src/rhash/aes.h | 49 + src/rcheevos/src/rhash/cdreader.c | 838 ++++++++++ src/rcheevos/src/rhash/hash.c | 1402 +++++++++++++++++ src/rcheevos/src/rhash/hash_disc.c | 1340 ++++++++++++++++ src/rcheevos/src/rhash/hash_encrypted.c | 566 +++++++ src/rcheevos/src/rhash/hash_rom.c | 426 +++++ src/rcheevos/src/rhash/hash_zip.c | 460 ++++++ src/rcheevos/src/rhash/md5.c | 382 +++++ src/rcheevos/src/rhash/md5.h | 91 ++ src/rcheevos/src/rhash/rc_hash_internal.h | 116 ++ 28 files changed, 17673 insertions(+) create mode 100644 src/rcheevos/src/rcheevos/alloc.c create mode 100644 src/rcheevos/src/rcheevos/condition.c create mode 100644 src/rcheevos/src/rcheevos/condset.c create mode 100644 src/rcheevos/src/rcheevos/consoleinfo.c create mode 100644 src/rcheevos/src/rcheevos/format.c create mode 100644 src/rcheevos/src/rcheevos/lboard.c create mode 100644 src/rcheevos/src/rcheevos/memref.c create mode 100644 src/rcheevos/src/rcheevos/operand.c create mode 100644 src/rcheevos/src/rcheevos/rc_internal.h create mode 100644 src/rcheevos/src/rcheevos/rc_runtime_types.natvis create mode 100644 src/rcheevos/src/rcheevos/rc_validate.c create mode 100644 src/rcheevos/src/rcheevos/rc_validate.h create mode 100644 src/rcheevos/src/rcheevos/richpresence.c create mode 100644 src/rcheevos/src/rcheevos/runtime.c create mode 100644 src/rcheevos/src/rcheevos/runtime_progress.c create mode 100644 src/rcheevos/src/rcheevos/trigger.c create mode 100644 src/rcheevos/src/rcheevos/value.c create mode 100644 src/rcheevos/src/rhash/aes.c create mode 100644 src/rcheevos/src/rhash/aes.h create mode 100644 src/rcheevos/src/rhash/cdreader.c create mode 100644 src/rcheevos/src/rhash/hash.c create mode 100644 src/rcheevos/src/rhash/hash_disc.c create mode 100644 src/rcheevos/src/rhash/hash_encrypted.c create mode 100644 src/rcheevos/src/rhash/hash_rom.c create mode 100644 src/rcheevos/src/rhash/hash_zip.c create mode 100644 src/rcheevos/src/rhash/md5.c create mode 100644 src/rcheevos/src/rhash/md5.h create mode 100644 src/rcheevos/src/rhash/rc_hash_internal.h diff --git a/src/rcheevos/src/rcheevos/alloc.c b/src/rcheevos/src/rcheevos/alloc.c new file mode 100644 index 0000000000..5dbf20cb0f --- /dev/null +++ b/src/rcheevos/src/rcheevos/alloc.c @@ -0,0 +1,312 @@ +#include "rc_internal.h" + +#include +#include + +void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset) +{ + void* data; + + /* if we have a real buffer, then allocate the data there */ + if (pointer) + return rc_alloc(pointer, offset, size, alignment, NULL, scratch_object_pointer_offset); + + /* update how much space will be required in the real buffer */ + { + const int32_t aligned_offset = (*offset + alignment - 1) & ~(alignment - 1); + *offset += (aligned_offset - *offset); + *offset += size; + } + + /* find a scratch buffer to hold the temporary data */ + data = rc_buffer_alloc(&scratch->buffer, size); + if (!data) { + *offset = RC_OUT_OF_MEMORY; + return NULL; + } + + return data; +} + +void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset) { + void* ptr; + + *offset = (*offset + alignment - 1) & ~(alignment - 1); + + if (pointer != 0) { + /* valid buffer, grab the next chunk */ + ptr = (void*)((uint8_t*)pointer + *offset); + } + else if (scratch != 0 && scratch_object_pointer_offset < sizeof(scratch->objs)) { + /* only allocate one instance of each object type (indentified by scratch_object_pointer_offset) */ + void** scratch_object_pointer = (void**)((uint8_t*)&scratch->objs + scratch_object_pointer_offset); + ptr = *scratch_object_pointer; + if (!ptr) { + int32_t used; + ptr = *scratch_object_pointer = rc_alloc_scratch(NULL, &used, size, alignment, scratch, -1); + } + } + else { + /* nowhere to get memory from, return NULL */ + ptr = NULL; + } + + *offset += size; + return ptr; +} + +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length) { + int32_t used = 0; + char* ptr; + + rc_scratch_string_t** next = &parse->scratch.strings; + while (*next) { + int diff = strncmp(text, (*next)->value, length); + if (diff == 0) { + diff = (*next)->value[length]; + if (diff == 0) + return (*next)->value; + } + + if (diff < 0) + next = &(*next)->left; + else + next = &(*next)->right; + } + + *next = (rc_scratch_string_t*)rc_alloc_scratch(NULL, &used, sizeof(rc_scratch_string_t), RC_ALIGNOF(rc_scratch_string_t), &parse->scratch, RC_OFFSETOF(parse->scratch.objs, __rc_scratch_string_t)); + ptr = (char*)rc_alloc_scratch(parse->buffer, &parse->offset, (uint32_t)length + 1, RC_ALIGNOF(char), &parse->scratch, -1); + + if (!ptr || !*next) { + if (parse->offset >= 0) + parse->offset = RC_OUT_OF_MEMORY; + + return NULL; + } + + memcpy(ptr, text, length); + ptr[length] = '\0'; + + (*next)->left = NULL; + (*next)->right = NULL; + (*next)->value = ptr; + + return ptr; +} + +void rc_init_preparse_state(rc_preparse_state_t* preparse) +{ + rc_init_parse_state(&preparse->parse, NULL); + rc_init_parse_state_memrefs(&preparse->parse, &preparse->memrefs); +} + +void rc_destroy_preparse_state(rc_preparse_state_t* preparse) +{ + rc_destroy_parse_state(&preparse->parse); +} + +void rc_preparse_alloc_memrefs(rc_memrefs_t* memrefs, rc_preparse_state_t* preparse) +{ + const uint32_t num_memrefs = rc_memrefs_count_memrefs(&preparse->memrefs); + const uint32_t num_modified_memrefs = rc_memrefs_count_modified_memrefs(&preparse->memrefs); + + if (preparse->parse.offset < 0) + return; + + if (memrefs) { + memset(memrefs, 0, sizeof(*memrefs)); + preparse->parse.memrefs = memrefs; + } + + if (num_memrefs) { + rc_memref_t* memref_items = RC_ALLOC_ARRAY(rc_memref_t, num_memrefs, &preparse->parse); + + if (memrefs) { + memrefs->memrefs.capacity = num_memrefs; + memrefs->memrefs.items = memref_items; + } + } + + if (num_modified_memrefs) { + rc_modified_memref_t* modified_memref_items = + RC_ALLOC_ARRAY(rc_modified_memref_t, num_modified_memrefs, &preparse->parse); + + if (memrefs) { + memrefs->modified_memrefs.capacity = num_modified_memrefs; + memrefs->modified_memrefs.items = modified_memref_items; + } + } + + /* when preparsing, this structure will be allocated at the end. when it's allocated earlier + * in the buffer, it could be followed by something aligned at 8 bytes. force the offset to + * an 8-byte boundary */ + if (!memrefs) { + rc_alloc(preparse->parse.buffer, &preparse->parse.offset, 0, 8, &preparse->parse.scratch, 0); + } +} + +static uint32_t rc_preparse_array_size(uint32_t needed, uint32_t minimum) +{ + while (minimum < needed) + minimum <<= 1; + + return minimum; +} + +void rc_preparse_reserve_memrefs(rc_preparse_state_t* preparse, rc_memrefs_t* memrefs) +{ + uint32_t num_memrefs = rc_memrefs_count_memrefs(&preparse->memrefs); + uint32_t num_modified_memrefs = rc_memrefs_count_modified_memrefs(&preparse->memrefs); + uint32_t available; + + if (preparse->parse.offset < 0) + return; + + if (num_memrefs) { + rc_memref_list_t* memref_list = &memrefs->memrefs; + while (memref_list->count == memref_list->capacity) { + if (!memref_list->next) + break; + + memref_list = memref_list->next; + } + + available = memref_list->capacity - memref_list->count; + if (available < num_memrefs) { + rc_memref_list_t* new_memref_list = (rc_memref_list_t*)calloc(1, sizeof(rc_memref_list_t)); + if (!new_memref_list) + return; + + new_memref_list->capacity = rc_preparse_array_size(num_memrefs - available, 16); + new_memref_list->items = (rc_memref_t*)malloc(new_memref_list->capacity * sizeof(rc_memref_t)); + new_memref_list->allocated = 1; + memref_list->next = new_memref_list; + } + } + + if (num_modified_memrefs) { + rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + while (modified_memref_list->count == modified_memref_list->capacity) { + if (!modified_memref_list->next) + break; + + modified_memref_list = modified_memref_list->next; + } + + available = modified_memref_list->capacity - modified_memref_list->count; + if (available < num_modified_memrefs) { + rc_modified_memref_list_t* new_modified_memref_list = (rc_modified_memref_list_t*)calloc(1, sizeof(rc_modified_memref_list_t)); + if (!new_modified_memref_list) + return; + + new_modified_memref_list->capacity = rc_preparse_array_size(num_modified_memrefs - available, 8); + new_modified_memref_list->items = (rc_modified_memref_t*)malloc(new_modified_memref_list->capacity * sizeof(rc_modified_memref_t)); + new_modified_memref_list->allocated = 1; + modified_memref_list->next = new_modified_memref_list; + } + } + + preparse->parse.memrefs = memrefs; +} + +static void rc_preparse_sync_operand(rc_operand_t* operand, rc_parse_state_t* parse, const rc_memrefs_t* memrefs) +{ + if (rc_operand_is_memref(operand) || rc_operand_is_recall(operand)) { + const rc_memref_t* src_memref = operand->value.memref; + + if (src_memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + for (; modified_memref_list; modified_memref_list = modified_memref_list->next) { + const rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_end = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_end; ++modified_memref) { + if ((const rc_modified_memref_t*)src_memref == modified_memref) { + rc_modified_memref_t* dst_modified_memref = rc_alloc_modified_memref(parse, modified_memref->memref.value.size, + &modified_memref->parent, modified_memref->modifier_type, &modified_memref->modifier); + + operand->value.memref = &dst_modified_memref->memref; + return; + } + } + } + } + else { + const rc_memref_list_t* memref_list = &memrefs->memrefs; + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + + for (; memref < memref_end; ++memref) { + if (src_memref == memref) { + operand->value.memref = rc_alloc_memref(parse, memref->address, memref->value.size); + return; + } + } + } + } + } +} + +void rc_preparse_copy_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs) +{ + const rc_memref_list_t* memref_list = &memrefs->memrefs; + const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + + for (; memref < memref_end; ++memref) + rc_alloc_memref(parse, memref->address, memref->value.size); + } + + for (; modified_memref_list; modified_memref_list = modified_memref_list->next) { + rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_end = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_end; ++modified_memref) { + rc_preparse_sync_operand(&modified_memref->parent, parse, memrefs); + rc_preparse_sync_operand(&modified_memref->modifier, parse, memrefs); + + rc_alloc_modified_memref(parse, modified_memref->memref.value.size, + &modified_memref->parent, modified_memref->modifier_type, &modified_memref->modifier); + } + } +} + +void rc_reset_parse_state(rc_parse_state_t* parse, void* buffer) +{ + parse->buffer = buffer; + + parse->offset = 0; + parse->memrefs = NULL; + parse->existing_memrefs = NULL; + parse->variables = NULL; + parse->measured_target = 0; + parse->lines_read = 0; + parse->addsource_oper = RC_OPERATOR_NONE; + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + parse->remember.type = RC_OPERAND_NONE; + parse->is_value = 0; + parse->has_required_hits = 0; + parse->measured_as_percent = 0; + parse->ignore_non_parse_errors = 0; + + parse->scratch.strings = NULL; +} + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer) +{ + /* could use memset here, but rc_parse_state_t contains a 512 byte buffer that doesn't need to be initialized */ + rc_buffer_init(&parse->scratch.buffer); + memset(&parse->scratch.objs, 0, sizeof(parse->scratch.objs)); + + rc_reset_parse_state(parse, buffer); +} + +void rc_destroy_parse_state(rc_parse_state_t* parse) +{ + rc_buffer_destroy(&parse->scratch.buffer); +} diff --git a/src/rcheevos/src/rcheevos/condition.c b/src/rcheevos/src/rcheevos/condition.c new file mode 100644 index 0000000000..ff6da02bf8 --- /dev/null +++ b/src/rcheevos/src/rcheevos/condition.c @@ -0,0 +1,754 @@ +#include "rc_internal.h" + +#include +#include +#include + +static int rc_test_condition_compare(uint32_t value1, uint32_t value2, uint8_t oper) { + switch (oper) { + case RC_OPERATOR_EQ: return value1 == value2; + case RC_OPERATOR_NE: return value1 != value2; + case RC_OPERATOR_LT: return value1 < value2; + case RC_OPERATOR_LE: return value1 <= value2; + case RC_OPERATOR_GT: return value1 > value2; + case RC_OPERATOR_GE: return value1 >= value2; + default: return 1; + } +} + +static uint8_t rc_condition_determine_comparator(const rc_condition_t* self) { + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + break; + + default: + /* not a comparison. should not be getting compared. but if it is, legacy behavior was to return 1 */ + return RC_PROCESSING_COMPARE_ALWAYS_TRUE; + } + + if ((self->operand1.type == RC_OPERAND_ADDRESS || self->operand1.type == RC_OPERAND_DELTA) && + /* TODO: allow modified memref comparisons */ + self->operand1.value.memref->value.memref_type == RC_MEMREF_TYPE_MEMREF && !rc_operand_is_float(&self->operand1)) { + /* left side is an integer memory reference */ + int needs_translate = (self->operand1.size != self->operand1.value.memref->value.size); + + if (self->operand2.type == RC_OPERAND_CONST) { + /* right side is a constant */ + if (self->operand1.type == RC_OPERAND_ADDRESS) + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_CONST; + + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_CONST; + } + else if ((self->operand2.type == RC_OPERAND_ADDRESS || self->operand2.type == RC_OPERAND_DELTA) && + self->operand2.value.memref->value.memref_type == RC_MEMREF_TYPE_MEMREF && !rc_operand_is_float(&self->operand2)) { + /* right side is an integer memory reference */ + const int is_same_memref = (self->operand1.value.memref == self->operand2.value.memref); + needs_translate |= (self->operand2.size != self->operand2.value.memref->value.size); + + if (self->operand1.type == RC_OPERAND_ADDRESS) { + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref && !needs_translate) { + /* comparing a memref to itself, will evaluate to a constant */ + return rc_test_condition_compare(0, 0, self->oper) ? RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF; + } + + assert(self->operand2.type == RC_OPERAND_DELTA); + + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_DELTA; + } + } + else { + assert(self->operand1.type == RC_OPERAND_DELTA); + + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_MEMREF; + } + } + } + } + } + + if (self->operand1.type == RC_OPERAND_CONST && self->operand2.type == RC_OPERAND_CONST) { + /* comparing constants will always generate a constant result */ + return rc_test_condition_compare(self->operand1.value.num, self->operand2.value.num, self->oper) ? + RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return RC_PROCESSING_COMPARE_DEFAULT; +} + +static int rc_parse_operator(const char** memaddr) { + const char* oper = *memaddr; + + switch (*oper) { + case '=': + ++(*memaddr); + (*memaddr) += (**memaddr == '='); + return RC_OPERATOR_EQ; + + case '!': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_NE; + } + /* fall through */ + default: + return RC_INVALID_OPERATOR; + + case '<': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_LE; + } + + ++(*memaddr); + return RC_OPERATOR_LT; + + case '>': + if (oper[1] == '=') { + (*memaddr) += 2; + return RC_OPERATOR_GE; + } + + ++(*memaddr); + return RC_OPERATOR_GT; + + case '*': + ++(*memaddr); + return RC_OPERATOR_MULT; + + case '/': + ++(*memaddr); + return RC_OPERATOR_DIV; + + case '&': + ++(*memaddr); + return RC_OPERATOR_AND; + + case '^': + ++(*memaddr); + return RC_OPERATOR_XOR; + + case '%': + ++(*memaddr); + return RC_OPERATOR_MOD; + + case '+': + ++(*memaddr); + return RC_OPERATOR_ADD; + + case '-': + ++(*memaddr); + return RC_OPERATOR_SUB; + + case '\0':/* end of string */ + case '_': /* next condition */ + case 'S': /* next condset */ + case ')': /* end of macro */ + case '$': /* maximum of values */ + /* valid condition separator, condition may not have an operator */ + return RC_OPERATOR_NONE; + } +} + +void rc_condition_convert_to_operand(const rc_condition_t* condition, rc_operand_t* operand, rc_parse_state_t* parse) { + if (condition->oper == RC_OPERATOR_NONE) { + if (operand != &condition->operand1) + memcpy(operand, &condition->operand1, sizeof(*operand)); + } + else { + uint8_t new_size = RC_MEMSIZE_32_BITS; + if (rc_operand_is_float(&condition->operand1) || rc_operand_is_float(&condition->operand2)) + new_size = RC_MEMSIZE_FLOAT; + + /* NOTE: this makes the operand include the modification, but we have to also + * leave the modification in the condition so the condition reflects the actual + * definition. This doesn't affect the evaluation logic since this method is only + * called for combining conditions and Measured, and the Measured handling function + * ignores the operator assuming it's been handled by a modified memref chain */ + operand->value.memref = (rc_memref_t*)rc_alloc_modified_memref(parse, + new_size, &condition->operand1, condition->oper, &condition->operand2); + + /* not actually an address, just a non-delta memref read */ + operand->type = operand->memref_access_type = RC_OPERAND_ADDRESS; + + operand->size = new_size; + } +} + +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse) { + rc_condition_t * self = RC_ALLOC(rc_condition_t, parse); + rc_parse_condition_internal(self, memaddr, parse); + return (parse->offset < 0) ? NULL : self; +} + +void rc_parse_condition_internal(rc_condition_t* self, const char** memaddr, rc_parse_state_t* parse) { + const char* aux; + int result; + int can_modify = 0; + + aux = *memaddr; + self->current_hits = 0; + self->is_true = 0; + self->optimized_comparator = RC_PROCESSING_COMPARE_DEFAULT; + + if (*aux != 0 && aux[1] == ':') { + switch (*aux) { + case 'p': case 'P': self->type = RC_CONDITION_PAUSE_IF; break; + case 'r': case 'R': self->type = RC_CONDITION_RESET_IF; break; + case 'a': case 'A': self->type = RC_CONDITION_ADD_SOURCE; can_modify = 1; break; + case 'b': case 'B': self->type = RC_CONDITION_SUB_SOURCE; can_modify = 1; break; + case 'c': case 'C': self->type = RC_CONDITION_ADD_HITS; break; + case 'd': case 'D': self->type = RC_CONDITION_SUB_HITS; break; + case 'n': case 'N': self->type = RC_CONDITION_AND_NEXT; break; + case 'o': case 'O': self->type = RC_CONDITION_OR_NEXT; break; + case 'm': case 'M': self->type = RC_CONDITION_MEASURED; break; + case 'q': case 'Q': self->type = RC_CONDITION_MEASURED_IF; break; + case 'i': case 'I': self->type = RC_CONDITION_ADD_ADDRESS; can_modify = 1; break; + case 't': case 'T': self->type = RC_CONDITION_TRIGGER; break; + case 'k': case 'K': self->type = RC_CONDITION_REMEMBER; can_modify = 1; break; + case 'z': case 'Z': self->type = RC_CONDITION_RESET_NEXT_IF; break; + case 'g': case 'G': + parse->measured_as_percent = 1; + self->type = RC_CONDITION_MEASURED; + break; + + /* e f h j l s u v w x y */ + default: + parse->offset = RC_INVALID_CONDITION_TYPE; + return; + } + + aux += 2; + } + else { + self->type = RC_CONDITION_STANDARD; + } + + result = rc_parse_operand(&self->operand1, &aux, parse); + if (result < 0) { + parse->offset = result; + return; + } + + result = rc_parse_operator(&aux); + if (result < 0) { + parse->offset = result; + return; + } + + self->oper = (uint8_t)result; + + if (self->oper == RC_OPERATOR_NONE) { + /* non-modifying statements must have a second operand */ + if (!can_modify) { + /* measured does not require a second operand when used in a value */ + if (self->type != RC_CONDITION_MEASURED && !parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_OPERATOR; + return; + } + } + + /* provide dummy operand of '1' and no required hits */ + rc_operand_set_const(&self->operand2, 1); + self->required_hits = 0; + *memaddr = aux; + return; + } + + if (can_modify && !rc_operator_is_modifying(self->oper)) { + /* comparison operators are not valid on modifying statements */ + switch (self->type) { + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_ADDRESS: + /* prevent parse errors on legacy achievements where a condition was present before changing the type */ + self->oper = RC_OPERATOR_NONE; + break; + + default: + if (!parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_OPERATOR; + return; + } + break; + } + } + + result = rc_parse_operand(&self->operand2, &aux, parse); + if (result < 0) { + parse->offset = result; + return; + } + + if (self->oper == RC_OPERATOR_NONE) { + /* if operator is none, explicitly clear out the right side */ + rc_operand_set_const(&self->operand2, 0); + } + + if (*aux == '(') { + char* end; + self->required_hits = (unsigned)strtoul(++aux, &end, 10); + + if (end == aux || *end != ')') { + parse->offset = RC_INVALID_REQUIRED_HITS; + return; + } + + /* if operator is none, explicitly clear out the required hits */ + if (self->oper == RC_OPERATOR_NONE) + self->required_hits = 0; + else + parse->has_required_hits = 1; + + aux = end + 1; + } + else if (*aux == '.') { + char* end; + self->required_hits = (unsigned)strtoul(++aux, &end, 10); + + if (end == aux || *end != '.') { + parse->offset = RC_INVALID_REQUIRED_HITS; + return; + } + + /* if operator is none, explicitly clear out the required hits */ + if (self->oper == RC_OPERATOR_NONE) + self->required_hits = 0; + else + parse->has_required_hits = 1; + + aux = end + 1; + } + else { + self->required_hits = 0; + } + + if (parse->buffer != 0) + self->optimized_comparator = rc_condition_determine_comparator(self); + + *memaddr = aux; +} + +void rc_condition_update_parse_state(rc_condition_t* condition, rc_parse_state_t* parse) { + /* type of values in the chain are determined by the parent. + * the last element of a chain is determined by the operand + * + * 1 + 1.5 + 1.75 + 1.0 => (int)1 + (int)1 + (int)1 + (float)1 = (float)4.0 + * 1.0 + 1.5 + 1.75 + 1.0 => (float)1.0 + (float)1.5 + (float)1.75 + (float)1.0 = (float)5.25 + * 1.0 + 1.5 + 1.75 + 1 => (float)1.0 + (float)1.5 + (float)1.75 + (int)1 = (int)5 + */ + + switch (condition->type) { + case RC_CONDITION_ADD_ADDRESS: + if (condition->oper != RC_OPERAND_NONE) + rc_condition_convert_to_operand(condition, &parse->indirect_parent, parse); + else + memcpy(&parse->indirect_parent, &condition->operand1, sizeof(parse->indirect_parent)); + + break; + + case RC_CONDITION_ADD_SOURCE: + if (parse->addsource_parent.type == RC_OPERAND_NONE) { + rc_condition_convert_to_operand(condition, &parse->addsource_parent, parse); + } + else { + rc_operand_t cond_operand; + /* type determined by parent */ + const uint8_t new_size = rc_operand_is_float(&parse->addsource_parent) ? RC_MEMSIZE_FLOAT : RC_MEMSIZE_32_BITS; + + rc_condition_convert_to_operand(condition, &cond_operand, parse); + rc_operand_addsource(&cond_operand, parse, new_size); + memcpy(&parse->addsource_parent, &cond_operand, sizeof(cond_operand)); + } + + parse->addsource_oper = RC_OPERATOR_ADD_ACCUMULATOR; + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + + case RC_CONDITION_SUB_SOURCE: + if (parse->addsource_parent.type == RC_OPERAND_NONE) { + rc_condition_convert_to_operand(condition, &parse->addsource_parent, parse); + parse->addsource_oper = RC_OPERATOR_SUB_PARENT; + } + else { + rc_operand_t cond_operand; + /* type determined by parent */ + const uint8_t new_size = rc_operand_is_float(&parse->addsource_parent) ? RC_MEMSIZE_FLOAT : RC_MEMSIZE_32_BITS; + + if (parse->addsource_oper == RC_OPERATOR_ADD_ACCUMULATOR && !rc_operand_is_memref(&parse->addsource_parent)) { + /* if the previous element was a constant we have to turn it into a memref by adding zero */ + rc_modified_memref_t* memref; + rc_operand_t zero; + rc_operand_set_const(&zero, 0); + memref = rc_alloc_modified_memref(parse, + parse->addsource_parent.size, &parse->addsource_parent, RC_OPERATOR_ADD_ACCUMULATOR, &zero); + parse->addsource_parent.value.memref = (rc_memref_t*)memref; + parse->addsource_parent.type = RC_OPERAND_ADDRESS; + } + else if (parse->addsource_oper == RC_OPERATOR_SUB_PARENT) { + /* if the previous element was also a SubSource, we have to insert a 0 and start subtracting from there */ + rc_modified_memref_t* negate; + rc_operand_t zero; + + if (rc_operand_is_float(&parse->addsource_parent)) + rc_operand_set_float_const(&zero, 0.0); + else + rc_operand_set_const(&zero, 0); + + negate = rc_alloc_modified_memref(parse, new_size, &parse->addsource_parent, RC_OPERATOR_SUB_PARENT, &zero); + parse->addsource_parent.value.memref = (rc_memref_t*)negate; + parse->addsource_parent.size = zero.size; + } + + /* subtract the condition from the chain */ + parse->addsource_oper = rc_operand_is_memref(&parse->addsource_parent) ? RC_OPERATOR_SUB_ACCUMULATOR : RC_OPERATOR_SUB_PARENT; + rc_condition_convert_to_operand(condition, &cond_operand, parse); + rc_operand_addsource(&cond_operand, parse, new_size); + memcpy(&parse->addsource_parent, &cond_operand, sizeof(cond_operand)); + + /* indicate the next value can be added to the chain */ + parse->addsource_oper = RC_OPERATOR_ADD_ACCUMULATOR; + } + + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + + case RC_CONDITION_REMEMBER: + if (condition->operand1.type == RC_OPERAND_RECALL && + condition->oper == RC_OPERATOR_NONE && + parse->addsource_parent.type == RC_OPERAND_NONE && + parse->indirect_parent.type == RC_OPERAND_NONE) { + /* Remembering {recall} without any modifications is a no-op */ + break; + } + + rc_condition_convert_to_operand(condition, &condition->operand1, parse); + + if (parse->addsource_parent.type != RC_OPERAND_NONE) { + /* type determined by leaf */ + rc_operand_addsource(&condition->operand1, parse, condition->operand1.size); + condition->operand1.is_combining = 1; + } + + memcpy(&parse->remember, &condition->operand1, sizeof(parse->remember)); + + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + + case RC_CONDITION_MEASURED: + /* Measured condition can have modifiers in values */ + if (parse->is_value) { + switch (condition->oper) { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_DIV: + case RC_OPERATOR_MULT: + case RC_OPERATOR_MOD: + case RC_OPERATOR_ADD: + case RC_OPERATOR_SUB: + rc_condition_convert_to_operand(condition, &condition->operand1, parse); + break; + + default: + break; + } + } + + /* fallthrough */ /* to default */ + + default: + if (parse->addsource_parent.type != RC_OPERAND_NONE) { + /* type determined by leaf */ + if (parse->addsource_oper == RC_OPERATOR_ADD_ACCUMULATOR) + parse->addsource_oper = RC_OPERATOR_ADD; + + rc_operand_addsource(&condition->operand1, parse, condition->operand1.size); + condition->operand1.is_combining = 1; + + if (parse->buffer) + condition->optimized_comparator = rc_condition_determine_comparator(condition); + } + + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + } +} + +static const rc_modified_memref_t* rc_operand_get_modified_memref(const rc_operand_t* operand) { + if (!rc_operand_is_memref(operand)) + return NULL; + + if (operand->value.memref->value.memref_type != RC_MEMREF_TYPE_MODIFIED_MEMREF) + return NULL; + + return (rc_modified_memref_t*)operand->value.memref; +} + +/* rc_condition_update_parse_state will mutate the operand1 to point at the modified memref + * containing the accumulated result up until that point. this function returns the original + * unmodified operand1 from parsing the definition. + */ +const rc_operand_t* rc_condition_get_real_operand1(const rc_condition_t* self) { + const rc_operand_t* operand = &self->operand1; + const rc_modified_memref_t* modified_memref; + + if (operand->is_combining) { + /* operand = "previous + current" - extract current */ + const rc_modified_memref_t* combining_modified_memref = rc_operand_get_modified_memref(operand); + if (combining_modified_memref) + operand = &combining_modified_memref->modifier; + } + + /* modifying operators are merged into an rc_modified_memref_t + * if operand1 is a modified memref, assume it's been merged with the right side and + * extract the parent which is the actual operand1. */ + modified_memref = rc_operand_get_modified_memref(operand); + if (modified_memref) { + if (modified_memref->modifier_type == RC_OPERATOR_INDIRECT_READ) { + /* if the modified memref is an indirect read, the parent is the indirect + * address and the modifier is the offset. the actual size and address are + * stored in the modified memref - use it */ + } else if (rc_operator_is_modifying(self->oper) && self->oper != RC_OPERATOR_NONE) { + /* operand = "parent*modifier" - extract parent.modifier will already be in operand2 */ + operand = &modified_memref->parent; + } + } + + return operand; +} + +int rc_condition_is_combining(const rc_condition_t* self) { + switch (self->type) { + case RC_CONDITION_STANDARD: + case RC_CONDITION_PAUSE_IF: + case RC_CONDITION_RESET_IF: + case RC_CONDITION_MEASURED_IF: + case RC_CONDITION_TRIGGER: + case RC_CONDITION_MEASURED: + return 0; + + default: + return 1; + } +} + +static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { + const uint32_t value1 = self->operand1.value.memref->value.value; + const uint32_t value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const(rc_condition_t* self) { + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const uint32_t value1 = (memref1->changed) ? memref1->prior : memref1->value; + const uint32_t value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref(rc_condition_t* self) { + const uint32_t value1 = self->operand1.value.memref->value.value; + const uint32_t value2 = self->operand2.value.memref->value.value; + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->value, memref->prior, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->prior, memref->value, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const uint32_t value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const uint32_t value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref_transformed(rc_condition_t* self) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = self->operand2.value.memref->value.value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->prior; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->prior; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { + rc_typed_value_t value1, value2; + + /* use an optimized comparator whenever possible */ + switch (self->optimized_comparator) { + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST: + return rc_test_condition_compare_memref_to_const(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA: + return rc_test_condition_compare_memref_to_delta(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF: + return rc_test_condition_compare_memref_to_memref(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST: + return rc_test_condition_compare_delta_to_const(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF: + return rc_test_condition_compare_delta_to_memref(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_memref_to_const_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED: + return rc_test_condition_compare_memref_to_delta_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_memref_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_delta_to_const_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_delta_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_ALWAYS_TRUE: + return 1; + case RC_PROCESSING_COMPARE_ALWAYS_FALSE: + return 0; + default: + rc_evaluate_operand(&value1, &self->operand1, eval_state); + break; + } + + rc_evaluate_operand(&value2, &self->operand2, eval_state); + + return rc_typed_value_compare(&value1, &value2, self->oper); +} + +void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state) { + rc_typed_value_t amount; + + rc_evaluate_operand(value, &self->operand1, eval_state); + rc_evaluate_operand(&amount, &self->operand2, eval_state); + + rc_typed_value_combine(value, &amount, self->oper); +} diff --git a/src/rcheevos/src/rcheevos/condset.c b/src/rcheevos/src/rcheevos/condset.c new file mode 100644 index 0000000000..df067a24f5 --- /dev/null +++ b/src/rcheevos/src/rcheevos/condset.c @@ -0,0 +1,770 @@ +#include "rc_internal.h" + +#include /* memcpy */ + +enum { + RC_CONDITION_CLASSIFICATION_COMBINING, + RC_CONDITION_CLASSIFICATION_PAUSE, + RC_CONDITION_CLASSIFICATION_RESET, + RC_CONDITION_CLASSIFICATION_HITTARGET, + RC_CONDITION_CLASSIFICATION_MEASURED, + RC_CONDITION_CLASSIFICATION_OTHER, + RC_CONDITION_CLASSIFICATION_INDIRECT +}; + +static int rc_classify_condition(const rc_condition_t* cond) { + switch (cond->type) { + case RC_CONDITION_PAUSE_IF: + return RC_CONDITION_CLASSIFICATION_PAUSE; + + case RC_CONDITION_RESET_IF: + return RC_CONDITION_CLASSIFICATION_RESET; + + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + /* these are handled by rc_modified_memref_t */ + return RC_CONDITION_CLASSIFICATION_INDIRECT; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_REMEMBER: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + return RC_CONDITION_CLASSIFICATION_COMBINING; + + case RC_CONDITION_MEASURED: + case RC_CONDITION_MEASURED_IF: + /* even if not measuring a hit target, we still want to evaluate it every frame */ + return RC_CONDITION_CLASSIFICATION_MEASURED; + + default: + if (cond->required_hits != 0) + return RC_CONDITION_CLASSIFICATION_HITTARGET; + + return RC_CONDITION_CLASSIFICATION_OTHER; + } +} + +static int32_t rc_classify_conditions(rc_condset_t* self, const char* memaddr, const rc_parse_state_t* parent_parse) { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_condition_t condition; + int classification; + uint32_t index = 0; + uint32_t chain_length = 1; + + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + parse.ignore_non_parse_errors = parent_parse->ignore_non_parse_errors; + + do { + rc_parse_condition_internal(&condition, &memaddr, &parse); + + if (parse.offset < 0) { + rc_destroy_parse_state(&parse); + return parse.offset; + } + + ++index; + + classification = rc_classify_condition(&condition); + switch (classification) { + case RC_CONDITION_CLASSIFICATION_COMBINING: + ++chain_length; + continue; + + case RC_CONDITION_CLASSIFICATION_INDIRECT: + ++self->num_indirect_conditions; + continue; + + case RC_CONDITION_CLASSIFICATION_PAUSE: + self->num_pause_conditions += chain_length; + break; + + case RC_CONDITION_CLASSIFICATION_RESET: + self->num_reset_conditions += chain_length; + break; + + case RC_CONDITION_CLASSIFICATION_HITTARGET: + self->num_hittarget_conditions += chain_length; + break; + + case RC_CONDITION_CLASSIFICATION_MEASURED: + self->num_measured_conditions += chain_length; + break; + + default: + self->num_other_conditions += chain_length; + break; + } + + chain_length = 1; + } while (*memaddr++ == '_'); + + /* any combining conditions that don't actually feed into a non-combining condition + * need to still have space allocated for them. put them in "other" to match the + * logic in rc_find_next_classification */ + self->num_other_conditions += chain_length - 1; + + rc_destroy_parse_state(&parse); + + return (int32_t)index; +} + +static int rc_find_next_classification(const char* memaddr) { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_condition_t condition; + int classification; + + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + do { + rc_parse_condition_internal(&condition, &memaddr, &parse); + if (parse.offset < 0) + break; + + classification = rc_classify_condition(&condition); + switch (classification) { + case RC_CONDITION_CLASSIFICATION_COMBINING: + case RC_CONDITION_CLASSIFICATION_INDIRECT: + break; + + default: + return classification; + } + } while (*memaddr++ == '_'); + + return RC_CONDITION_CLASSIFICATION_OTHER; +} + +static void rc_condition_update_recall_operand(rc_operand_t* operand, const rc_operand_t* remember) +{ + if (operand->type == RC_OPERAND_RECALL) { + if (rc_operand_type_is_memref(operand->memref_access_type) && operand->value.memref == NULL) { + memcpy(operand, remember, sizeof(*remember)); + operand->memref_access_type = operand->type; + operand->type = RC_OPERAND_RECALL; + } + } + else if (rc_operand_is_memref(operand) && operand->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + rc_modified_memref_t* modified_memref = (rc_modified_memref_t*)operand->value.memref; + rc_condition_update_recall_operand(&modified_memref->parent, remember); + rc_condition_update_recall_operand(&modified_memref->modifier, remember); + } +} + +static void rc_update_condition_pause_remember(rc_condset_t* self) { + rc_operand_t* pause_remember = NULL; + rc_condition_t* condition; + rc_condition_t* pause_conditions; + const rc_condition_t* end_pause_condition; + + /* ASSERT: pause conditions are first conditions */ + pause_conditions = rc_condset_get_conditions(self); + end_pause_condition = pause_conditions + self->num_pause_conditions; + + for (condition = pause_conditions; condition < end_pause_condition; ++condition) { + if (condition->type == RC_CONDITION_REMEMBER) { + pause_remember = &condition->operand1; + } + else if (pause_remember == NULL) { + /* if we picked up a non-pause remember, discard it */ + if (condition->operand1.type == RC_OPERAND_RECALL && + rc_operand_type_is_memref(condition->operand1.memref_access_type)) { + condition->operand1.value.memref = NULL; + } + + if (condition->operand2.type == RC_OPERAND_RECALL && + rc_operand_type_is_memref(condition->operand2.memref_access_type)) { + condition->operand2.value.memref = NULL; + } + } + } + + if (pause_remember) { + for (condition = self->conditions; condition; condition = condition->next) { + if (condition >= end_pause_condition) { + /* if we didn't find a remember for a non-pause condition, use the last pause remember */ + rc_condition_update_recall_operand(&condition->operand1, pause_remember); + rc_condition_update_recall_operand(&condition->operand2, pause_remember); + } + + /* Anything after this point will have already been handled */ + if (condition->type == RC_CONDITION_REMEMBER) + break; + } + } +} + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse) { + rc_condset_with_trailing_conditions_t* condset_with_conditions; + rc_condset_t local_condset; + rc_condset_t* self; + rc_condition_t condition; + rc_condition_t* conditions; + rc_condition_t** next; + rc_condition_t* pause_conditions = NULL; + rc_condition_t* reset_conditions = NULL; + rc_condition_t* hittarget_conditions = NULL; + rc_condition_t* measured_conditions = NULL; + rc_condition_t* other_conditions = NULL; + rc_condition_t* indirect_conditions = NULL; + int classification, combining_classification = RC_CONDITION_CLASSIFICATION_COMBINING; + uint32_t measured_target = 0; + int32_t result; + + if (**memaddr == 'S' || **memaddr == 's' || !**memaddr) { + /* empty group - editor allows it, so we have to support it */ + self = RC_ALLOC(rc_condset_t, parse); + memset(self, 0, sizeof(*self)); + return self; + } + + memset(&local_condset, 0, sizeof(local_condset)); + result = rc_classify_conditions(&local_condset, *memaddr, parse); + if (result < 0) { + parse->offset = result; + return NULL; + } + + condset_with_conditions = RC_ALLOC_WITH_TRAILING(rc_condset_with_trailing_conditions_t, + rc_condition_t, conditions, result, parse); + if (parse->offset < 0) + return NULL; + + self = (rc_condset_t*)condset_with_conditions; + memcpy(self, &local_condset, sizeof(local_condset)); + conditions = &condset_with_conditions->conditions[0]; + + if (parse->buffer) { + pause_conditions = conditions; + conditions += self->num_pause_conditions; + + reset_conditions = conditions; + conditions += self->num_reset_conditions; + + hittarget_conditions = conditions; + conditions += self->num_hittarget_conditions; + + measured_conditions = conditions; + conditions += self->num_measured_conditions; + + other_conditions = conditions; + conditions += self->num_other_conditions; + + indirect_conditions = conditions; + } + + next = &self->conditions; + + /* each condition set has a functionally new recall accumulator */ + parse->remember.type = RC_OPERAND_NONE; + + for (;;) { + rc_parse_condition_internal(&condition, memaddr, parse); + + if (parse->offset < 0) + return NULL; + + if (condition.oper == RC_OPERATOR_NONE && !parse->ignore_non_parse_errors) { + switch (condition.type) { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_REMEMBER: + /* these conditions don't require a right hand side (implied *1) */ + break; + + case RC_CONDITION_MEASURED: + /* right hand side is not required when Measured is used in a value */ + if (parse->is_value) + break; + /* fallthrough */ /* to default */ + + default: + parse->offset = RC_INVALID_OPERATOR; + return NULL; + } + } + + switch (condition.type) { + case RC_CONDITION_MEASURED: + if (measured_target != 0) { + /* multiple Measured flags cannot exist in the same group */ + if (!parse->ignore_non_parse_errors) { + parse->offset = RC_MULTIPLE_MEASURED; + return NULL; + } + } + else if (parse->is_value) { + measured_target = (uint32_t)-1; + if (!rc_operator_is_modifying(condition.oper)) { + /* measuring comparison in a value results in a tally (hit count). set target to MAX_INT */ + condition.required_hits = measured_target; + } + } + else if (condition.required_hits != 0) { + measured_target = condition.required_hits; + } + else if (condition.operand2.type == RC_OPERAND_CONST) { + measured_target = condition.operand2.value.num; + } + else if (condition.operand2.type == RC_OPERAND_FP) { + measured_target = (unsigned)condition.operand2.value.dbl; + } + else if (!parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_MEASURED_TARGET; + return NULL; + } + + if (parse->measured_target && measured_target != parse->measured_target) { + /* multiple Measured flags in separate groups must have the same target */ + if (!parse->ignore_non_parse_errors) { + parse->offset = RC_MULTIPLE_MEASURED; + return NULL; + } + } + + parse->measured_target = measured_target; + break; + + case RC_CONDITION_STANDARD: + case RC_CONDITION_TRIGGER: + /* these flags are not allowed in value expressions */ + if (parse->is_value && !parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_VALUE_FLAG; + return NULL; + } + break; + } + + rc_condition_update_parse_state(&condition, parse); + + if (parse->buffer) { + classification = rc_classify_condition(&condition); + if (classification == RC_CONDITION_CLASSIFICATION_COMBINING) { + if (combining_classification == RC_CONDITION_CLASSIFICATION_COMBINING) { + if (**memaddr == '_') + combining_classification = rc_find_next_classification(&(*memaddr)[1]); /* skip over '_' */ + else + combining_classification = RC_CONDITION_CLASSIFICATION_OTHER; + } + + classification = combining_classification; + } + else { + combining_classification = RC_CONDITION_CLASSIFICATION_COMBINING; + } + + switch (classification) { + case RC_CONDITION_CLASSIFICATION_PAUSE: + memcpy(pause_conditions, &condition, sizeof(condition)); + *next = pause_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_RESET: + memcpy(reset_conditions, &condition, sizeof(condition)); + *next = reset_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_HITTARGET: + memcpy(hittarget_conditions, &condition, sizeof(condition)); + *next = hittarget_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_MEASURED: + memcpy(measured_conditions, &condition, sizeof(condition)); + *next = measured_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_INDIRECT: + memcpy(indirect_conditions, &condition, sizeof(condition)); + *next = indirect_conditions++; + break; + + default: + memcpy(other_conditions, &condition, sizeof(condition)); + *next = other_conditions++; + break; + } + + next = &(*next)->next; + } + + if (**memaddr != '_') + break; + + (*memaddr)++; + } + + *next = NULL; + + self->has_pause = self->num_pause_conditions > 0; + if (self->has_pause && parse->buffer && parse->remember.type != RC_OPERAND_NONE) + rc_update_condition_pause_remember(self); + + return self; +} + +static uint8_t rc_condset_evaluate_condition_no_add_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + /* evaluate the current condition */ + uint8_t cond_valid = (uint8_t)rc_test_condition(condition, eval_state); + condition->is_true = cond_valid; + + if (eval_state->reset_next) { + /* previous ResetNextIf resets the hit count on this condition and prevents it from being true */ + eval_state->was_cond_reset |= (condition->current_hits != 0); + + condition->current_hits = 0; + cond_valid = 0; + } + else { + /* apply chained logic flags */ + cond_valid &= eval_state->and_next; + cond_valid |= eval_state->or_next; + + if (cond_valid) { + /* true conditions should update their hit count */ + eval_state->has_hits = 1; + + if (condition->required_hits == 0) { + /* no target hit count, just keep tallying */ + ++condition->current_hits; + } + else if (condition->current_hits < condition->required_hits) { + /* target hit count hasn't been met, tally and revalidate - only true if hit count becomes met */ + ++condition->current_hits; + cond_valid = (condition->current_hits == condition->required_hits); + } + else { + /* target hit count has been met, do nothing */ + } + } + else if (condition->current_hits > 0) { + /* target has been true in the past, if the hit target is met, consider it true now */ + eval_state->has_hits = 1; + cond_valid = (condition->current_hits == condition->required_hits); + } + } + + /* reset chained logic flags for the next condition */ + eval_state->and_next = 1; + eval_state->or_next = 0; + + return cond_valid; +} + +static uint32_t rc_condset_evaluate_total_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + uint32_t total_hits = condition->current_hits; + + if (condition->required_hits != 0) { + /* if the condition has a target hit count, we have to recalculate cond_valid including the AddHits counter */ + const int32_t signed_hits = (int32_t)condition->current_hits + eval_state->add_hits; + total_hits = (signed_hits >= 0) ? (uint32_t)signed_hits : 0; + } + else { + /* no target hit count. we can't tell if the add_hits value is from this frame or not, so ignore it. + complex condition will only be true if the current condition is true */ + } + + eval_state->add_hits = 0; + + return total_hits; +} + +static uint8_t rc_condset_evaluate_condition(rc_condition_t* condition, rc_eval_state_t* eval_state) { + uint8_t cond_valid = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + + if (eval_state->add_hits != 0 && condition->required_hits != 0) { + uint32_t total_hits = rc_condset_evaluate_total_hits(condition, eval_state); + cond_valid = (total_hits >= condition->required_hits); + } + + /* reset logic flags for the next condition */ + eval_state->reset_next = 0; + + return cond_valid; +} + +static void rc_condset_evaluate_standard(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + eval_state->is_true &= cond_valid; + eval_state->is_primed &= cond_valid; + + if (!cond_valid && eval_state->can_short_curcuit) + eval_state->stop_processing = 1; +} + +static void rc_condset_evaluate_pause_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + if (cond_valid) { + eval_state->is_paused = 1; + + /* set cannot be valid if it's paused */ + eval_state->is_true = eval_state->is_primed = 0; + + /* as soon as we find a PauseIf that evaluates to true, stop processing the rest of the group */ + eval_state->stop_processing = 1; + } + else if (condition->required_hits == 0) { + /* PauseIf didn't evaluate true, and doesn't have a HitCount, reset the HitCount to indicate the condition didn't match */ + condition->current_hits = 0; + } + else { + /* PauseIf has a HitCount that hasn't been met, ignore it for now. */ + } +} + +static void rc_condset_evaluate_reset_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + if (cond_valid) { + /* flag the condition as being responsible for the reset */ + /* make sure not to modify bit0, as we use bitwise-and operators to combine truthiness */ + condition->is_true |= 0x02; + + /* set cannot be valid if we've hit a reset condition */ + eval_state->is_true = eval_state->is_primed = 0; + + /* let caller know to reset all hit counts */ + eval_state->was_reset = 1; + + /* can stop processing once an active ResetIf is encountered */ + eval_state->stop_processing = 1; + } +} + +static void rc_condset_evaluate_trigger(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + eval_state->is_true &= cond_valid; +} + +static void rc_condset_evaluate_measured(rc_condition_t* condition, rc_eval_state_t* eval_state) { + if (condition->required_hits == 0) { + rc_condset_evaluate_standard(condition, eval_state); + + /* Measured condition without a hit target measures the value of the left operand */ + rc_evaluate_operand(&eval_state->measured_value, &condition->operand1, eval_state); + eval_state->measured_from_hits = 0; + } + else { + /* this largely mimicks rc_condset_evaluate_condition, but captures the total_hits */ + uint8_t cond_valid = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + const uint32_t total_hits = rc_condset_evaluate_total_hits(condition, eval_state); + + cond_valid = (total_hits >= condition->required_hits); + eval_state->is_true &= cond_valid; + eval_state->is_primed &= cond_valid; + + /* if there is a hit target, capture the current hits */ + eval_state->measured_value.value.u32 = total_hits; + eval_state->measured_value.type = RC_VALUE_TYPE_UNSIGNED; + eval_state->measured_from_hits = 1; + + /* reset logic flags for the next condition */ + eval_state->reset_next = 0; + } +} + +static void rc_condset_evaluate_measured_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + eval_state->is_true &= cond_valid; + eval_state->is_primed &= cond_valid; + eval_state->can_measure &= cond_valid; +} + +static void rc_condset_evaluate_add_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + + eval_state->add_hits += (int32_t)condition->current_hits; + + /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + eval_state->reset_next = 0; +} + +static void rc_condset_evaluate_sub_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + + eval_state->add_hits -= (int32_t)condition->current_hits; + + /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + eval_state->reset_next = 0; +} + +static void rc_condset_evaluate_reset_next_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + eval_state->reset_next = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); +} + +static void rc_condset_evaluate_and_next(rc_condition_t* condition, rc_eval_state_t* eval_state) { + eval_state->and_next = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); +} + +static void rc_condset_evaluate_or_next(rc_condition_t* condition, rc_eval_state_t* eval_state) { + eval_state->or_next = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); +} + +void rc_test_condset_internal(rc_condition_t* condition, uint32_t num_conditions, + rc_eval_state_t* eval_state, int can_short_circuit) { + const rc_condition_t* condition_end = condition + num_conditions; + for (; condition < condition_end; ++condition) { + switch (condition->type) { + case RC_CONDITION_STANDARD: + rc_condset_evaluate_standard(condition, eval_state); + break; + case RC_CONDITION_PAUSE_IF: + rc_condset_evaluate_pause_if(condition, eval_state); + break; + case RC_CONDITION_RESET_IF: + rc_condset_evaluate_reset_if(condition, eval_state); + break; + case RC_CONDITION_TRIGGER: + rc_condset_evaluate_trigger(condition, eval_state); + break; + case RC_CONDITION_MEASURED: + rc_condset_evaluate_measured(condition, eval_state); + break; + case RC_CONDITION_MEASURED_IF: + rc_condset_evaluate_measured_if(condition, eval_state); + break; + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_REMEMBER: + /* these are handled by rc_modified_memref_t */ + break; + case RC_CONDITION_ADD_HITS: + rc_condset_evaluate_add_hits(condition, eval_state); + break; + case RC_CONDITION_SUB_HITS: + rc_condset_evaluate_sub_hits(condition, eval_state); + break; + case RC_CONDITION_RESET_NEXT_IF: + rc_condset_evaluate_reset_next_if(condition, eval_state); + break; + case RC_CONDITION_AND_NEXT: + rc_condset_evaluate_and_next(condition, eval_state); + break; + case RC_CONDITION_OR_NEXT: + rc_condset_evaluate_or_next(condition, eval_state); + break; + default: + eval_state->stop_processing = 1; + eval_state->is_true = eval_state->is_primed = 0; + break; + } + + if (eval_state->stop_processing && can_short_circuit) + break; + } +} + +rc_condition_t* rc_condset_get_conditions(rc_condset_t* self) { + if (self->conditions) + return RC_GET_TRAILING(self, rc_condset_with_trailing_conditions_t, rc_condition_t, conditions); + + return NULL; +} + +int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state) { + rc_condition_t* conditions; + + /* reset the processing state before processing each condset. do not reset the result state. */ + eval_state->measured_value.type = RC_VALUE_TYPE_NONE; + eval_state->add_hits = 0; + eval_state->is_true = 1; + eval_state->is_primed = 1; + eval_state->is_paused = 0; + eval_state->can_measure = 1; + eval_state->measured_from_hits = 0; + eval_state->and_next = 1; + eval_state->or_next = 0; + eval_state->reset_next = 0; + eval_state->stop_processing = 0; + + /* the conditions array is allocated immediately after the rc_condset_t, without a separate pointer */ + conditions = rc_condset_get_conditions(self); + + if (self->num_pause_conditions) { + /* one or more Pause conditions exist. if any of them are true (eval_state->is_paused), + * stop processing this group */ + rc_test_condset_internal(conditions, self->num_pause_conditions, eval_state, 1); + + self->is_paused = eval_state->is_paused; + if (self->is_paused) { + /* condset is paused. stop processing immediately. */ + return 0; + } + + conditions += self->num_pause_conditions; + } + + if (self->num_reset_conditions) { + /* one or more Reset conditions exists. if any of them are true (eval_state->was_reset), + * we'll skip some of the later steps */ + rc_test_condset_internal(conditions, self->num_reset_conditions, eval_state, eval_state->can_short_curcuit); + conditions += self->num_reset_conditions; + } + + if (self->num_hittarget_conditions) { + /* one or more hit target conditions exists. these must be processed every frame, + * unless their hit count is going to be reset */ + if (!eval_state->was_reset) + rc_test_condset_internal(conditions, self->num_hittarget_conditions, eval_state, 0); + + conditions += self->num_hittarget_conditions; + } + + if (self->num_measured_conditions) { + /* IMPORTANT: reset hit counts on these conditions before processing them so + * the MeasuredIf logic and Measured value are correct. + * NOTE: a ResetIf in a later alt group may not have been processed yet. + * Accept that as a weird edge case, and just recommend the user + * move the ResetIf if it becomes a problem. */ + if (eval_state->was_reset) { + int i; + for (i = 0; i < self->num_measured_conditions; ++i) + conditions[i].current_hits = 0; + } + + /* the measured value must be calculated every frame, even if hit counts will be reset */ + rc_test_condset_internal(conditions, self->num_measured_conditions, eval_state, 0); + conditions += self->num_measured_conditions; + + if (eval_state->measured_value.type != RC_VALUE_TYPE_NONE) { + /* if a MeasuredIf was false (!eval_state->can_measure), or the measured + * value is a hitcount and a ResetIf is true, zero out the measured value */ + if (!eval_state->can_measure || + (eval_state->measured_from_hits && eval_state->was_reset)) { + eval_state->measured_value.type = RC_VALUE_TYPE_UNSIGNED; + eval_state->measured_value.value.u32 = 0; + } + } + } + + if (self->num_other_conditions) { + /* the remaining conditions only need to be evaluated if the rest of the condset is true */ + if (eval_state->is_true) + rc_test_condset_internal(conditions, self->num_other_conditions, eval_state, eval_state->can_short_curcuit); + /* something else is false. if we can't short circuit, and there wasn't a reset, we still need to evaluate these */ + else if (!eval_state->can_short_curcuit && !eval_state->was_reset) + rc_test_condset_internal(conditions, self->num_other_conditions, eval_state, eval_state->can_short_curcuit); + } + + return eval_state->is_true; +} + +void rc_reset_condset(rc_condset_t* self) { + rc_condition_t* condition; + + for (condition = self->conditions; condition != 0; condition = condition->next) { + condition->current_hits = 0; + } +} diff --git a/src/rcheevos/src/rcheevos/consoleinfo.c b/src/rcheevos/src/rcheevos/consoleinfo.c new file mode 100644 index 0000000000..aeecb38d2c --- /dev/null +++ b/src/rcheevos/src/rcheevos/consoleinfo.c @@ -0,0 +1,1215 @@ +#include "rc_consoles.h" + +#include + +const char* rc_console_name(uint32_t console_id) +{ + switch (console_id) + { + case RC_CONSOLE_3DO: + return "3DO"; + + case RC_CONSOLE_AMIGA: + return "Amiga"; + + case RC_CONSOLE_AMSTRAD_PC: + return "Amstrad CPC"; + + case RC_CONSOLE_APPLE_II: + return "Apple II"; + + case RC_CONSOLE_ARCADE: + return "Arcade"; + + case RC_CONSOLE_ARCADIA_2001: + return "Arcadia 2001"; + + case RC_CONSOLE_ARDUBOY: + return "Arduboy"; + + case RC_CONSOLE_ATARI_2600: + return "Atari 2600"; + + case RC_CONSOLE_ATARI_5200: + return "Atari 5200"; + + case RC_CONSOLE_ATARI_7800: + return "Atari 7800"; + + case RC_CONSOLE_ATARI_JAGUAR: + return "Atari Jaguar"; + + case RC_CONSOLE_ATARI_JAGUAR_CD: + return "Atari Jaguar CD"; + + case RC_CONSOLE_ATARI_LYNX: + return "Atari Lynx"; + + case RC_CONSOLE_ATARI_ST: + return "Atari ST"; + + case RC_CONSOLE_CASSETTEVISION: + return "CassetteVision"; + + case RC_CONSOLE_CDI: + return "CD-I"; + + case RC_CONSOLE_COLECOVISION: + return "ColecoVision"; + + case RC_CONSOLE_COMMODORE_64: + return "Commodore 64"; + + case RC_CONSOLE_DREAMCAST: + return "Dreamcast"; + + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + return "Elektor TV Games Computer"; + + case RC_CONSOLE_EVENTS: + return "Events"; + + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return "Fairchild Channel F"; + + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + return "Famicom Disk System"; + + case RC_CONSOLE_FM_TOWNS: + return "FM Towns"; + + case RC_CONSOLE_GAME_AND_WATCH: + return "Game & Watch"; + + case RC_CONSOLE_GAMEBOY: + return "GameBoy"; + + case RC_CONSOLE_GAMEBOY_ADVANCE: + return "GameBoy Advance"; + + case RC_CONSOLE_GAMEBOY_COLOR: + return "GameBoy Color"; + + case RC_CONSOLE_GAMECUBE: + return "GameCube"; + + case RC_CONSOLE_GAME_GEAR: + return "Game Gear"; + + case RC_CONSOLE_HUBS: + return "Hubs"; + + case RC_CONSOLE_INTELLIVISION: + return "Intellivision"; + + case RC_CONSOLE_INTERTON_VC_4000: + return "Interton VC 4000"; + + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + return "Magnavox Odyssey 2"; + + case RC_CONSOLE_MASTER_SYSTEM: + return "Master System"; + + case RC_CONSOLE_MEGA_DRIVE: + return "Sega Genesis"; + + case RC_CONSOLE_MEGADUCK: + return "Mega Duck"; + + case RC_CONSOLE_MS_DOS: + return "MS-DOS"; + + case RC_CONSOLE_MSX: + return "MSX"; + + case RC_CONSOLE_NEO_GEO_CD: + return "Neo Geo CD"; + + case RC_CONSOLE_NEOGEO_POCKET: + return "Neo Geo Pocket"; + + case RC_CONSOLE_NINTENDO: + return "Nintendo Entertainment System"; + + case RC_CONSOLE_NINTENDO_64: + return "Nintendo 64"; + + case RC_CONSOLE_NINTENDO_DS: + return "Nintendo DS"; + + case RC_CONSOLE_NINTENDO_DSI: + return "Nintendo DSi"; + + case RC_CONSOLE_NINTENDO_3DS: + return "Nintendo 3DS"; + + case RC_CONSOLE_NOKIA_NGAGE: + return "Nokia N-Gage"; + + case RC_CONSOLE_ORIC: + return "Oric"; + + case RC_CONSOLE_PC6000: + return "PC-6000"; + + case RC_CONSOLE_PC8800: + return "PC-8000/8800"; + + case RC_CONSOLE_PC9800: + return "PC-9800"; + + case RC_CONSOLE_PCFX: + return "PC-FX"; + + case RC_CONSOLE_PC_ENGINE: + return "PC Engine"; + + case RC_CONSOLE_PC_ENGINE_CD: + return "PC Engine CD"; + + case RC_CONSOLE_PLAYSTATION: + return "PlayStation"; + + case RC_CONSOLE_PLAYSTATION_2: + return "PlayStation 2"; + + case RC_CONSOLE_PSP: + return "PlayStation Portable"; + + case RC_CONSOLE_POKEMON_MINI: + return "Pokemon Mini"; + + case RC_CONSOLE_SEGA_32X: + return "Sega 32X"; + + case RC_CONSOLE_SEGA_CD: + return "Sega CD"; + + case RC_CONSOLE_PICO: + return "Sega Pico"; + + case RC_CONSOLE_SATURN: + return "Sega Saturn"; + + case RC_CONSOLE_SG1000: + return "SG-1000"; + + case RC_CONSOLE_SHARPX1: + return "Sharp X1"; + + case RC_CONSOLE_STANDALONE: + return "Standalone"; + + case RC_CONSOLE_SUPER_NINTENDO: + return "Super Nintendo Entertainment System"; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return "Super CassetteVision"; + + case RC_CONSOLE_SUPERVISION: + return "Watara Supervision"; + + case RC_CONSOLE_THOMSONTO8: + return "Thomson TO8"; + + case RC_CONSOLE_TI83: + return "TI-83"; + + case RC_CONSOLE_TIC80: + return "TIC-80"; + + case RC_CONSOLE_UZEBOX: + return "Uzebox"; + + case RC_CONSOLE_VECTREX: + return "Vectrex"; + + case RC_CONSOLE_VIC20: + return "VIC-20"; + + case RC_CONSOLE_VIRTUAL_BOY: + return "Virtual Boy"; + + case RC_CONSOLE_WASM4: + return "WASM-4"; + + case RC_CONSOLE_WII: + return "Wii"; + + case RC_CONSOLE_WII_U: + return "Wii-U"; + + case RC_CONSOLE_WONDERSWAN: + return "WonderSwan"; + + case RC_CONSOLE_X68K: + return "X68K"; + + case RC_CONSOLE_XBOX: + return "XBOX"; + + case RC_CONSOLE_ZEEBO: + return "Zeebo"; + + case RC_CONSOLE_ZX81: + return "ZX-81"; + + case RC_CONSOLE_ZX_SPECTRUM: + return "ZX Spectrum"; + + default: + return "Unknown"; + } +} + +/* ===== 3DO ===== */ +/* http://www.arcaderestoration.com/memorymap/48/3DO+Bios.aspx */ +/* NOTE: the Opera core attempts to expose the NVRAM as RETRO_SAVE_RAM, but the 3DO documentation + * says that applications should only access NVRAM through API calls as it's shared across mulitple + * games. This suggests that even if the core does expose it, it may change depending on which other + * games the user has played - so ignore it. + */ +static const rc_memory_region_t _rc_memory_regions_3do[] = { + { 0x000000U, 0x1FFFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_3do = { _rc_memory_regions_3do, 1 }; + +/* ===== Amiga ===== */ +/* http://amigadev.elowar.com/read/ADCD_2.1/Hardware_Manual_guide/node00D3.html */ +static const rc_memory_region_t _rc_memory_regions_amiga[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, /* 512KB main RAM */ + { 0x080000U, 0x0FFFFFU, 0x080000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, /* 512KB extended RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_amiga = { _rc_memory_regions_amiga, 2 }; + +/* ===== Amstrad CPC ===== */ +/* http://www.cpcalive.com/docs/amstrad_cpc_6128_memory_map.html */ +/* https://www.cpcwiki.eu/index.php/File:AWMG_page151.jpg */ +/* The original CPC only had 64KB of memory, but the newer model has 128KB (expandable to 576KB) */ +/* https://www.grimware.org/doku.php/documentations/devices/gatearraydo=export_xhtml#mmr */ +static const rc_memory_region_t _rc_memory_regions_amstrad_pc[] = { + { 0x000000U, 0x00003FU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Firmware" }, + { 0x000040U, 0x00B0FFU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00B100U, 0x00BFFFU, 0x00B100U, RC_MEMORY_TYPE_SYSTEM_RAM, "Stack and Firmware Data" }, + { 0x00C000U, 0x00FFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen Memory" }, + { 0x010000U, 0x08FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_amstrad_pc = { _rc_memory_regions_amstrad_pc, 5 }; + +/* ===== Apple II ===== */ +static const rc_memory_region_t _rc_memory_regions_appleii[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, + { 0x010000U, 0x01FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Auxillary RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_appleii = { _rc_memory_regions_appleii, 2 }; + +/* ===== Arcadia 2001 ===== */ +/* https://amigan.yatho.com/a-coding.txt */ +/* RAM banks 1 and 2 only exist on some variant models - no game actually uses them */ +static const rc_memory_region_t _rc_memory_regions_arcadia_2001[] = { + { 0x000000U, 0x0000FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 3 */ + { 0x000100U, 0x0001FFU, 0x001900U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, + { 0x000200U, 0x0002FFU, 0x001A00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 4 */ +}; +static const rc_memory_regions_t rc_memory_regions_arcadia_2001 = { _rc_memory_regions_arcadia_2001, 3 }; + +/* ===== Arduboy ===== */ +/* https://scienceprog.com/avr-microcontroller-memory-map/ (Atmega32) */ +static const rc_memory_region_t _rc_memory_regions_arduboy[] = { + { 0x000000U, 0x0000FFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Registers" }, + /* https://www.dailydot.com/debug/arduboy-kickstarter/ 2.5KB of RAM */ + /* https://github.com/buserror/simavr/blob/1d227277b3d0039f9faef9ea62880ca3051b14f8/simavr/cores/avr/iom32u4.h#L1444-L1445 */ + { 0x000100U, 0x000AFFU, 0x00000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* 1KB of EEPROM https://github.com/libretro/arduous/blob/93e1a6289b42ef48de1fcfb96443981725955ad0/src/arduous/arduous.cpp#L453-L455 + * https://github.com/buserror/simavr/blob/1d227277b3d0039f9faef9ea62880ca3051b14f8/simavr/cores/avr/iom32u4.h#L1450 */ + /* EEPROM has it's own addressing scheme starting at $0000. I've chosen to virtualize the address + * at $80000000 to avoid a conflict */ + { 0x000B00U, 0x000EFFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "EEPROM" } +}; +static const rc_memory_regions_t rc_memory_regions_arduboy = { _rc_memory_regions_arduboy, 3 }; + +/* ===== Atari 2600 ===== */ +static const rc_memory_region_t _rc_memory_regions_atari2600[] = { + { 0x000000U, 0x00007FU, 0x000080U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari2600 = { _rc_memory_regions_atari2600, 1 }; + +/* ===== Atari 7800 ===== */ +/* http://www.atarihq.com/danb/files/78map.txt */ +/* http://pdf.textfiles.com/technical/7800_devkit.pdf */ +static const rc_memory_region_t _rc_memory_regions_atari7800[] = { + { 0x000000U, 0x0017FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware Interface" }, + { 0x001800U, 0x0027FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x002800U, 0x002FFFU, 0x002800U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x003000U, 0x0037FFU, 0x003000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x003800U, 0x003FFFU, 0x003800U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored RAM" }, + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + { 0x008000U, 0x00FFFFU, 0x008000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari7800 = { _rc_memory_regions_atari7800, 7 }; + +/* ===== Atari Jaguar ===== */ +/* https://www.mulle-kybernetik.com/jagdox/memorymap.html */ +static const rc_memory_region_t _rc_memory_regions_atari_jaguar[] = { + { 0x000000U, 0x1FFFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_atari_jaguar = { _rc_memory_regions_atari_jaguar, 1 }; + +/* ===== Atari Lynx ===== */ +/* http://www.retroisle.com/atari/lynx/Technical/Programming/lynxprgdumm.php */ +static const rc_memory_region_t _rc_memory_regions_atari_lynx[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Zero Page" }, + { 0x000100U, 0x0001FFU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "Stack" }, + { 0x000200U, 0x00FBFFU, 0x000200U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00FC00U, 0x00FCFFU, 0x00FC00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "SUZY hardware access" }, + { 0x00FD00U, 0x00FDFFU, 0x00FD00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "MIKEY hardware access" }, + { 0x00FE00U, 0x00FFF7U, 0x00FE00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Boot ROM" }, + { 0x00FFF8U, 0x00FFFFU, 0x00FFF8U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware vectors" } +}; +static const rc_memory_regions_t rc_memory_regions_atari_lynx = { _rc_memory_regions_atari_lynx, 7 }; + +/* ===== ColecoVision ===== */ +static const rc_memory_region_t _rc_memory_regions_colecovision[] = { + /* "System RAM" refers to the main RAM at 0x6000-0x63FF. However, this RAM might not always be visible. + * If the Super Game Module (SGM) is active, then it might overlay its own RAM at 0x0000-0x1FFF and 0x2000-0x7FFF. + * These positions overlap the BIOS and System RAM, therefore we use virtual addresses for these memory spaces. */ + { 0x000000U, 0x0003FFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x000400U, 0x0023FFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "SGM Low RAM" }, /* Normally situated at 0x0000-0x1FFF, which overlaps the BIOS */ + { 0x002400U, 0x0083FFU, 0x012000U, RC_MEMORY_TYPE_SYSTEM_RAM, "SGM High RAM" } /* Normally situated at 0x2000-0x7FFF, which overlaps System RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_regions_colecovision, 3 }; + +/* ===== Commodore 64 ===== */ +/* https://www.c64-wiki.com/wiki/Memory_Map */ +/* https://sta.c64.org/cbm64mem.html */ +/* NOTE: Several blocks of C64 memory can be bank-switched for ROM data (see https://www.c64-wiki.com/wiki/Bank_Switching). + * Achievement triggers rely on values changing, so we don't really need to look at the ROM data. + * The achievement logic assumes the RAM data is always present in the bankable blocks. As such, + * clients providing memory to achievements should always return the RAM values at the queried address. */ +static const rc_memory_region_t _rc_memory_regions_c64[] = { + { 0x000000U, 0x0003FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x000400U, 0x0007FFU, 0x000400U, RC_MEMORY_TYPE_VIDEO_RAM, "Screen RAM" }, + { 0x000800U, 0x009FFFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* BASIC area. $8000-$9FFF can bank to cartridge ROM */ + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* can bank to BASIC ROM or cartridge ROM */ + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* can bank to I/O Area or character ROM */ + { 0x00E000U, 0x00FFFFU, 0x00E000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* can bank to kernel ROM */ +}; +static const rc_memory_regions_t rc_memory_regions_c64 = { _rc_memory_regions_c64, 7 }; + +/* ===== Dreamcast ===== */ +/* http://archiv.sega-dc.de/munkeechuff/hardware/Memory.html */ +static const rc_memory_region_t _rc_memory_regions_dreamcast[] = { + { 0x00000000U, 0x00FFFFFFU, 0x0C000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_dreamcast = { _rc_memory_regions_dreamcast, 1 }; + +/* ===== Elektor TV Games Computer ===== */ +/* https://amigan.yatho.com/e-coding.txt */ +static const rc_memory_region_t _rc_memory_regions_elektor_tv_games[] = { + { 0x000000U, 0x0013FFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x001400U, 0x0014FFU, 0x001C00U, RC_MEMORY_TYPE_UNUSED, "Unused" }, /* mirror of $1D00-$1DFF */ + { 0x001500U, 0x0016FFU, 0x001D00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, /* two 256-byte I/O areas */ + { 0x001700U, 0x0017FFU, 0x001F00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_elektor_tv_games = { _rc_memory_regions_elektor_tv_games, 4 }; + +/* ===== Fairchild Channel F ===== */ +static const rc_memory_region_t _rc_memory_regions_fairchild_channel_f[] = { + /* "System RAM" is actually just a bunch of registers internal to CPU so all carts have it. + * "Video RAM" is part of the console so it's always available but it is write-only by the ROMs. + * "Cartridge RAM" is the cart BUS. Most carts only have ROMs on this bus. Exception are + * German Schach and homebrew carts that have 2K of RAM there in addition to ROM. + * "F2102 RAM" is used by Maze for 1K of RAM. + * https://discord.com/channels/310192285306454017/645777658319208448/967001438087708714 */ + { 0x00000000U, 0x0000003FU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00000040U, 0x0000083FU, 0x00300000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, + { 0x00000840U, 0x0001083FU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x00010840U, 0x00010C3FU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "F2102 RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_fairchild_channel_f = { _rc_memory_regions_fairchild_channel_f, 4 }; + +/* ===== Famicon Disk System ===== */ +/* https://fms.komkon.org/EMUL8/NES.html */ +static const rc_memory_region_t _rc_memory_regions_famicom_disk_system[] = { + { 0x0000U, 0x07FFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x0800U, 0x0FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1000U, 0x17FFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1800U, 0x1FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x2000U, 0x2007U, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "PPU Register" }, + { 0x2008U, 0x3FFFU, 0x2008U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored PPU Register" }, /* repeats every 8 bytes */ + { 0x4000U, 0x4017U, 0x4000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O register" }, + { 0x4018U, 0x401FU, 0x4018U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O test register" }, + { 0x4020U, 0x40FFU, 0x4020U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "FDS I/O registers"}, + { 0x4100U, 0x5FFFU, 0x4100U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, /* varies by mapper */ + { 0x6000U, 0xDFFFU, 0x6000U, RC_MEMORY_TYPE_SYSTEM_RAM, "FDS RAM"}, + { 0xE000U, 0xFFFFU, 0xE000U, RC_MEMORY_TYPE_READONLY, "FDS BIOS ROM"}, +}; +static const rc_memory_regions_t rc_memory_regions_famicom_disk_system = { _rc_memory_regions_famicom_disk_system, 12 }; + +/* ===== GameBoy / MegaDuck ===== */ +static const rc_memory_region_t _rc_memory_regions_gameboy[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt vector" }, + { 0x000100U, 0x00014FU, 0x000100U, RC_MEMORY_TYPE_READONLY, "Cartridge header" }, + { 0x000150U, 0x003FFFU, 0x000150U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (fixed)" }, /* bank 0 */ + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (paged)" }, /* bank 1-XX (switchable) */ + { 0x008000U, 0x0097FFU, 0x008000U, RC_MEMORY_TYPE_VIDEO_RAM, "Tile RAM" }, + { 0x009800U, 0x009BFFU, 0x009800U, RC_MEMORY_TYPE_VIDEO_RAM, "BG1 map data" }, + { 0x009C00U, 0x009FFFU, 0x009C00U, RC_MEMORY_TYPE_VIDEO_RAM, "BG2 map data" }, + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM (bank 0)"}, + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (fixed)" }, + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (fixed)" }, + { 0x00E000U, 0x00FDFFU, 0x00C000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Echo RAM" }, + { 0x00FE00U, 0x00FE9FU, 0x00FE00U, RC_MEMORY_TYPE_VIDEO_RAM, "Sprite RAM"}, + { 0x00FEA0U, 0x00FEFFU, 0x00FEA0U, RC_MEMORY_TYPE_UNUSED, ""}, + { 0x00FF00U, 0x00FF7FU, 0x00FF00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware I/O"}, + { 0x00FF80U, 0x00FFFEU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Quick RAM"}, + { 0x00FFFFU, 0x00FFFFU, 0x00FFFFU, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt enable"}, + + /* GameBoy's cartridge RAM may have a total of up to 16 banks that can be paged through $A000-$BFFF. + * It is desirable to always have access to these extra banks. We do this by expecting the extra banks + * to be addressable at addresses not supported by the native system. 0x10000-0x16000 is reserved + * for the extra banks of system memory that are exclusive to the GameBoy Color. */ + { 0x010000U, 0x015FFFU, 0x010000U, RC_MEMORY_TYPE_UNUSED, "Unused (GameBoy Color exclusive)" }, + { 0x016000U, 0x033FFFU, 0x016000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM (banks 1-15)" }, +}; +static const rc_memory_regions_t rc_memory_regions_megaduck = { _rc_memory_regions_gameboy, 16 }; +static const rc_memory_regions_t rc_memory_regions_gameboy = { _rc_memory_regions_gameboy, 18 }; + +/* ===== GameBoy Color ===== */ +static const rc_memory_region_t _rc_memory_regions_gameboy_color[] = { + { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt vector" }, + { 0x000100U, 0x00014FU, 0x000100U, RC_MEMORY_TYPE_READONLY, "Cartridge header" }, + { 0x000150U, 0x003FFFU, 0x000150U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (fixed)" }, /* bank 0 */ + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM (paged)" }, /* bank 1-XX (switchable) */ + { 0x008000U, 0x0097FFU, 0x008000U, RC_MEMORY_TYPE_VIDEO_RAM, "Tile RAM" }, + { 0x009800U, 0x009BFFU, 0x009800U, RC_MEMORY_TYPE_VIDEO_RAM, "BG1 map data" }, + { 0x009C00U, 0x009FFFU, 0x009C00U, RC_MEMORY_TYPE_VIDEO_RAM, "BG2 map data" }, + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM (bank 0)"}, + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (bank 0)" }, + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (bank 1)" }, + { 0x00E000U, 0x00FDFFU, 0x00C000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Echo RAM" }, + { 0x00FE00U, 0x00FE9FU, 0x00FE00U, RC_MEMORY_TYPE_VIDEO_RAM, "Sprite RAM"}, + { 0x00FEA0U, 0x00FEFFU, 0x00FEA0U, RC_MEMORY_TYPE_UNUSED, ""}, + { 0x00FF00U, 0x00FF7FU, 0x00FF00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Hardware I/O"}, + { 0x00FF80U, 0x00FFFEU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Quick RAM"}, + { 0x00FFFFU, 0x00FFFFU, 0x00FFFFU, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt enable"}, + + /* GameBoy Color provides 6 extra banks of system memory that can be paged out through the $D000-$DFFF, + * and the cartridge RAM may have a total of up to 16 banks page through $A000-$BFFF. + * It is desirable to always have access to these extra banks. We do this by expecting the extra banks + * to be addressable at addresses not supported by the native system. */ + { 0x010000U, 0x015FFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM (banks 2-7)" }, + { 0x016000U, 0x033FFFU, 0x016000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM (banks 1-15)" }, +}; +static const rc_memory_regions_t rc_memory_regions_gameboy_color = { _rc_memory_regions_gameboy_color, 18 }; + +/* ===== GameBoy Advance ===== */ +/* http://problemkaputt.de/gbatek-gba-memory-map.htm */ +static const rc_memory_region_t _rc_memory_regions_gameboy_advance[] = { + { 0x000000U, 0x007FFFU, 0x03000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* 32KB Internal Work RAM */ + { 0x008000U, 0x047FFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* 256KB External Work RAM */ + { 0x048000U, 0x057FFFU, 0x0E000000U, RC_MEMORY_TYPE_SAVE_RAM, "Save RAM" } /* 64KB Game Pak SRAM */ +}; +static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 3 }; + +/* ===== GameCube ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_gamecube[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_gamecube = { _rc_memory_regions_gamecube, 1 }; + +/* ===== Game Gear ===== */ +/* https://www.smspower.org/Development/MemoryMap */ +/* https://www.smspower.org/Development/Mappers */ +static const rc_memory_region_t _rc_memory_regions_game_gear[] = { + { 0x000000U, 0x001FFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* GG/SMS have various possible mappings for cartridge memory depending on the mapper used. + * However, these ultimately do not map all of their memory at once, typically requiring banking. + * Thus, the "real address" used is just a virtual address mapping all cartridge memory in one contiguous block. + * Note that this may possibly refer to non-battery backed "extended RAM" so this isn't strictly RC_MEMORY_TYPE_SAVE_RAM. + * libretro cores expose "extended RAM" as RETRO_MEMORY_SAVE_RAM regardless however. + */ + { 0x002000U, 0x009FFFU, 0x010000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_game_gear = { _rc_memory_regions_game_gear, 2 }; + +/* ===== Intellivision ===== */ +/* http://wiki.intellivision.us/index.php/Memory_Map */ +/* NOTE: Intellivision memory addresses point at 16-bit values. FreeIntv exposes them as little-endian + * 32-bit values. As such, the addresses are off by a factor of 4 _and_ the data is only where we + * expect it on little-endian systems. + */ +static const rc_memory_region_t _rc_memory_regions_intellivision[] = { + /* For backwards compatibility, register a 128-byte chunk of video RAM so the system memory + * will start at $0080. $0000-$007F previously tried to map to the STIC video registers as + * RETRO_MEMORY_VIDEO_RAM, and FreeIntv didn't expose any RETRO_MEMORY_VIDEO_RAM, so the first + * byte of RETRO_MEMORY_SYSTEM_RAM was registered at $0080. The data at $0080 is actually the + * STIC registers (4 bytes each), so we need to provide an arbitrary 128-byte padding that + * claims to be video RAM to ensure the system RAM ends up at the right address. + */ + { 0x000000U, 0x00007FU, 0xFFFFFFU, RC_MEMORY_TYPE_VIDEO_RAM, "" }, + + /* RetroAchievements address = real address x4 + 0x80. + * These all have to map to RETRO_MEMORY_SYSTEM_RAM (even the video-related fields) as the + * entire block is exposed as a single entity by FreeIntv */ + + /* $0000-$007F: STIC registers, $0040-$007F are readonly */ + { 0x000080U, 0x00027FU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "STIC Registers" }, + /* $0080-$00FF: unused */ + { 0x000280U, 0x00047FU, 0x000080U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $0100-$035F: system RAM, $0100-$01EF is scratch memory and only 8-bits per address */ + { 0x000480U, 0x000DFFU, 0x000100U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* $0360-$03FF: unused */ + { 0x000E00U, 0x00107FU, 0x000360U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $0400-$0FFF: cartridge RAM */ + { 0x001080U, 0x00407FU, 0x000400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + /* $1000-$1FFF: unused */ + { 0x004080U, 0x00807FU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + /* $2000-$2FFF: cartridge RAM */ + { 0x008080U, 0x00C07FU, 0x002000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + /* $3000-$3FFF: video RAM */ + { 0x00C080U, 0x01007FU, 0x003000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Video RAM" }, + /* $4000-$FFFF: cartridge RAM */ + { 0x010080U, 0x04007FU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_intellivision = { _rc_memory_regions_intellivision, 10 }; + +/* ===== Interton VC 4000 ===== */ +/* https://amigan.yatho.com/i-coding.txt */ +/* Cartridge RAM is not persisted, it's just expanded storage */ +static const rc_memory_region_t _rc_memory_regions_interton_vc_4000[] = { + { 0x000000U, 0x0003FFU, 0x001800U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x000400U, 0x0004FFU, 0x001E00U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "I/O Area" }, + { 0x000500U, 0x0005FFU, 0x001F00U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_interton_vc_4000 = { _rc_memory_regions_interton_vc_4000, 3 }; + +/* ===== Magnavox Odyssey 2 ===== */ +/* https://sudonull.com/post/76885-Architecture-and-programming-Philips-Videopac-Magnavox-Odyssey-2 */ +static const rc_memory_region_t _rc_memory_regions_magnavox_odyssey_2[] = { + /* Internal and external RAMs are reachable using unique instructions. + * The real addresses provided are virtual and for mapping purposes only. */ + { 0x000000U, 0x00003FU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Internal RAM" }, + { 0x000040U, 0x00013FU, 0x000040U, RC_MEMORY_TYPE_SYSTEM_RAM, "External RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_magnavox_odyssey_2 = { _rc_memory_regions_magnavox_odyssey_2, 2 }; + +/* ===== Master System ===== */ +/* https://www.smspower.org/Development/MemoryMap */ +/* https://www.smspower.org/Development/Mappers */ +static const rc_memory_region_t _rc_memory_regions_master_system[] = { + { 0x000000U, 0x001FFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* GG/SMS have various possible mappings for cartridge memory depending on the mapper used. + * However, these ultimately do not map all of their memory at once, typically requiring banking. + * Thus, the "real address" used is just a virtual address mapping all cartridge memory in one contiguous block. + * Note that this may possibly refer to non-battery backed "extended RAM" so this isn't strictly RC_MEMORY_TYPE_SAVE_RAM. + * libretro cores expose "extended RAM" as RETRO_MEMORY_SAVE_RAM regardless however. + */ + { 0x002000U, 0x009FFFU, 0x010000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_master_system = { _rc_memory_regions_master_system, 2 }; + +/* ===== MegaDrive (Genesis) ===== */ +/* https://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_megadrive[] = { + { 0x000000U, 0x00FFFFU, 0xFF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x010000U, 0x01FFFFU, 0x000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; + +/* ===== MegaDrive 32X (Genesis 32X) ===== */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ +static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Main MegaDrive RAM */ + { 0x010000U, 0x04FFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, /* Additional 32X RAM */ + { 0x050000U, 0x05FFFFU, 0x00000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive_32x = { _rc_memory_regions_megadrive_32x, 3 }; + +/* ===== MSX ===== */ +/* https://www.msx.org/wiki/The_Memory */ +/* MSX only has 64KB of addressable RAM, of which 32KB is reserved for the system/BIOS. + * However, the system has up to 512KB of RAM, which is paged into the addressable RAM + * We expect the raw RAM to be exposed, rather than force the devs to worry about the + * paging system. The entire RAM is expected to appear starting at $10000, which is not + * addressable by the system itself. + */ +static const rc_memory_region_t _rc_memory_regions_msx[] = { + { 0x000000U, 0x07FFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_msx = { _rc_memory_regions_msx, 1 }; + +/* ===== MS DOS ===== */ +static const rc_memory_region_t _rc_memory_regions_ms_dos[] = { + /* DOS emulators split the 640 KB conventional memory into two regions. + * First the part of the conventional memory given to the running game at $000000. + * The part of the conventional memory containing DOS and BIOS controlled memory + * is at $100000. The length of these can vary depending on the hardware + * and DOS version (or emulated DOS shell). + * These first two regions will only ever total to 640 KB but the regions map + * to 1 MB bounds to make resulting memory addresses more readable. + * When emulating a game not under DOS (so called 'PC Booter' games), the entirety + * of the 640 KB conventional memory block will be at $000000. + */ + { 0x00000000U, 0x0009FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Game Conventional Memory" }, + { 0x000A0000U, 0x000FFFFFU, 0x000A0000U, RC_MEMORY_TYPE_UNUSED, "Padding to align OS Conventional Memory" }, + { 0x00100000U, 0x0019FFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "OS Conventional Memory" }, + { 0x001A0000U, 0x001FFFFFU, 0x001A0000U, RC_MEMORY_TYPE_UNUSED, "Padding to align Expanded Memory" }, + /* Last is all the expanded memory which for now we map up to 64 MB which should be + * enough for the games we want to cover. An emulator might emulate more than that. + */ + { 0x00200000U, 0x041FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Expanded Memory" } +}; +static const rc_memory_regions_t rc_memory_regions_ms_dos = { _rc_memory_regions_ms_dos, 5 }; + +/* ===== Neo Geo Pocket ===== */ +/* http://neopocott.emuunlim.com/docs/tech-11.txt */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = { + /* The docs suggest there's Work RAM exposed from $0000-$6FFF, Sound RAM from $7000-$7FFF, and Video + * RAM from $8000-$BFFF, but both MednafenNGP and FBNeo only expose system RAM from $4000-$7FFF */ + { 0x000000U, 0x003FFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_pocket = { _rc_memory_regions_neo_geo_pocket, 1 }; + +/* ===== Neo Geo CD ===== */ +/* https://wiki.neogeodev.org/index.php?title=68k_memory_map */ +/* NeoCD exposes $000000-$1FFFFF as System RAM, but it seems like only the WORKRAM section is used. + * This is consistent with http://www.hardmvs.fr/manuals/NeoGeoProgrammersGuide.pdf (page25), which says: + * + * Furthermore, the NEO-GEO provides addresses 100000H-10FFFFH as a work area, out of which the + * addresses 10F300H-10FFFFH are reserved exclusively for use by the system program. Therefore, + * every game is to use addresses 100000H-10F2FFH. + * + * Also note that PRG files (game ROM) can be loaded anywhere else in the $000000-$1FFFFF range. + * AoF3 illustrates this pretty clearly: https://wiki.neogeodev.org/index.php?title=IPL_file + * + * PROG_CD.PRG,0,0 + * PROG_CDX.PRG,0,058000 + * CNV_NM.PRG,0,0C0000 + * FIX_DATA.PRG,0,0FD000 + * OBJACTLK.PRG,0,130000 + * SSEL_CNV.PRG,0,15A000 + * SSEL_BAK.PRG,0,16F000 + * HITMSG.PRG,0,170000 + * SSEL_SPR.PRG,0,19D000 + */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_cd[] = { + { 0x000000U, 0x00F2FFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* NOTE: some BIOS settings are exposed through the reserved RAM: https://wiki.neogeodev.org/index.php?title=68k_ASM_defines */ + { 0x00F300U, 0x00FFFFU, 0x0010F300U, RC_MEMORY_TYPE_SYSTEM_RAM, "Reserved RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_cd = { _rc_memory_regions_neo_geo_cd, 2 }; + +/* ===== Nintendo Entertainment System ===== */ +/* https://wiki.nesdev.com/w/index.php/CPU_memory_map */ +static const rc_memory_region_t _rc_memory_regions_nes[] = { + { 0x0000U, 0x07FFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x0800U, 0x0FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1000U, 0x17FFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1800U, 0x1FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x2000U, 0x2007U, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "PPU Register" }, + { 0x2008U, 0x3FFFU, 0x2008U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored PPU Register" }, /* repeats every 8 bytes */ + { 0x4000U, 0x4017U, 0x4000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O register" }, + { 0x4018U, 0x401FU, 0x4018U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O test register" }, + + /* NOTE: these are for the original NES/Famicom */ + { 0x4020U, 0x5FFFU, 0x4020U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, /* varies by mapper */ + { 0x6000U, 0x7FFFU, 0x6000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM"}, + { 0x8000U, 0xFFFFU, 0x8000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM"}, +}; +static const rc_memory_regions_t rc_memory_regions_nes = { _rc_memory_regions_nes, 11 }; + +/* ===== Nintendo 64 ===== */ +/* https://raw.githubusercontent.com/mikeryan/n64dev/master/docs/n64ops/n64ops%23h.txt */ +/* https://n64brew.dev/wiki/Memory_map#Virtual_Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_n64[] = { + { 0x000000U, 0x1FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ + { 0x200000U, 0x3FFFFFU, 0x80200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ + { 0x400000U, 0x7FFFFFU, 0x80400000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak */ +}; +static const rc_memory_regions_t rc_memory_regions_n64 = { _rc_memory_regions_n64, 3 }; + +/* ===== Nintendo DS ===== */ +/* https://www.akkit.org/info/gbatek.htm#dsmemorymaps */ +static const rc_memory_region_t _rc_memory_regions_nintendo_ds[] = { + { 0x0000000U, 0x03FFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* To keep DS/DSi memory maps aligned, padding is set here for the DSi's extra RAM */ + { 0x0400000U, 0x0FFFFFFU, 0x02400000U, RC_MEMORY_TYPE_UNUSED, "Unused (DSi exclusive)" }, + /* The DS/DSi have "tightly coupled memory": very fast memory directly connected to the CPU. + * This memory has an instruction variant (ITCM) and a data variant (DTCM). + * For achievement purposes it is useful to be able to access the data variant. + * This memory does not have a fixed address on console, being able to be moved to any $0xxxx000 region. + * While normally this kind of memory is addressed outside of the possible native addressing space, this is simply not possible, + * as the DS/DSi's address space covers all possible uint32_t values. + * $0E000000 is used here as a "pseudo-end," as this is nearly the end of all the memory actually mapped to addresses + * This means that (with the exception of $FFFF0000 onwards, which has the ARM9 BIOS mapped) $0E000000 onwards has nothing mapped to it + */ + { 0x1000000U, 0x1003FFFU, 0x0E000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Data TCM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 3 }; + +/* ===== Nintendo DSi ===== */ +/* https://problemkaputt.de/gbatek.htm#dsiiomap */ +static const rc_memory_region_t _rc_memory_regions_nintendo_dsi[] = { + { 0x0000000U, 0x0FFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x1000000U, 0x1003FFFU, 0x0E000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Data TCM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 2 }; + +/* ===== Oric ===== */ +static const rc_memory_region_t _rc_memory_regions_oric[] = { + /* actual size depends on machine type - up to 64KB */ + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_oric = { _rc_memory_regions_oric, 1 }; + +/* ===== PC-8800 ===== */ +static const rc_memory_region_t _rc_memory_regions_pc8800[] = { + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Main RAM" }, + { 0x010000U, 0x010FFFU, 0x010000U, RC_MEMORY_TYPE_VIDEO_RAM, "Text VRAM" } /* technically VRAM, but often used as system RAM */ +}; +static const rc_memory_regions_t rc_memory_regions_pc8800 = { _rc_memory_regions_pc8800, 2 }; + +/* ===== PC Engine ===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 1 }; + +/* ===== PC Engine CD===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine_cd[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x002000U, 0x011FFFU, 0x100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD RAM" }, + { 0x012000U, 0x041FFFU, 0x0D0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Super System Card RAM" }, + { 0x042000U, 0x0427FFU, 0x1EE000U, RC_MEMORY_TYPE_SAVE_RAM, "CD Battery-backed RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine_cd = { _rc_memory_regions_pc_engine_cd, 4 }; + +/* ===== PC-FX ===== */ +/* http://daifukkat.su/pcfx/data/memmap.html */ +static const rc_memory_region_t _rc_memory_regions_pcfx[] = { + { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x200000U, 0x207FFFU, 0xE0000000U, RC_MEMORY_TYPE_SAVE_RAM, "Internal Backup Memory" }, + { 0x208000U, 0x20FFFFU, 0xE8000000U, RC_MEMORY_TYPE_SAVE_RAM, "External Backup Memory" }, +}; +static const rc_memory_regions_t rc_memory_regions_pcfx = { _rc_memory_regions_pcfx, 3 }; + +/* ===== PlayStation ===== */ +/* http://www.raphnet.net/electronique/psx_adaptor/Playstation.txt */ +static const rc_memory_region_t _rc_memory_regions_playstation[] = { + { 0x000000U, 0x00FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x010000U, 0x1FFFFFU, 0x00010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x200000U, 0x2003FFU, 0x1F800000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Scratchpad RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_playstation = { _rc_memory_regions_playstation, 3 }; + +/* ===== PlayStation 2 ===== */ +/* https://psi-rockin.github.io/ps2tek/ */ +static const rc_memory_region_t _rc_memory_regions_playstation2[] = { + { 0x00000000U, 0x000FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x00100000U, 0x01FFFFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x02000000U, 0x02003FFFU, 0x70000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Scratchpad RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_playstation2 = { _rc_memory_regions_playstation2, 3 }; + +/* ===== PlayStation Portable ===== */ +/* https://github.com/uofw/upspd/wiki/Memory-map */ +static const rc_memory_region_t _rc_memory_regions_psp[] = { + { 0x00000000U, 0x007FFFFFU, 0x08000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x00800000U, 0x01FFFFFFU, 0x08800000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_psp = { _rc_memory_regions_psp, 2 }; + +/* ===== Pokemon Mini ===== */ +/* https://www.pokemon-mini.net/documentation/memory-map/ */ +static const rc_memory_region_t _rc_memory_regions_pokemini[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "BIOS RAM" }, + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_pokemini = { _rc_memory_regions_pokemini, 2 }; + +/* ===== Sega CD ===== */ +/* https://en.wikibooks.org/wiki/Genesis_Programming/68K_Memory_map/ */ +static const rc_memory_region_t _rc_memory_regions_segacd[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "68000 RAM" }, + { 0x010000U, 0x08FFFFU, 0x80020000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD PRG RAM" }, /* normally banked into $020000-$03FFFF */ + { 0x090000U, 0x0AFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD WORD RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_segacd = { _rc_memory_regions_segacd, 3 }; + +/* ===== Sega Saturn ===== */ +/* https://segaretro.org/Sega_Saturn_hardware_notes_(2004-04-27) */ +static const rc_memory_region_t _rc_memory_regions_saturn[] = { + { 0x000000U, 0x0FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Work RAM Low" }, + { 0x100000U, 0x1FFFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Work RAM High" } +}; +static const rc_memory_regions_t rc_memory_regions_saturn = { _rc_memory_regions_saturn, 2 }; + +/* ===== SG-1000 ===== */ +/* https://www.smspower.org/Development/MemoryMap */ +static const rc_memory_region_t _rc_memory_regions_sg1000[] = { + { 0x000000U, 0x0003FFU, 0xC000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* https://github.com/libretro/FBNeo/blob/697801c6262be6ca91615cf905444d3e039bc06f/src/burn/drv/sg1000/d_sg1000.cpp#L210-L237 */ + /* Expansion mode B exposes 8KB at $C000. The first 2KB hides the System RAM, but since the address matches, + we'll leverage that definition and expand it another 6KB */ + { 0x000400U, 0x001FFFU, 0xC400U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, + /* Expansion mode A exposes 8KB at $2000 */ + { 0x002000U, 0x003FFFU, 0x2000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, + /* Othello exposes 2KB at $8000, and The Castle exposes 8KB at $8000 */ + { 0x004000U, 0x005FFFU, 0x8000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_sg1000 = { _rc_memory_regions_sg1000, 4 }; + +/* ===== Super Cassette Vision ===== */ +/* https://github.com/mamedev/mame/blob/f32bb79e8541ba96d3a8144b220c48fb7536ba4b/src/mame/epoch/scv.cpp#L78-L86 */ +/* SCV only has 128 bytes of system RAM, any additional memory is provided on the individual carts and is + * not backed up by battery. */ +/* http://www.videogameconsolelibrary.com/pg80-super_cass_vis.htm#page=specs */ +static const rc_memory_region_t _rc_memory_regions_scv[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_READONLY, "System ROM" }, /* BIOS */ + { 0x001000U, 0x001FFFU, 0x001000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x002000U, 0x003FFFU, 0x002000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" }, /* only really goes to $33FF? */ + { 0x004000U, 0x007FFFU, 0x004000U, RC_MEMORY_TYPE_UNUSED, "" }, + { 0x008000U, 0x00FF7FU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Cartridge RAM" }, + { 0x00FF80U, 0x00FFFFU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_scv, 6 }; + +/* ===== Super Nintendo ===== */ +/* https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM */ +static const rc_memory_region_t _rc_memory_regions_snes[] = { + { 0x000000U, 0x01FFFFU, 0x07E0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Cartridge RAM here could be in a variety of places in SNES memory, depending on the ROM type. + * Due to this, we place Cartridge RAM outside of the possible native addressing space. + * Note that this also covers SA-1 BW-RAM (which is exposed as RETRO_MEMORY_SAVE_RAM for libretro). + */ + { 0x020000U, 0x09FFFFU, 0x1000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + /* I-RAM on the SA-1 is normally at 0x003000. However, this address typically just has a mirror of System RAM for other ROM types. + * To avoid conflicts, don't use 0x003000, instead map it outside of the possible native addressing space. + */ + { 0x0A0000U, 0x0A07FFU, 0x1080000U, RC_MEMORY_TYPE_SYSTEM_RAM, "I-RAM (SA-1)" } +}; +static const rc_memory_regions_t rc_memory_regions_snes = { _rc_memory_regions_snes, 3 }; + +/* ===== Thomson TO8 ===== */ +/* https://github.com/mamedev/mame/blob/master/src/mame/drivers/thomson.cpp#L1617 */ +static const rc_memory_region_t _rc_memory_regions_thomson_to8[] = { + { 0x000000U, 0x07FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_thomson_to8 = { _rc_memory_regions_thomson_to8, 1 }; + +/* ===== TI-83 ===== */ +/* https://tutorials.eeems.ca/ASMin28Days/lesson/day03.html#mem */ +static const rc_memory_region_t _rc_memory_regions_ti83[] = { + { 0x000000U, 0x007FFFU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_ti83 = { _rc_memory_regions_ti83, 1 }; + +/* ===== TIC-80 ===== */ +/* https://github.com/nesbox/TIC-80/wiki/RAM */ +static const rc_memory_region_t _rc_memory_regions_tic80[] = { + { 0x000000U, 0x003FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Video RAM" }, /* have to classify this as system RAM because the core exposes it as part of the RETRO_MEMORY_SYSTEM_RAM */ + { 0x004000U, 0x005FFFU, 0x004000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Tile RAM" }, + { 0x006000U, 0x007FFFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Sprite RAM" }, + { 0x008000U, 0x00FF7FU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "MAP RAM" }, + { 0x00FF80U, 0x00FF8BU, 0x00FF80U, RC_MEMORY_TYPE_SYSTEM_RAM, "Input State" }, + { 0x00FF8CU, 0x014003U, 0x00FF8CU, RC_MEMORY_TYPE_SYSTEM_RAM, "Sound RAM" }, + { 0x014004U, 0x014403U, 0x014004U, RC_MEMORY_TYPE_SAVE_RAM, "Persistent Memory" }, /* this is also returned as part of RETRO_MEMORY_SYSTEM_RAM, but can be extrapolated correctly because the pointer starts at the first SYSTEM_RAM region */ + { 0x014404U, 0x014603U, 0x014404U, RC_MEMORY_TYPE_SYSTEM_RAM, "Sprite Flags" }, + { 0x014604U, 0x014E03U, 0x014604U, RC_MEMORY_TYPE_SYSTEM_RAM, "System Font" }, + { 0x014E04U, 0x017FFFU, 0x014E04U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM"} +}; +static const rc_memory_regions_t rc_memory_regions_tic80 = { _rc_memory_regions_tic80, 10 }; + +/* ===== Uzebox ===== */ +/* https://uzebox.org/index.php */ +static const rc_memory_region_t _rc_memory_regions_uzebox[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_uzebox = { _rc_memory_regions_uzebox, 1 }; + +/* ===== Vectrex ===== */ +/* https://roadsidethoughts.com/vectrex/vectrex-memory-map.htm */ +static const rc_memory_region_t _rc_memory_regions_vectrex[] = { + { 0x000000U, 0x0003FFU, 0x00C800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_vectrex = { _rc_memory_regions_vectrex, 1 }; + +/* ===== Virtual Boy ===== */ +static const rc_memory_region_t _rc_memory_regions_virtualboy[] = { + { 0x000000U, 0x00FFFFU, 0x05000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x010000U, 0x01FFFFU, 0x06000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_virtualboy = { _rc_memory_regions_virtualboy, 2 }; + +/* ===== Watara Supervision ===== */ +/* https://github.com/libretro/potator/blob/b5e5ba02914fcdf4a8128072dbc709da28e08832/common/memorymap.c#L231-L259 */ +static const rc_memory_region_t _rc_memory_regions_watara_supervision[] = { + { 0x0000U, 0x001FFFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x2000U, 0x003FFFU, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Registers" }, + { 0x4000U, 0x005FFFU, 0x4000U, RC_MEMORY_TYPE_VIDEO_RAM, "Video RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_watara_supervision = { _rc_memory_regions_watara_supervision, 3 }; + +/* ===== WASM-4 ===== */ +/* fantasy console that runs specifically designed WebAssembly games */ +/* https://github.com/aduros/wasm4/blob/main/site/docs/intro.md#hardware-specs */ +static const rc_memory_region_t _rc_memory_regions_wasm4[] = { + { 0x000000U, 0x00FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Persistent storage is not directly accessible from the game. It has to be loaded into System RAM first + { 0x010000U, 0x0103FFU, 0x80000000U, RC_MEMORY_TYPE_SAVE_RAM, "Disk Storage"} + */ +}; +static const rc_memory_regions_t rc_memory_regions_wasm4 = { _rc_memory_regions_wasm4, 1 }; + +/* ===== Wii ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_wii[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x01800000U, 0x0FFFFFFF, 0x81800000U, RC_MEMORY_TYPE_UNUSED, "Unused" }, + { 0x10000000U, 0x13FFFFFF, 0x90000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wii = { _rc_memory_regions_wii, 3 }; + +/* ===== WonderSwan ===== */ +/* http://daifukkat.su/docs/wsman/#ovr_memmap */ +static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { + /* RAM ends at 0x3FFF for WonderSwan, WonderSwan color uses all 64KB */ + { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Only 64KB of SRAM is accessible via the addressing scheme, but the cartridge + * may have up to 512KB of SRAM. http://daifukkat.su/docs/wsman/#cart_meta + * Since beetle_wswan exposes it as a contiguous block, assume its contiguous + * even though the documentation says $20000-$FFFFF is ROM data. If this causes + * a conflict in the future, we can revisit. A new region with a virtual address + * could be added to pick up the additional SRAM data. As long as it immediately + * follows the 64KB at $10000, all existing achievements should be unaffected. + */ + { 0x010000U, 0x08FFFFU, 0x010000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_regions_wonderswan, 2 }; + +/* ===== ZX Spectrum ===== */ +/* https://github.com/TASEmulators/BizHawk/blob/3a3b22c/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs + * https://github.com/TASEmulators/BizHawk/blob/3a3b22c/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs + * https://worldofspectrum.org/faq/reference/128kreference.htm */ +static const rc_memory_region_t _rc_memory_regions_zx_spectrum[] = { + /* ZX Spectrum is complicated as multiple models exist with varying amounts of memory. + * In practice, this can be reduced to two categories: 16K/48K units, and 128K units. + * 16K/48K units have RAM starting at $4000 onwards, 16K ending at $7FFF, 48K ending at $FFFF. + * 128K units have banked memory, with $4000-$7FFF normally having RAM bank 5, and $8000-$BFFF normally having RAM bank 2. + * $C000-$FFFF is normally reserved for banked RAM, having any of banks 0-7. + * For the purposes of the RAM map, $C000-$FFFF is assumed to be bank 0, and $10000 onwards has the other banks in order (1, 3, 4, 6, 7) + * Doing it this way always for 16K/48K games to have the same memory map on the 128K, and thus avoid issues due to the model selected. + * Later 128K units also have a special banking mode that changes up banking completely, but for 16K/48K compatibility purposes this doesn't matter, and so is irrelevant. + */ + { 0x00000U, 0x03FFFU, 0x04000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen RAM" }, /* RAM bank 5 on 128K units */ + { 0x04000U, 0x07FFFU, 0x08000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 2 on 128K units */ + { 0x08000U, 0x0BFFFU, 0x0C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 0-7 on 128K units, assumed to be bank 0 here */ + { 0x0C000U, 0x0FFFFU, 0x10000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 1 on 128K units */ + { 0x10000U, 0x13FFFU, 0x14000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 3 on 128K units */ + { 0x14000U, 0x17FFFU, 0x18000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 4 on 128K units */ + { 0x18000U, 0x1BFFFU, 0x1C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 6 on 128K units */ + { 0x1C000U, 0x1FFFFU, 0x20000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen RAM" } /* RAM bank 7 on 128K units */ +}; +static const rc_memory_regions_t rc_memory_regions_zx_spectrum = { _rc_memory_regions_zx_spectrum, 8 }; + +/* ===== default ===== */ +static const rc_memory_regions_t rc_memory_regions_none = { 0, 0 }; + +const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) +{ + switch (console_id) + { + case RC_CONSOLE_3DO: + return &rc_memory_regions_3do; + + case RC_CONSOLE_AMIGA: + return &rc_memory_regions_amiga; + + case RC_CONSOLE_AMSTRAD_PC: + return &rc_memory_regions_amstrad_pc; + + case RC_CONSOLE_APPLE_II: + return &rc_memory_regions_appleii; + + case RC_CONSOLE_ARCADIA_2001: + return &rc_memory_regions_arcadia_2001; + + case RC_CONSOLE_ARDUBOY: + return &rc_memory_regions_arduboy; + + case RC_CONSOLE_ATARI_2600: + return &rc_memory_regions_atari2600; + + case RC_CONSOLE_ATARI_7800: + return &rc_memory_regions_atari7800; + + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_ATARI_JAGUAR_CD: + return &rc_memory_regions_atari_jaguar; + + case RC_CONSOLE_ATARI_LYNX: + return &rc_memory_regions_atari_lynx; + + case RC_CONSOLE_COLECOVISION: + return &rc_memory_regions_colecovision; + + case RC_CONSOLE_COMMODORE_64: + return &rc_memory_regions_c64; + + case RC_CONSOLE_DREAMCAST: + return &rc_memory_regions_dreamcast; + + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + return &rc_memory_regions_elektor_tv_games; + + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + return &rc_memory_regions_fairchild_channel_f; + + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + return &rc_memory_regions_famicom_disk_system; + + case RC_CONSOLE_GAMEBOY: + return &rc_memory_regions_gameboy; + + case RC_CONSOLE_GAMEBOY_COLOR: + return &rc_memory_regions_gameboy_color; + + case RC_CONSOLE_GAMEBOY_ADVANCE: + return &rc_memory_regions_gameboy_advance; + + case RC_CONSOLE_GAMECUBE: + return &rc_memory_regions_gamecube; + + case RC_CONSOLE_GAME_GEAR: + return &rc_memory_regions_game_gear; + + case RC_CONSOLE_INTELLIVISION: + return &rc_memory_regions_intellivision; + + case RC_CONSOLE_INTERTON_VC_4000: + return &rc_memory_regions_interton_vc_4000; + + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + return &rc_memory_regions_magnavox_odyssey_2; + + case RC_CONSOLE_MASTER_SYSTEM: + return &rc_memory_regions_master_system; + + case RC_CONSOLE_MEGA_DRIVE: + return &rc_memory_regions_megadrive; + + case RC_CONSOLE_MEGADUCK: + return &rc_memory_regions_megaduck; + + case RC_CONSOLE_SEGA_32X: + return &rc_memory_regions_megadrive_32x; + + case RC_CONSOLE_MSX: + return &rc_memory_regions_msx; + + case RC_CONSOLE_MS_DOS: + return &rc_memory_regions_ms_dos; + + case RC_CONSOLE_NEOGEO_POCKET: + return &rc_memory_regions_neo_geo_pocket; + + case RC_CONSOLE_NEO_GEO_CD: + return &rc_memory_regions_neo_geo_cd; + + case RC_CONSOLE_NINTENDO: + return &rc_memory_regions_nes; + + case RC_CONSOLE_NINTENDO_64: + return &rc_memory_regions_n64; + + case RC_CONSOLE_NINTENDO_DS: + return &rc_memory_regions_nintendo_ds; + + case RC_CONSOLE_NINTENDO_DSI: + return &rc_memory_regions_nintendo_dsi; + + case RC_CONSOLE_ORIC: + return &rc_memory_regions_oric; + + case RC_CONSOLE_PC8800: + return &rc_memory_regions_pc8800; + + case RC_CONSOLE_PC_ENGINE: + return &rc_memory_regions_pc_engine; + + case RC_CONSOLE_PC_ENGINE_CD: + return &rc_memory_regions_pc_engine_cd; + + case RC_CONSOLE_PCFX: + return &rc_memory_regions_pcfx; + + case RC_CONSOLE_PLAYSTATION: + return &rc_memory_regions_playstation; + + case RC_CONSOLE_PLAYSTATION_2: + return &rc_memory_regions_playstation2; + + case RC_CONSOLE_PSP: + return &rc_memory_regions_psp; + + case RC_CONSOLE_POKEMON_MINI: + return &rc_memory_regions_pokemini; + + case RC_CONSOLE_SATURN: + return &rc_memory_regions_saturn; + + case RC_CONSOLE_SEGA_CD: + return &rc_memory_regions_segacd; + + case RC_CONSOLE_SG1000: + return &rc_memory_regions_sg1000; + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return &rc_memory_regions_scv; + + case RC_CONSOLE_SUPER_NINTENDO: + return &rc_memory_regions_snes; + + case RC_CONSOLE_SUPERVISION: + return &rc_memory_regions_watara_supervision; + + case RC_CONSOLE_THOMSONTO8: + return &rc_memory_regions_thomson_to8; + + case RC_CONSOLE_TI83: + return &rc_memory_regions_ti83; + + case RC_CONSOLE_TIC80: + return &rc_memory_regions_tic80; + + case RC_CONSOLE_UZEBOX: + return &rc_memory_regions_uzebox; + + case RC_CONSOLE_VECTREX: + return &rc_memory_regions_vectrex; + + case RC_CONSOLE_VIRTUAL_BOY: + return &rc_memory_regions_virtualboy; + + case RC_CONSOLE_WASM4: + return &rc_memory_regions_wasm4; + + case RC_CONSOLE_WII: + return &rc_memory_regions_wii; + + case RC_CONSOLE_WONDERSWAN: + return &rc_memory_regions_wonderswan; + + case RC_CONSOLE_ZX_SPECTRUM: + return &rc_memory_regions_zx_spectrum; + + default: + return &rc_memory_regions_none; + } +} diff --git a/src/rcheevos/src/rcheevos/format.c b/src/rcheevos/src/rcheevos/format.c new file mode 100644 index 0000000000..bfeadf44cf --- /dev/null +++ b/src/rcheevos/src/rcheevos/format.c @@ -0,0 +1,330 @@ +#include "rc_internal.h" + +#include "../rc_compat.h" + +#include +#include +#include + +int rc_parse_format(const char* format_str) { + switch (*format_str++) { + case 'F': + if (!strcmp(format_str, "RAMES")) { + return RC_FORMAT_FRAMES; + } + if (!strncmp(format_str, "LOAT", 4) && format_str[4] >= '1' && format_str[4] <= '6' && format_str[5] == '\0') { + return RC_FORMAT_FLOAT1 + (format_str[4] - '1'); + } + if (!strncmp(format_str, "IXED", 4) && format_str[4] >= '1' && format_str[4] <= '3' && format_str[5] == '\0') { + return RC_FORMAT_FIXED1 + (format_str[4] - '1'); + } + + break; + + case 'T': + if (!strcmp(format_str, "IME")) { + return RC_FORMAT_FRAMES; + } + if (!strcmp(format_str, "IMESECS")) { + return RC_FORMAT_SECONDS; + } + if (!strcmp(format_str, "HOUSANDS")) { + return RC_FORMAT_THOUSANDS; + } + if (!strcmp(format_str, "ENS")) { + return RC_FORMAT_TENS; + } + + break; + + case 'S': + if (!strcmp(format_str, "ECS")) { + return RC_FORMAT_SECONDS; + } + if (!strcmp(format_str, "CORE")) { + return RC_FORMAT_SCORE; + } + if (!strcmp(format_str, "ECS_AS_MINS")) { + return RC_FORMAT_SECONDS_AS_MINUTES; + } + + break; + + case 'M': + if (!strcmp(format_str, "ILLISECS")) { + return RC_FORMAT_CENTISECS; + } + if (!strcmp(format_str, "INUTES")) { + return RC_FORMAT_MINUTES; + } + + break; + + case 'P': + if (!strcmp(format_str, "OINTS")) { + return RC_FORMAT_SCORE; + } + + break; + + case 'V': + if (!strcmp(format_str, "ALUE")) { + return RC_FORMAT_VALUE; + } + + break; + + case 'U': + if (!strcmp(format_str, "NSIGNED")) { + return RC_FORMAT_UNSIGNED_VALUE; + } + + break; + + case 'O': + if (!strcmp(format_str, "THER")) { + return RC_FORMAT_SCORE; + } + + break; + + case 'H': + if (!strcmp(format_str, "UNDREDS")) { + return RC_FORMAT_HUNDREDS; + } + + break; + } + + return RC_FORMAT_VALUE; +} + +static int rc_format_value_minutes(char* buffer, size_t size, uint32_t minutes) { + uint32_t hours; + + hours = minutes / 60; + minutes -= hours * 60; + return snprintf(buffer, size, "%uh%02u", hours, minutes); +} + +static int rc_format_value_seconds(char* buffer, size_t size, uint32_t seconds) { + uint32_t hours, minutes; + + /* apply modulus math to split the seconds into hours/minutes/seconds */ + minutes = seconds / 60; + seconds -= minutes * 60; + if (minutes < 60) { + return snprintf(buffer, size, "%u:%02u", minutes, seconds); + } + + hours = minutes / 60; + minutes -= hours * 60; + return snprintf(buffer, size, "%uh%02u:%02u", hours, minutes, seconds); +} + +static int rc_format_value_centiseconds(char* buffer, size_t size, uint32_t centiseconds) { + uint32_t seconds; + int chars, chars2; + + /* modulus off the centiseconds */ + seconds = centiseconds / 100; + centiseconds -= seconds * 100; + + chars = rc_format_value_seconds(buffer, size, seconds); + if (chars > 0) { + chars2 = snprintf(buffer + chars, size - chars, ".%02u", centiseconds); + if (chars2 > 0) { + chars += chars2; + } else { + chars = chars2; + } + } + + return chars; +} + +static int rc_format_value_fixed(char* buffer, size_t size, const char* format, int32_t value, int32_t factor) +{ + if (value >= 0) + return snprintf(buffer, size, format, value / factor, value % factor); + + return snprintf(buffer, size, format, value / factor, (-value) % factor); +} + +static int rc_format_value_padded(char* buffer, size_t size, const char* format, int32_t value) +{ + if (value == 0) + return snprintf(buffer, size, "0"); + + return snprintf(buffer, size, format, value); +} + +static int rc_format_insert_commas(int chars, char* buffer, size_t size) +{ + int to_insert; + char* src = buffer; + char* ptr; + char* dst = &buffer[chars]; + if (chars == 0) + return 0; + + /* ignore leading negative sign */ + if (*src == '-') + src++; + + /* determine how many digits are present in the leading number */ + ptr = src; + while (ptr < dst && isdigit((int)*ptr)) + ++ptr; + + /* determine how many commas are needed */ + to_insert = (int)((ptr - src - 1) / 3); + if (to_insert == 0) /* no commas needed */ + return chars; + + /* if there's not enough room to insert the commas, leave string as-is, but return wanted space */ + chars += to_insert; + if (chars >= (int)size) + return chars; + + /* move the trailing part of the string */ + memmove(ptr + to_insert, ptr, dst - ptr + 1); + + /* shift blocks of three digits at a time, inserting commas in front of them */ + src = ptr - 1; + dst = src + to_insert; + while (to_insert > 0) { + *dst-- = *src--; + *dst-- = *src--; + *dst-- = *src--; + *dst-- = ','; + + --to_insert; + } + + return chars; +} + +int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format) { + int chars; + rc_typed_value_t converted_value; + + memcpy(&converted_value, value, sizeof(converted_value)); + + switch (format) { + default: + case RC_FORMAT_VALUE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = snprintf(buffer, size, "%d", converted_value.value.i32); + break; + + case RC_FORMAT_FRAMES: + /* 60 frames per second = 100 centiseconds / 60 frames; multiply frames by 100 / 60 */ + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_centiseconds(buffer, size, converted_value.value.u32 * 10 / 6); + break; + + case RC_FORMAT_CENTISECS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_centiseconds(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SECONDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_seconds(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SECONDS_AS_MINUTES: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_minutes(buffer, size, converted_value.value.u32 / 60); + break; + + case RC_FORMAT_MINUTES: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = rc_format_value_minutes(buffer, size, converted_value.value.u32); + break; + + case RC_FORMAT_SCORE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + return snprintf(buffer, size, "%06d", converted_value.value.i32); + + case RC_FORMAT_FLOAT1: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.1f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT2: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.2f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT3: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.3f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT4: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.4f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT5: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.5f", converted_value.value.f32); + break; + + case RC_FORMAT_FLOAT6: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); + chars = snprintf(buffer, size, "%.6f", converted_value.value.f32); + break; + + case RC_FORMAT_FIXED1: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_fixed(buffer, size, "%d.%u", converted_value.value.i32, 10); + break; + + case RC_FORMAT_FIXED2: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_fixed(buffer, size, "%d.%02u", converted_value.value.i32, 100); + break; + + case RC_FORMAT_FIXED3: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_fixed(buffer, size, "%d.%03u", converted_value.value.i32, 1000); + break; + + case RC_FORMAT_TENS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_padded(buffer, size, "%d0", converted_value.value.i32); + break; + + case RC_FORMAT_HUNDREDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_padded(buffer, size, "%d00", converted_value.value.i32); + break; + + case RC_FORMAT_THOUSANDS: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); + chars = rc_format_value_padded(buffer, size, "%d000", converted_value.value.i32); + break; + + case RC_FORMAT_UNSIGNED_VALUE: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + chars = snprintf(buffer, size, "%u", converted_value.value.u32); + break; + + case RC_FORMAT_UNFORMATTED: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + return snprintf(buffer, size, "%u", converted_value.value.u32); + } + + return rc_format_insert_commas(chars, buffer, size); +} + +int rc_format_value(char* buffer, int size, int32_t value, int format) { + rc_typed_value_t typed_value; + + typed_value.value.i32 = value; + typed_value.type = RC_VALUE_TYPE_SIGNED; + return rc_format_typed_value(buffer, size, &typed_value, format); +} diff --git a/src/rcheevos/src/rcheevos/lboard.c b/src/rcheevos/src/rcheevos/lboard.c new file mode 100644 index 0000000000..84ff5460af --- /dev/null +++ b/src/rcheevos/src/rcheevos/lboard.c @@ -0,0 +1,287 @@ +#include "rc_internal.h" + +enum { + RC_LBOARD_START = 1 << 0, + RC_LBOARD_CANCEL = 1 << 1, + RC_LBOARD_SUBMIT = 1 << 2, + RC_LBOARD_VALUE = 1 << 3, + RC_LBOARD_PROGRESS = 1 << 4, + RC_LBOARD_COMPLETE = RC_LBOARD_START | RC_LBOARD_CANCEL | RC_LBOARD_SUBMIT | RC_LBOARD_VALUE +}; + +void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse) { + int found; + + self->progress = 0; + found = 0; + + for (;;) + { + if ((memaddr[0] == 's' || memaddr[0] == 'S') && + (memaddr[1] == 't' || memaddr[1] == 'T') && + (memaddr[2] == 'a' || memaddr[2] == 'A') && memaddr[3] == ':') { + if ((found & RC_LBOARD_START) != 0) { + parse->offset = RC_DUPLICATED_START; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_START; + rc_parse_trigger_internal(&self->start, &memaddr, parse); + } + } + else if ((memaddr[0] == 'c' || memaddr[0] == 'C') && + (memaddr[1] == 'a' || memaddr[1] == 'A') && + (memaddr[2] == 'n' || memaddr[2] == 'N') && memaddr[3] == ':') { + if ((found & RC_LBOARD_CANCEL) != 0) { + parse->offset = RC_DUPLICATED_CANCEL; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_CANCEL; + rc_parse_trigger_internal(&self->cancel, &memaddr, parse); + } + } + else if ((memaddr[0] == 's' || memaddr[0] == 'S') && + (memaddr[1] == 'u' || memaddr[1] == 'U') && + (memaddr[2] == 'b' || memaddr[2] == 'B') && memaddr[3] == ':') { + if ((found & RC_LBOARD_SUBMIT) != 0) { + parse->offset = RC_DUPLICATED_SUBMIT; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_SUBMIT; + rc_parse_trigger_internal(&self->submit, &memaddr, parse); + } + } + else if ((memaddr[0] == 'v' || memaddr[0] == 'V') && + (memaddr[1] == 'a' || memaddr[1] == 'A') && + (memaddr[2] == 'l' || memaddr[2] == 'L') && memaddr[3] == ':') { + if ((found & RC_LBOARD_VALUE) != 0) { + parse->offset = RC_DUPLICATED_VALUE; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_VALUE; + rc_parse_value_internal(&self->value, &memaddr, parse); + } + } + else if ((memaddr[0] == 'p' || memaddr[0] == 'P') && + (memaddr[1] == 'r' || memaddr[1] == 'R') && + (memaddr[2] == 'o' || memaddr[2] == 'O') && memaddr[3] == ':') { + if ((found & RC_LBOARD_PROGRESS) != 0) { + parse->offset = RC_DUPLICATED_PROGRESS; + return; + } + + memaddr += 4; + if (*memaddr && *memaddr != ':') { + found |= RC_LBOARD_PROGRESS; + + self->progress = RC_ALLOC(rc_value_t, parse); + rc_parse_value_internal(self->progress, &memaddr, parse); + } + } + + /* encountered an error parsing one of the parts */ + if (parse->offset < 0) + return; + + /* end of string, or end of quoted string - stop processing */ + if (memaddr[0] == '\0' || memaddr[0] == '\"') + break; + + /* expect two colons between fields */ + if (memaddr[0] != ':' || memaddr[1] != ':') { + parse->offset = RC_INVALID_LBOARD_FIELD; + return; + } + + memaddr += 2; + } + + if ((found & RC_LBOARD_COMPLETE) != RC_LBOARD_COMPLETE) { + if ((found & RC_LBOARD_START) == 0) { + parse->offset = RC_MISSING_START; + } + else if ((found & RC_LBOARD_CANCEL) == 0) { + parse->offset = RC_MISSING_CANCEL; + } + else if ((found & RC_LBOARD_SUBMIT) == 0) { + parse->offset = RC_MISSING_SUBMIT; + } + else if ((found & RC_LBOARD_VALUE) == 0) { + parse->offset = RC_MISSING_VALUE; + } + + return; + } + + self->state = RC_LBOARD_STATE_WAITING; + self->has_memrefs = 0; +} + +int rc_lboard_size(const char* memaddr) { + rc_lboard_with_memrefs_t* lboard; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); + + lboard = RC_ALLOC(rc_lboard_with_memrefs_t, &preparse.parse); + rc_parse_lboard_internal(&lboard->lboard, memaddr, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); + + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; +} + +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx) { + rc_lboard_with_memrefs_t* lboard; + rc_preparse_state_t preparse; + + (void)unused_L; + (void)unused_funcs_idx; + + if (!buffer || !memaddr) + return 0; + + rc_init_preparse_state(&preparse); + lboard = RC_ALLOC(rc_lboard_with_memrefs_t, &preparse.parse); + rc_parse_lboard_internal(&lboard->lboard, memaddr, &preparse.parse); + + rc_reset_parse_state(&preparse.parse, buffer); + lboard = RC_ALLOC(rc_lboard_with_memrefs_t, &preparse.parse); + rc_preparse_alloc_memrefs(&lboard->memrefs, &preparse); + + rc_parse_lboard_internal(&lboard->lboard, memaddr, &preparse.parse); + lboard->lboard.has_memrefs = 1; + + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &lboard->lboard : NULL; +} + +static void rc_update_lboard_memrefs(rc_lboard_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_lboard_with_memrefs_t* lboard = (rc_lboard_with_memrefs_t*)self; + rc_update_memref_values(&lboard->memrefs, peek, ud); + } +} + +int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* peek_ud, void* unused_L) { + int start_ok, cancel_ok, submit_ok; + + rc_update_lboard_memrefs(self, peek, peek_ud); + + if (self->state == RC_LBOARD_STATE_INACTIVE || self->state == RC_LBOARD_STATE_DISABLED) + return RC_LBOARD_STATE_INACTIVE; + + /* these are always tested once every frame, to ensure hit counts work properly */ + start_ok = rc_test_trigger(&self->start, peek, peek_ud, unused_L); + cancel_ok = rc_test_trigger(&self->cancel, peek, peek_ud, unused_L); + submit_ok = rc_test_trigger(&self->submit, peek, peek_ud, unused_L); + + switch (self->state) + { + case RC_LBOARD_STATE_WAITING: + case RC_LBOARD_STATE_TRIGGERED: + case RC_LBOARD_STATE_CANCELED: + /* don't activate/reactivate until the start condition becomes false */ + if (start_ok) { + *value = 0; + return RC_LBOARD_STATE_INACTIVE; /* just return inactive for all of these */ + } + + /* start condition is false, allow the leaderboard to start on future frames */ + self->state = RC_LBOARD_STATE_ACTIVE; + break; + + case RC_LBOARD_STATE_ACTIVE: + /* leaderboard attempt is not in progress. if the start condition is true and the cancel condition is not, start the attempt */ + if (start_ok && !cancel_ok) { + if (submit_ok) { + /* start and submit are both true in the same frame, just submit without announcing the leaderboard is available */ + self->state = RC_LBOARD_STATE_TRIGGERED; + } + else if (!self->start.requirement && !self->start.alternative) { + /* start trigger is empty. assume the leaderboard is in development and ignore */ + } + else { + /* start the leaderboard attempt */ + self->state = RC_LBOARD_STATE_STARTED; + } + + /* reset any hit counts in the value */ + if (self->progress) + rc_reset_value(self->progress); + + rc_reset_value(&self->value); + } + break; + + case RC_LBOARD_STATE_STARTED: + /* leaderboard attempt in progress */ + if (cancel_ok) { + /* cancel condition is true, abort the attempt */ + self->state = RC_LBOARD_STATE_CANCELED; + } + else if (submit_ok) { + /* submit condition is true, submit the current value */ + self->state = RC_LBOARD_STATE_TRIGGERED; + } + break; + } + + /* Calculate the value */ + switch (self->state) { + case RC_LBOARD_STATE_STARTED: + if (self->progress) { + *value = rc_evaluate_value(self->progress, peek, peek_ud, unused_L); + break; + } + /* fallthrough */ /* to RC_LBOARD_STATE_TRIGGERED */ + + case RC_LBOARD_STATE_TRIGGERED: + *value = rc_evaluate_value(&self->value, peek, peek_ud, unused_L); + break; + + default: + *value = 0; + break; + } + + return self->state; +} + +int rc_lboard_state_active(int state) { + switch (state) + { + case RC_LBOARD_STATE_DISABLED: + case RC_LBOARD_STATE_INACTIVE: + return 0; + + default: + return 1; + } +} + +void rc_reset_lboard(rc_lboard_t* self) { + if (!self) + return; + + self->state = RC_LBOARD_STATE_WAITING; + + rc_reset_trigger(&self->start); + rc_reset_trigger(&self->submit); + rc_reset_trigger(&self->cancel); + + if (self->progress) + rc_reset_value(self->progress); + + rc_reset_value(&self->value); +} diff --git a/src/rcheevos/src/rcheevos/memref.c b/src/rcheevos/src/rcheevos/memref.c new file mode 100644 index 0000000000..9c8f16066e --- /dev/null +++ b/src/rcheevos/src/rcheevos/memref.c @@ -0,0 +1,805 @@ +#include "rc_internal.h" + +#include /* malloc/realloc */ +#include /* memcpy */ +#include /* INFINITY/NAN */ + +#define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF + +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size) { + rc_memref_list_t* memref_list = NULL; + rc_memref_t* memref = NULL; + int i; + + for (i = 0; i < 2; i++) { + if (i == 0) { + if (!parse->existing_memrefs) + continue; + + memref_list = &parse->existing_memrefs->memrefs; + } + else { + memref_list = &parse->memrefs->memrefs; + } + + do + { + const rc_memref_t* memref_stop; + + memref = memref_list->items; + memref_stop = memref + memref_list->count; + + for (; memref < memref_stop; ++memref) { + if (memref->address == address && memref->value.size == size) + return memref; + } + + if (!memref_list->next) + break; + + memref_list = memref_list->next; + } while (1); + } + + /* no match found, find a place to put the new entry */ + memref_list = &parse->memrefs->memrefs; + while (memref_list->count == memref_list->capacity && memref_list->next) + memref_list = memref_list->next; + + /* create a new entry */ + if (memref_list->count < memref_list->capacity) { + memref = &memref_list->items[memref_list->count++]; + } else { + const int32_t old_offset = parse->offset; + + if (memref_list->capacity != 0) { + memref_list = memref_list->next = RC_ALLOC_SCRATCH(rc_memref_list_t, parse); + memref_list->next = NULL; + } + + memref_list->items = RC_ALLOC_ARRAY_SCRATCH(rc_memref_t, 8, parse); + memref_list->count = 1; + memref_list->capacity = 8; + memref_list->allocated = 0; + + memref = memref_list->items; + + /* in preparse mode, don't count this memory, we'll do a single allocation once we have + * the final total */ + if (!parse->buffer) + parse->offset = old_offset; + } + + memset(memref, 0, sizeof(*memref)); + memref->value.memref_type = RC_MEMREF_TYPE_MEMREF; + memref->value.type = RC_VALUE_TYPE_UNSIGNED; + memref->value.size = size; + memref->address = address; + + return memref; +} + +rc_modified_memref_t* rc_alloc_modified_memref(rc_parse_state_t* parse, uint8_t size, const rc_operand_t* parent, + uint8_t modifier_type, const rc_operand_t* modifier) { + rc_modified_memref_list_t* modified_memref_list = NULL; + rc_modified_memref_t* modified_memref = NULL; + int i = 0; + + for (i = 0; i < 2; i++) { + if (i == 0) { + if (!parse->existing_memrefs) + continue; + + modified_memref_list = &parse->existing_memrefs->modified_memrefs; + } + else { + modified_memref_list = &parse->memrefs->modified_memrefs; + } + + do { + const rc_modified_memref_t* memref_stop; + + modified_memref = modified_memref_list->items; + memref_stop = modified_memref + modified_memref_list->count; + + for (; modified_memref < memref_stop; ++modified_memref) { + if (modified_memref->memref.value.size == size && + modified_memref->modifier_type == modifier_type && + rc_operands_are_equal(&modified_memref->parent, parent) && + rc_operands_are_equal(&modified_memref->modifier, modifier)) { + return modified_memref; + } + } + + if (!modified_memref_list->next) + break; + + modified_memref_list = modified_memref_list->next; + } while (1); + } + + /* no match found, find a place to put the new entry */ + modified_memref_list = &parse->memrefs->modified_memrefs; + while (modified_memref_list->count == modified_memref_list->capacity && modified_memref_list->next) + modified_memref_list = modified_memref_list->next; + + /* create a new entry */ + if (modified_memref_list->count < modified_memref_list->capacity) { + modified_memref = &modified_memref_list->items[modified_memref_list->count++]; + } else { + const int32_t old_offset = parse->offset; + + if (modified_memref_list->capacity != 0) { + modified_memref_list = modified_memref_list->next = RC_ALLOC_SCRATCH(rc_modified_memref_list_t, parse); + modified_memref_list->next = NULL; + } + + modified_memref_list->items = RC_ALLOC_ARRAY_SCRATCH(rc_modified_memref_t, 8, parse); + modified_memref_list->count = 1; + modified_memref_list->capacity = 8; + modified_memref_list->allocated = 0; + + modified_memref = modified_memref_list->items; + + /* in preparse mode, don't count this memory, we'll do a single allocation once we have + * the final total */ + if (!parse->buffer) + parse->offset = old_offset; + } + + memset(modified_memref, 0, sizeof(*modified_memref)); + modified_memref->memref.value.memref_type = RC_MEMREF_TYPE_MODIFIED_MEMREF; + modified_memref->memref.value.size = size; + modified_memref->memref.value.type = rc_memsize_is_float(size) ? RC_VALUE_TYPE_FLOAT : RC_VALUE_TYPE_UNSIGNED; + memcpy(&modified_memref->parent, parent, sizeof(modified_memref->parent)); + memcpy(&modified_memref->modifier, modifier, sizeof(modified_memref->modifier)); + modified_memref->modifier_type = modifier_type; + modified_memref->depth = 0; + modified_memref->memref.address = rc_operand_is_memref(modifier) ? modifier->value.memref->address : modifier->value.num; + + if (rc_operand_is_memref(parent) && parent->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + const rc_modified_memref_t* parent_modified_memref = (rc_modified_memref_t*)parent->value.memref; + modified_memref->depth = parent_modified_memref->depth + 1; + } + + return modified_memref; +} + +void rc_memrefs_init(rc_memrefs_t* memrefs) +{ + memset(memrefs, 0, sizeof(*memrefs)); + + memrefs->memrefs.capacity = 32; + memrefs->memrefs.items = + (rc_memref_t*)malloc(memrefs->memrefs.capacity * sizeof(rc_memref_t)); + memrefs->memrefs.allocated = 1; + + memrefs->modified_memrefs.capacity = 16; + memrefs->modified_memrefs.items = + (rc_modified_memref_t*)malloc(memrefs->modified_memrefs.capacity * sizeof(rc_modified_memref_t)); + memrefs->modified_memrefs.allocated = 1; +} + +void rc_memrefs_destroy(rc_memrefs_t* memrefs) +{ + rc_memref_list_t* memref_list = &memrefs->memrefs; + rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + + do { + rc_memref_list_t* current_memref_list = memref_list; + memref_list = memref_list->next; + + if (current_memref_list->allocated) { + if (current_memref_list->items) + free(current_memref_list->items); + + if (current_memref_list != &memrefs->memrefs) + free(current_memref_list); + } + } while (memref_list); + + do { + rc_modified_memref_list_t* current_modified_memref_list = modified_memref_list; + modified_memref_list = modified_memref_list->next; + + if (current_modified_memref_list->allocated) { + if (current_modified_memref_list->items) + free(current_modified_memref_list->items); + + if (current_modified_memref_list != &memrefs->modified_memrefs) + free(current_modified_memref_list); + } + } while (modified_memref_list); + + free(memrefs); +} + +uint32_t rc_memrefs_count_memrefs(const rc_memrefs_t* memrefs) +{ + uint32_t count = 0; + const rc_memref_list_t* memref_list = &memrefs->memrefs; + while (memref_list) { + count += memref_list->count; + memref_list = memref_list->next; + } + + return count; +} + +uint32_t rc_memrefs_count_modified_memrefs(const rc_memrefs_t* memrefs) +{ + uint32_t count = 0; + const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + while (modified_memref_list) { + count += modified_memref_list->count; + modified_memref_list = modified_memref_list->next; + } + + return count; +} + +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address) { + const char* aux = *memaddr; + char* end; + unsigned long value; + + if (aux[0] == '0') { + if (aux[1] != 'x' && aux[1] != 'X') + return RC_INVALID_MEMORY_OPERAND; + + aux += 2; + switch (*aux++) { + /* ordered by estimated frequency in case compiler doesn't build a jump table */ + case 'h': case 'H': *size = RC_MEMSIZE_8_BITS; break; + case ' ': *size = RC_MEMSIZE_16_BITS; break; + case 'x': case 'X': *size = RC_MEMSIZE_32_BITS; break; + + case 'm': case 'M': *size = RC_MEMSIZE_BIT_0; break; + case 'n': case 'N': *size = RC_MEMSIZE_BIT_1; break; + case 'o': case 'O': *size = RC_MEMSIZE_BIT_2; break; + case 'p': case 'P': *size = RC_MEMSIZE_BIT_3; break; + case 'q': case 'Q': *size = RC_MEMSIZE_BIT_4; break; + case 'r': case 'R': *size = RC_MEMSIZE_BIT_5; break; + case 's': case 'S': *size = RC_MEMSIZE_BIT_6; break; + case 't': case 'T': *size = RC_MEMSIZE_BIT_7; break; + case 'l': case 'L': *size = RC_MEMSIZE_LOW; break; + case 'u': case 'U': *size = RC_MEMSIZE_HIGH; break; + case 'k': case 'K': *size = RC_MEMSIZE_BITCOUNT; break; + case 'w': case 'W': *size = RC_MEMSIZE_24_BITS; break; + case 'g': case 'G': *size = RC_MEMSIZE_32_BITS_BE; break; + case 'i': case 'I': *size = RC_MEMSIZE_16_BITS_BE; break; + case 'j': case 'J': *size = RC_MEMSIZE_24_BITS_BE; break; + + /* case 'v': case 'V': */ + /* case 'y': case 'Y': 64 bit? */ + /* case 'z': case 'Z': 128 bit? */ + + case '0': + if (*aux == 'x') /* user mistyped an extra 0x: 0x0xabcd */ + return RC_INVALID_MEMORY_OPERAND; + /* fallthrough */ + + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + /* legacy support - addresses without a size prefix are assumed to be 16-bit */ + aux--; + *size = RC_MEMSIZE_16_BITS; + break; + + default: + return RC_INVALID_MEMORY_OPERAND; + } + } + else if (aux[0] == 'f' || aux[0] == 'F') { + ++aux; + switch (*aux++) { + case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; + case 'b': case 'B': *size = RC_MEMSIZE_FLOAT_BE; break; + case 'h': case 'H': *size = RC_MEMSIZE_DOUBLE32; break; + case 'i': case 'I': *size = RC_MEMSIZE_DOUBLE32_BE; break; + case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; + case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; + + default: + return RC_INVALID_FP_OPERAND; + } + } + else { + return RC_INVALID_MEMORY_OPERAND; + } + + value = strtoul(aux, &end, 16); + + if (end == aux) + return RC_INVALID_MEMORY_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + *address = (uint32_t)value; + *memaddr = end; + return RC_OK; +} + +static float rc_build_float(uint32_t mantissa_bits, int32_t exponent, int sign) { + /* 32-bit float has a 23-bit mantissa and 8-bit exponent */ + const uint32_t implied_bit = 1 << 23; + const uint32_t mantissa = mantissa_bits | implied_bit; + double dbl = ((double)mantissa) / ((double)implied_bit); + + if (exponent > 127) { + /* exponent above 127 is a special number */ + if (mantissa_bits == 0) { + /* infinity */ +#ifdef INFINITY /* INFINITY and NAN #defines require C99 */ + dbl = (double)INFINITY; +#else + dbl = -log(0.0); +#endif + } + else { + /* NaN */ +#ifdef NAN + dbl = NAN; +#else + dbl = -sqrt(-1); +#endif + } + } + else if (exponent > 0) { + /* exponent from 1 to 127 is a number greater than 1 */ + while (exponent > 30) { + dbl *= (double)(1 << 30); + exponent -= 30; + } + dbl *= (double)((long long)1 << exponent); + } + else if (exponent < 0) { + /* exponent from -1 to -127 is a number less than 1 */ + + if (exponent == -127) { + /* exponent -127 (all exponent bits were zero) is a denormalized value + * (no implied leading bit) with exponent -126 */ + dbl = ((double)mantissa_bits) / ((double)implied_bit); + exponent = 126; + } else { + exponent = -exponent; + } + + while (exponent > 30) { + dbl /= (double)(1 << 30); + exponent -= 30; + } + dbl /= (double)((long long)1 << exponent); + } + else { + /* exponent of 0 requires no adjustment */ + } + + return (sign) ? (float)-dbl : (float)dbl; +} + +static void rc_transform_memref_float(rc_typed_value_t* value) { + /* decodes an IEEE 754 float */ + const uint32_t mantissa = (value->value.u32 & 0x7FFFFF); + const int32_t exponent = (int32_t)((value->value.u32 >> 23) & 0xFF) - 127; + const int sign = (value->value.u32 & 0x80000000); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_float_be(rc_typed_value_t* value) { + /* decodes an IEEE 754 float in big endian format */ + const uint32_t mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00007F00) << 8); + const int32_t exponent = (int32_t)(((value->value.u32 & 0x0000007F) << 1) | + ((value->value.u32 & 0x00008000) >> 15)) - 127; + const int sign = (value->value.u32 & 0x00000080); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_double32(rc_typed_value_t* value) +{ + /* decodes the four most significant bytes of an IEEE 754 double into a float */ + const uint32_t mantissa = (value->value.u32 & 0x000FFFFF) << 3; + const int32_t exponent = (int32_t)((value->value.u32 >> 20) & 0x7FF) - 1023; + const int sign = (value->value.u32 & 0x80000000); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_double32_be(rc_typed_value_t* value) +{ + /* decodes the four most significant bytes of an IEEE 754 double in big endian format into a float */ + const uint32_t mantissa = (((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00000F00) << 8)) << 3; + const int32_t exponent = (int32_t)(((value->value.u32 & 0x0000007F) << 4) | + ((value->value.u32 & 0x0000F000) >> 12)) - 1023; + const int sign = (value->value.u32 & 0x00000080); + value->value.f32 = rc_build_float(mantissa, exponent, sign); + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_mbf32(rc_typed_value_t* value) { + /* decodes a Microsoft Binary Format float */ + /* NOTE: 32-bit MBF is stored in memory as big endian (at least for Apple II) */ + const uint32_t mantissa = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x00007F00) << 8); + const int32_t exponent = (int32_t)(value->value.u32 & 0xFF) - 129; + const int sign = (value->value.u32 & 0x00008000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + +static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { + /* decodes a Microsoft Binary Format float */ + /* Locomotive BASIC (CPC) uses MBF40, but in little endian format */ + const uint32_t mantissa = value->value.u32 & 0x007FFFFF; + const int32_t exponent = (int32_t)(value->value.u32 >> 24) - 129; + const int sign = (value->value.u32 & 0x00800000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + +static const uint8_t rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; + +void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size) { + /* ASSERT: value->type == RC_VALUE_TYPE_UNSIGNED */ + switch (size) + { + case RC_MEMSIZE_8_BITS: + value->value.u32 = (value->value.u32 & 0x000000ff); + break; + + case RC_MEMSIZE_16_BITS: + value->value.u32 = (value->value.u32 & 0x0000ffff); + break; + + case RC_MEMSIZE_24_BITS: + value->value.u32 = (value->value.u32 & 0x00ffffff); + break; + + case RC_MEMSIZE_32_BITS: + break; + + case RC_MEMSIZE_BIT_0: + value->value.u32 = (value->value.u32 >> 0) & 1; + break; + + case RC_MEMSIZE_BIT_1: + value->value.u32 = (value->value.u32 >> 1) & 1; + break; + + case RC_MEMSIZE_BIT_2: + value->value.u32 = (value->value.u32 >> 2) & 1; + break; + + case RC_MEMSIZE_BIT_3: + value->value.u32 = (value->value.u32 >> 3) & 1; + break; + + case RC_MEMSIZE_BIT_4: + value->value.u32 = (value->value.u32 >> 4) & 1; + break; + + case RC_MEMSIZE_BIT_5: + value->value.u32 = (value->value.u32 >> 5) & 1; + break; + + case RC_MEMSIZE_BIT_6: + value->value.u32 = (value->value.u32 >> 6) & 1; + break; + + case RC_MEMSIZE_BIT_7: + value->value.u32 = (value->value.u32 >> 7) & 1; + break; + + case RC_MEMSIZE_LOW: + value->value.u32 = value->value.u32 & 0x0f; + break; + + case RC_MEMSIZE_HIGH: + value->value.u32 = (value->value.u32 >> 4) & 0x0f; + break; + + case RC_MEMSIZE_BITCOUNT: + value->value.u32 = rc_bits_set[(value->value.u32 & 0x0F)] + + rc_bits_set[((value->value.u32 >> 4) & 0x0F)]; + break; + + case RC_MEMSIZE_16_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF00) >> 8) | + ((value->value.u32 & 0x00FF) << 8); + break; + + case RC_MEMSIZE_24_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF0000) >> 16) | + (value->value.u32 & 0x00FF00) | + ((value->value.u32 & 0x0000FF) << 16); + break; + + case RC_MEMSIZE_32_BITS_BE: + value->value.u32 = ((value->value.u32 & 0xFF000000) >> 24) | + ((value->value.u32 & 0x00FF0000) >> 8) | + ((value->value.u32 & 0x0000FF00) << 8) | + ((value->value.u32 & 0x000000FF) << 24); + break; + + case RC_MEMSIZE_FLOAT: + rc_transform_memref_float(value); + break; + + case RC_MEMSIZE_FLOAT_BE: + rc_transform_memref_float_be(value); + break; + + case RC_MEMSIZE_DOUBLE32: + rc_transform_memref_double32(value); + break; + + case RC_MEMSIZE_DOUBLE32_BE: + rc_transform_memref_double32_be(value); + break; + + case RC_MEMSIZE_MBF32: + rc_transform_memref_mbf32(value); + break; + + case RC_MEMSIZE_MBF32_LE: + rc_transform_memref_mbf32_le(value); + break; + + default: + break; + } +} + +static const uint32_t rc_memref_masks[] = { + 0x000000ff, /* RC_MEMSIZE_8_BITS */ + 0x0000ffff, /* RC_MEMSIZE_16_BITS */ + 0x00ffffff, /* RC_MEMSIZE_24_BITS */ + 0xffffffff, /* RC_MEMSIZE_32_BITS */ + 0x0000000f, /* RC_MEMSIZE_LOW */ + 0x000000f0, /* RC_MEMSIZE_HIGH */ + 0x00000001, /* RC_MEMSIZE_BIT_0 */ + 0x00000002, /* RC_MEMSIZE_BIT_1 */ + 0x00000004, /* RC_MEMSIZE_BIT_2 */ + 0x00000008, /* RC_MEMSIZE_BIT_3 */ + 0x00000010, /* RC_MEMSIZE_BIT_4 */ + 0x00000020, /* RC_MEMSIZE_BIT_5 */ + 0x00000040, /* RC_MEMSIZE_BIT_6 */ + 0x00000080, /* RC_MEMSIZE_BIT_7 */ + 0x000000ff, /* RC_MEMSIZE_BITCOUNT */ + 0x0000ffff, /* RC_MEMSIZE_16_BITS_BE */ + 0x00ffffff, /* RC_MEMSIZE_24_BITS_BE */ + 0xffffffff, /* RC_MEMSIZE_32_BITS_BE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT */ + 0xffffffff, /* RC_MEMSIZE_MBF32 */ + 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ + 0xffffffff, /* RC_MEMSIZE_FLOAT_BE */ + 0xffffffff, /* RC_MEMSIZE_DOUBLE32 */ + 0xffffffff, /* RC_MEMSIZE_DOUBLE32_BE*/ + 0xffffffff /* RC_MEMSIZE_VARIABLE */ +}; + +uint32_t rc_memref_mask(uint8_t size) { + const size_t index = (size_t)size; + if (index >= sizeof(rc_memref_masks) / sizeof(rc_memref_masks[0])) + return 0xffffffff; + + return rc_memref_masks[index]; +} + +/* all sizes less than 8-bits (1 byte) are mapped to 8-bits. 24-bit is mapped to 32-bit + * as we don't expect the client to understand a request for 3 bytes. all other reads are + * mapped to the little-endian read of the same size. */ +static const uint8_t rc_memref_shared_sizes[] = { + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_8_BITS */ + RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_LOW */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_HIGH */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_0 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_1 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_2 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_3 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_4 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_5 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_6 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BIT_7 */ + RC_MEMSIZE_8_BITS, /* RC_MEMSIZE_BITCOUNT */ + RC_MEMSIZE_16_BITS, /* RC_MEMSIZE_16_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_24_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT_BE */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_DOUBLE32 */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_DOUBLE32_BE*/ + RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ +}; + +uint8_t rc_memref_shared_size(uint8_t size) { + const size_t index = (size_t)size; + if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) + return size; + + return rc_memref_shared_sizes[index]; +} + +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud) { + if (!peek) + return 0; + + switch (size) + { + case RC_MEMSIZE_8_BITS: + return peek(address, 1, ud); + + case RC_MEMSIZE_16_BITS: + return peek(address, 2, ud); + + case RC_MEMSIZE_32_BITS: + return peek(address, 4, ud); + + default: + { + uint32_t value; + const size_t index = (size_t)size; + if (index >= sizeof(rc_memref_shared_sizes) / sizeof(rc_memref_shared_sizes[0])) + return 0; + + /* fetch the larger value and mask off the bits associated to the specified size + * for correct deduction of prior value. non-prior memrefs should already be using + * shared size memrefs to minimize the total number of memory reads required. */ + value = rc_peek_value(address, rc_memref_shared_sizes[index], peek, ud); + return value & rc_memref_masks[index]; + } + } +} + +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t new_value) { + if (memref->value == new_value) { + memref->changed = 0; + } + else { + memref->prior = memref->value; + memref->value = new_value; + memref->changed = 1; + } +} + +void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs) +{ + if (memrefs) + memset(memrefs, 0, sizeof(*memrefs)); + + parse->memrefs = memrefs; +} + +static uint32_t rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { + switch (operand_type) + { + /* most common case explicitly first, even though it could be handled by default case. + * this helps the compiler to optimize if it turns the switch into a series of if/elses */ + case RC_OPERAND_ADDRESS: + return memref->value; + + case RC_OPERAND_DELTA: + if (!memref->changed) { + /* fallthrough */ + default: + return memref->value; + } + /* fallthrough */ + case RC_OPERAND_PRIOR: + return memref->prior; + } +} + +void rc_get_memref_value(rc_typed_value_t* value, rc_memref_t* memref, int operand_type) { + value->type = memref->value.type; + value->value.u32 = rc_get_memref_value_value(&memref->value, operand_type); +} + +uint32_t rc_get_modified_memref_value(const rc_modified_memref_t* memref, rc_peek_t peek, void* ud) { + rc_typed_value_t value, modifier; + + rc_evaluate_operand(&value, &memref->parent, NULL); + rc_evaluate_operand(&modifier, &memref->modifier, NULL); + + switch (memref->modifier_type) { + case RC_OPERATOR_INDIRECT_READ: + rc_typed_value_add(&value, &modifier); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + value.value.u32 = rc_peek_value(value.value.u32, memref->memref.value.size, peek, ud); + value.type = memref->memref.value.type; + break; + + case RC_OPERATOR_SUB_PARENT: + /* sub parent is "-parent + modifier" */ + rc_typed_value_negate(&value); + rc_typed_value_add(&value, &modifier); + rc_typed_value_convert(&value, memref->memref.value.type); + break; + + case RC_OPERATOR_SUB_ACCUMULATOR: + rc_typed_value_negate(&modifier); + /* fallthrough */ /* to case RC_OPERATOR_SUB_ACCUMULATOR */ + + case RC_OPERATOR_ADD_ACCUMULATOR: + /* when modifying the accumulator, force the modifier to match the accumulator + * type instead of promoting them both to the less restrictive type. + * + * 18 - 17.5 will result in an integer. should it be 0 or 1? + * + * default: float is less restrictive, convert both to float for combine, + * then convert to the memref type. + * (int)((float)18 - 17.5) -> (int)(0.5) -> 0 + * + * accumulator is integer: force modifier to be integer before combining + * (int)(18 - (int)17.5) -> (int)(18 - 17) -> 1 + */ + rc_typed_value_convert(&modifier, value.type); + rc_typed_value_add(&value, &modifier); + rc_typed_value_convert(&value, memref->memref.value.type); + break; + + default: + rc_typed_value_combine(&value, &modifier, memref->modifier_type); + rc_typed_value_convert(&value, memref->memref.value.type); + break; + } + + return value.value.u32; +} + +void rc_update_memref_values(rc_memrefs_t* memrefs, rc_peek_t peek, void* ud) { + rc_memref_list_t* memref_list; + rc_modified_memref_list_t* modified_memref_list; + + memref_list = &memrefs->memrefs; + do + { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + + for (; memref < memref_stop; ++memref) { + if (memref->value.type != RC_VALUE_TYPE_NONE) + rc_update_memref_value(&memref->value, rc_peek_value(memref->address, memref->value.size, peek, ud)); + } + + memref_list = memref_list->next; + } while (memref_list); + + modified_memref_list = &memrefs->modified_memrefs; + if (modified_memref_list->count) { + do { + rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_stop = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_stop; ++modified_memref) + rc_update_memref_value(&modified_memref->memref.value, rc_get_modified_memref_value(modified_memref, peek, ud)); + + modified_memref_list = modified_memref_list->next; + } while (modified_memref_list); + } +} diff --git a/src/rcheevos/src/rcheevos/operand.c b/src/rcheevos/src/rcheevos/operand.c new file mode 100644 index 0000000000..ae18d93155 --- /dev/null +++ b/src/rcheevos/src/rcheevos/operand.c @@ -0,0 +1,607 @@ +#include "rc_internal.h" + +#include +#include +#include +#include + +static int rc_parse_operand_func_call(rc_operand_t* self, const char** memaddr) { + const char* aux = *memaddr; + + if (*aux++ != '@') { + return RC_INVALID_FUNC_OPERAND; + } + + if (!isalpha((unsigned char)*aux)) { + return RC_INVALID_FUNC_OPERAND; + } + + while (isalnum((unsigned char)*aux) || *aux == '_') { + aux++; + } + + self->type = RC_OPERAND_FUNC; + self->size = RC_MEMSIZE_32_BITS; + self->memref_access_type = RC_OPERAND_ADDRESS; + *memaddr = aux; + return RC_OK; +} + +static int rc_parse_operand_variable(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { + const char* aux = *memaddr; + size_t i; + char varName[RC_VALUE_MAX_NAME_LENGTH + 1] = { 0 }; + + for (i = 0; i < RC_VALUE_MAX_NAME_LENGTH && *aux != '}'; i++) { + if (!rc_is_valid_variable_character(*aux, i == 0)) + return RC_INVALID_VARIABLE_NAME; + + varName[i] = *aux++; + } + + if (i == 0) + return RC_INVALID_VARIABLE_NAME; + + if (*aux != '}') + return RC_INVALID_VARIABLE_NAME; + + ++aux; + + if (strcmp(varName, "recall") == 0) { + if (parse->remember.type == RC_OPERAND_NONE) { + self->value.memref = NULL; + self->size = RC_MEMSIZE_32_BITS; + self->memref_access_type = RC_OPERAND_ADDRESS; + } + else { + memcpy(self, &parse->remember, sizeof(*self)); + self->is_combining = 0; + self->memref_access_type = self->type; + } + self->type = RC_OPERAND_RECALL; + } + else { /* process named variable when feature is available.*/ + return RC_UNKNOWN_VARIABLE_NAME; + } + + *memaddr = aux; + return RC_OK; +} + +static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { + const char* aux = *memaddr; + uint32_t address; + uint8_t size; + int ret; + + switch (*aux) { + case 'd': case 'D': + self->type = RC_OPERAND_DELTA; + ++aux; + break; + + case 'p': case 'P': + self->type = RC_OPERAND_PRIOR; + ++aux; + break; + + case 'b': case 'B': + self->type = RC_OPERAND_BCD; + ++aux; + break; + + case '~': + self->type = RC_OPERAND_INVERTED; + ++aux; + break; + + default: + self->type = RC_OPERAND_ADDRESS; + break; + } + + self->memref_access_type = self->type; + + ret = rc_parse_memref(&aux, &self->size, &address); + if (ret != RC_OK) + return ret; + + size = rc_memref_shared_size(self->size); + if (size != self->size && self->type == RC_OPERAND_PRIOR) { + /* if the shared size differs from the requested size and it's a prior operation, we + * have to check to make sure both sizes use the same mask, or the prior value may be + * updated when bits outside the mask are modified, which would make it look like the + * current value once the mask is applied. if the mask differs, create a new + * non-shared record for tracking the prior data. */ + if (rc_memref_mask(size) != rc_memref_mask(self->size)) + size = self->size; + } + + if (parse->indirect_parent.type != RC_OPERAND_NONE) { + rc_operand_t offset; + rc_operand_set_const(&offset, address); + + self->value.memref = (rc_memref_t*)rc_alloc_modified_memref(parse, + size, &parse->indirect_parent, RC_OPERATOR_INDIRECT_READ, &offset); + } + else { + self->value.memref = rc_alloc_memref(parse, address, size); + } + + if (parse->offset < 0) + return parse->offset; + + *memaddr = aux; + return RC_OK; +} + +int rc_parse_operand(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { + const char* aux = *memaddr; + char* end; + int ret; + unsigned long value; + int negative; + int allow_decimal = 0; + + self->is_combining = 0; + + switch (*aux) { + case 'h': case 'H': /* hex constant */ + if (aux[2] == 'x' || aux[2] == 'X') { + /* H0x1234 is a typo - either H1234 or 0xH1234 was probably meant */ + return RC_INVALID_CONST_OPERAND; + } + + value = strtoul(++aux, &end, 16); + if (end == aux) + return RC_INVALID_CONST_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + rc_operand_set_const(self, (unsigned)value); + + aux = end; + break; + + case 'f': case 'F': /* floating point constant */ + if (isalpha((unsigned char)aux[1])) { + ret = rc_parse_operand_memory(self, &aux, parse); + + if (ret < 0) + return ret; + + break; + } + allow_decimal = 1; + /* fall through */ + case 'v': case 'V': /* signed integer constant */ + ++aux; + /* fall through */ + case '+': case '-': /* signed integer constant */ + negative = 0; + if (*aux == '-') { + negative = 1; + ++aux; + } + else if (*aux == '+') { + ++aux; + } + + value = strtoul(aux, &end, 10); + + if (*end == '.' && allow_decimal) { + /* custom parser for decimal values to ignore locale */ + unsigned long shift = 1; + unsigned long fraction = 0; + double dbl_val; + + aux = end + 1; + if (*aux < '0' || *aux > '9') + return RC_INVALID_FP_OPERAND; + + do { + /* only keep as many digits as will fit in a 32-bit value to prevent overflow. + * float only has around 7 digits of precision anyway. */ + if (shift < 1000000000) { + fraction *= 10; + fraction += (*aux - '0'); + shift *= 10; + } + ++aux; + } while (*aux >= '0' && *aux <= '9'); + + if (fraction != 0) { + /* non-zero fractional part, convert to double and merge in integer portion */ + const double dbl_fraction = ((double)fraction) / ((double)shift); + if (negative) + dbl_val = ((double)(-((long)value))) - dbl_fraction; + else + dbl_val = (double)value + dbl_fraction; + } + else { + /* fractional part is 0, just convert the integer portion */ + if (negative) + dbl_val = (double)(-((long)value)); + else + dbl_val = (double)value; + } + + rc_operand_set_float_const(self, dbl_val); + } + else { + /* not a floating point value, make sure something was read and advance the read pointer */ + if (end == aux) + return allow_decimal ? RC_INVALID_FP_OPERAND : RC_INVALID_CONST_OPERAND; + + aux = end; + + if (value > 0x7fffffffU) + value = 0x7fffffffU; + + if (negative) + rc_operand_set_const(self, (unsigned)(-((long)value))); + else + rc_operand_set_const(self, (unsigned)value); + } + break; + case '{': /* variable */ + ++aux; + ret = rc_parse_operand_variable(self, &aux, parse); + if (ret < 0) + return ret; + + break; + + case '0': + if (aux[1] == 'x' || aux[1] == 'X') { /* hex integer constant */ + /* fallthrough */ /* to default */ + default: + ret = rc_parse_operand_memory(self, &aux, parse); + + if (ret < 0) + return ret; + + break; + } + /* fallthrough */ /* to case '1' for case '0' where not '0x' */ + case '1': case '2': case '3': case '4': case '5': /* unsigned integer constant */ + case '6': case '7': case '8': case '9': + value = strtoul(aux, &end, 10); + if (end == aux) + return RC_INVALID_CONST_OPERAND; + + if (value > 0xffffffffU) + value = 0xffffffffU; + + rc_operand_set_const(self, (unsigned)value); + + aux = end; + break; + + case '@': + ret = rc_parse_operand_func_call(self, &aux); + + if (ret < 0) + return ret; + + break; + } + + *memaddr = aux; + return RC_OK; +} + +void rc_operand_set_const(rc_operand_t* self, uint32_t value) { + self->size = RC_MEMSIZE_32_BITS; + self->type = RC_OPERAND_CONST; + self->memref_access_type = RC_OPERAND_NONE; + self->value.num = value; +} + +void rc_operand_set_float_const(rc_operand_t* self, double value) { + self->size = RC_MEMSIZE_FLOAT; + self->type = RC_OPERAND_FP; + self->memref_access_type = RC_OPERAND_NONE; + self->value.dbl = value; +} + +int rc_operands_are_equal(const rc_operand_t* left, const rc_operand_t* right) { + if (left == right) + return 1; + + if (left->type != right->type) + return 0; + + switch (left->type) { + case RC_OPERAND_CONST: + return (left->value.num == right->value.num); + case RC_OPERAND_FP: + return (left->value.dbl == right->value.dbl); + case RC_OPERAND_RECALL: + return (left->value.memref == right->value.memref); + default: + break; + } + + /* comparing two memrefs - look for exact matches on type and size */ + if (left->size != right->size || left->value.memref->value.memref_type != right->value.memref->value.memref_type) + return 0; + + switch (left->value.memref->value.memref_type) { + case RC_MEMREF_TYPE_MODIFIED_MEMREF: + { + const rc_modified_memref_t* left_memref = (const rc_modified_memref_t*)left->value.memref; + const rc_modified_memref_t* right_memref = (const rc_modified_memref_t*)right->value.memref; + return (left_memref->modifier_type == right_memref->modifier_type && + left_memref->depth == right_memref->depth && + rc_operands_are_equal(&left_memref->modifier, &right_memref->modifier) && + rc_operands_are_equal(&left_memref->parent, &right_memref->parent) && + 1 == 1 + ); + } + + default: + return (left->value.memref->address == right->value.memref->address && + left->value.memref->value.size == right->value.memref->value.size); + } +} + +int rc_operator_is_modifying(int oper) { + switch (oper) { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_DIV: + case RC_OPERATOR_MULT: + case RC_OPERATOR_MOD: + case RC_OPERATOR_ADD: + case RC_OPERATOR_SUB: + case RC_OPERATOR_NONE: /* NONE operator implies "* 1" */ + return 1; + + default: + return 0; + } +} + +int rc_memsize_is_float(uint8_t size) { + switch (size) { + case RC_MEMSIZE_FLOAT: + case RC_MEMSIZE_FLOAT_BE: + case RC_MEMSIZE_DOUBLE32: + case RC_MEMSIZE_DOUBLE32_BE: + case RC_MEMSIZE_MBF32: + case RC_MEMSIZE_MBF32_LE: + return 1; + + default: + return 0; + } +} + +int rc_operand_is_float_memref(const rc_operand_t* self) { + if (!rc_operand_is_memref(self)) + return 0; + + if (self->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + const rc_modified_memref_t* memref = (const rc_modified_memref_t*)self->value.memref; + if (memref->modifier_type != RC_OPERATOR_INDIRECT_READ) + return rc_memsize_is_float(self->value.memref->value.size); + } + + return rc_memsize_is_float(self->size); +} + +int rc_operand_type_is_memref(uint8_t type) { + switch (type) { + case RC_OPERAND_CONST: + case RC_OPERAND_FP: + case RC_OPERAND_FUNC: + case RC_OPERAND_RECALL: + return 0; + + default: + return 1; + } +} + +int rc_operand_type_is_transform(uint8_t type) { + switch (type) { + case RC_OPERAND_BCD: + case RC_OPERAND_INVERTED: + return 1; + + default: + return 0; + } +} + +int rc_operand_is_memref(const rc_operand_t* self) { + return rc_operand_type_is_memref(self->type); +} + +int rc_operand_is_recall(const rc_operand_t* self) { + switch (self->type) { + case RC_OPERAND_RECALL: + return 1; + + default: + return 0; + } +} + +int rc_operand_is_float(const rc_operand_t* self) { + if (self->type == RC_OPERAND_FP) + return 1; + + if (self->type == RC_OPERAND_RECALL) + return rc_memsize_is_float(self->size); + + return rc_operand_is_float_memref(self); +} + +static uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { + switch (self->type) + { + case RC_OPERAND_BCD: + switch (self->size) + { + case RC_MEMSIZE_8_BITS: + value = ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + value = ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + value = ((value >> 20) & 0x0f) * 100000 + + ((value >> 16) & 0x0f) * 10000 + + ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_32_BITS_BE: + case RC_MEMSIZE_VARIABLE: + value = ((value >> 28) & 0x0f) * 10000000 + + ((value >> 24) & 0x0f) * 1000000 + + ((value >> 20) & 0x0f) * 100000 + + ((value >> 16) & 0x0f) * 10000 + + ((value >> 12) & 0x0f) * 1000 + + ((value >> 8) & 0x0f) * 100 + + ((value >> 4) & 0x0f) * 10 + + ((value ) & 0x0f); + break; + + default: + break; + } + break; + + case RC_OPERAND_INVERTED: + switch (self->size) + { + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + value ^= 0x0f; + break; + + case RC_MEMSIZE_8_BITS: + value ^= 0xff; + break; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + value ^= 0xffff; + break; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + value ^= 0xffffff; + break; + + case RC_MEMSIZE_32_BITS: + case RC_MEMSIZE_32_BITS_BE: + case RC_MEMSIZE_VARIABLE: + value ^= 0xffffffff; + break; + + default: + value ^= 0x01; + break; + } + break; + + default: + break; + } + + return value; +} + +void rc_operand_addsource(rc_operand_t* self, rc_parse_state_t* parse, uint8_t new_size) { + rc_modified_memref_t* modified_memref; + + if ((self->type == RC_OPERAND_DELTA || self->type == RC_OPERAND_PRIOR) && + self->type == parse->addsource_parent.type) { + /* if adding prev(x) and prev(y), just add x and y and take the prev of that. + * same for adding prior(x) and prior(y). */ + rc_operand_t modifier; + memcpy(&modifier, self, sizeof(modifier)); + modifier.type = parse->addsource_parent.type = RC_OPERAND_ADDRESS; + + modified_memref = rc_alloc_modified_memref(parse, + new_size, &parse->addsource_parent, parse->addsource_oper, &modifier); + } + else { + modified_memref = rc_alloc_modified_memref(parse, + new_size, &parse->addsource_parent, parse->addsource_oper, self); + + /* the modified memref will contain the combination of modified values, take the current value from that */ + self->type = self->memref_access_type = RC_OPERAND_ADDRESS; + } + + self->value.memref = (rc_memref_t*)modified_memref; + + /* result of an AddSource operation is always a 32-bit integer (even if parent or modifier is a float) */ + self->size = RC_MEMSIZE_32_BITS; +} + +void rc_evaluate_operand(rc_typed_value_t* result, const rc_operand_t* self, rc_eval_state_t* eval_state) { + /* step 1: read memory */ + switch (self->type) { + case RC_OPERAND_CONST: + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = self->value.num; + return; + + case RC_OPERAND_FP: + result->type = RC_VALUE_TYPE_FLOAT; + result->value.f32 = (float)self->value.dbl; + return; + + case RC_OPERAND_FUNC: + /* this feature was never actualized */ + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = 0; + return; + + case RC_OPERAND_RECALL: + if (!rc_operand_type_is_memref(self->memref_access_type)) { + rc_operand_t recall; + memcpy(&recall, self, sizeof(recall)); + recall.type = self->memref_access_type; + rc_evaluate_operand(result, &recall, eval_state); + return; + } + + if (!self->value.memref) { + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = 0; + return; + } + + rc_get_memref_value(result, self->value.memref, self->memref_access_type); + break; + + default: + rc_get_memref_value(result, self->value.memref, self->type); + break; + } + + /* step 2: convert read memory to desired format */ + rc_transform_memref_value(result, self->size); + + /* step 3: apply logic (BCD/invert) */ + if (result->type == RC_VALUE_TYPE_UNSIGNED) + result->value.u32 = rc_transform_operand_value(result->value.u32, self); +} diff --git a/src/rcheevos/src/rcheevos/rc_internal.h b/src/rcheevos/src/rcheevos/rc_internal.h new file mode 100644 index 0000000000..f5c3b19b33 --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_internal.h @@ -0,0 +1,390 @@ +#ifndef RC_INTERNAL_H +#define RC_INTERNAL_H + +#include "rc_runtime_types.h" +#include "rc_util.h" + +RC_BEGIN_C_DECLS + +typedef struct rc_scratch_string { + char* value; + struct rc_scratch_string* left; + struct rc_scratch_string* right; +} +rc_scratch_string_t; + +typedef struct rc_modified_memref_t { + rc_memref_t memref; /* For compatibility with rc_operand_t.value.memref */ + rc_operand_t parent; /* The parent memref this memref is derived from (type will always be a memref type) */ + rc_operand_t modifier; /* The modifier to apply to the parent. */ + uint8_t modifier_type; /* How to apply the modifier to the parent. (RC_OPERATOR_*) */ + uint16_t depth; /* The number of parents this memref has. */ +} +rc_modified_memref_t; + +typedef struct rc_memref_list_t { + rc_memref_t* items; + struct rc_memref_list_t* next; + uint16_t count; + uint16_t capacity; + uint8_t allocated; +} rc_memref_list_t; + +typedef struct rc_modified_memref_list_t { + rc_modified_memref_t* items; + struct rc_modified_memref_list_t* next; + uint16_t count; + uint16_t capacity; + uint8_t allocated; +} rc_modified_memref_list_t; + +typedef struct rc_memrefs_t { + rc_memref_list_t memrefs; + rc_modified_memref_list_t modified_memrefs; +} rc_memrefs_t; + +typedef struct rc_trigger_with_memrefs_t { + rc_trigger_t trigger; + rc_memrefs_t memrefs; +} rc_trigger_with_memrefs_t; + +typedef struct rc_lboard_with_memrefs_t { + rc_lboard_t lboard; + rc_memrefs_t memrefs; +} rc_lboard_with_memrefs_t; + +typedef struct rc_richpresence_with_memrefs_t { + rc_richpresence_t richpresence; + rc_memrefs_t memrefs; +} rc_richpresence_with_memrefs_t; + +typedef struct rc_value_with_memrefs_t { + rc_value_t value; + rc_memrefs_t memrefs; +} rc_value_with_memrefs_t; + +/* enum helpers for natvis expansion. Have to use a struct to define the mapping, + * and a single field to allow the conditional logic to switch on the value */ +typedef struct __rc_bool_enum_t { uint8_t value; } __rc_bool_enum_t; +typedef struct __rc_memsize_enum_t { uint8_t value; } __rc_memsize_enum_t; +typedef struct __rc_memsize_enum_func_t { uint8_t value; } __rc_memsize_enum_func_t; +typedef struct __rc_operand_enum_t { uint8_t value; } __rc_operand_enum_t; +typedef struct __rc_value_type_enum_t { uint8_t value; } __rc_value_type_enum_t; +typedef struct __rc_memref_type_enum_t { uint8_t value; } __rc_memref_type_enum_t; +typedef struct __rc_condition_enum_t { uint8_t value; } __rc_condition_enum_t; +typedef struct __rc_condition_enum_str_t { uint8_t value; } __rc_condition_enum_str_t; +typedef struct __rc_condset_list_t { rc_condset_t* first_condset; } __rc_condset_list_t; +typedef struct __rc_operator_enum_t { uint8_t value; } __rc_operator_enum_t; +typedef struct __rc_operator_enum_str_t { uint8_t value; } __rc_operator_enum_str_t; +typedef struct __rc_operand_memref_t { rc_operand_t operand; } __rc_operand_memref_t; /* requires &rc_operand_t to be the same as &rc_operand_t.value.memref */ +typedef struct __rc_value_list_t { rc_value_t* first_value; } __rc_value_list_t; +typedef struct __rc_trigger_state_enum_t { uint8_t value; } __rc_trigger_state_enum_t; +typedef struct __rc_lboard_state_enum_t { uint8_t value; } __rc_lboard_state_enum_t; +typedef struct __rc_richpresence_display_list_t { rc_richpresence_display_t* first_display; } __rc_richpresence_display_list_t; +typedef struct __rc_richpresence_display_part_list_t { rc_richpresence_display_part_t* display; } __rc_richpresence_display_part_list_t; +typedef struct __rc_richpresence_lookup_list_t { rc_richpresence_lookup_t* first_lookup; } __rc_richpresence_lookup_list_t; +typedef struct __rc_format_enum_t { uint8_t value; } __rc_format_enum_t; + +#define RC_ALLOW_ALIGN(T) struct __align_ ## T { uint8_t ch; T t; }; + +RC_ALLOW_ALIGN(rc_condition_t) +RC_ALLOW_ALIGN(rc_condset_t) +RC_ALLOW_ALIGN(rc_modified_memref_t) +RC_ALLOW_ALIGN(rc_lboard_t) +RC_ALLOW_ALIGN(rc_lboard_with_memrefs_t) +RC_ALLOW_ALIGN(rc_memref_t) +RC_ALLOW_ALIGN(rc_memref_list_t) +RC_ALLOW_ALIGN(rc_memrefs_t) +RC_ALLOW_ALIGN(rc_modified_memref_list_t) +RC_ALLOW_ALIGN(rc_operand_t) +RC_ALLOW_ALIGN(rc_richpresence_t) +RC_ALLOW_ALIGN(rc_richpresence_display_t) +RC_ALLOW_ALIGN(rc_richpresence_display_part_t) +RC_ALLOW_ALIGN(rc_richpresence_lookup_t) +RC_ALLOW_ALIGN(rc_richpresence_lookup_item_t) +RC_ALLOW_ALIGN(rc_richpresence_with_memrefs_t) +RC_ALLOW_ALIGN(rc_scratch_string_t) +RC_ALLOW_ALIGN(rc_trigger_t) +RC_ALLOW_ALIGN(rc_trigger_with_memrefs_t) +RC_ALLOW_ALIGN(rc_value_t) +RC_ALLOW_ALIGN(rc_value_with_memrefs_t) +RC_ALLOW_ALIGN(char) + +#define RC_ALIGNOF(T) (sizeof(struct __align_ ## T) - sizeof(T)) +#define RC_OFFSETOF(o, t) (int)((uint8_t*)&(o.t) - (uint8_t*)&(o)) + +#define RC_ALLOC(t, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +#define RC_ALLOC_SCRATCH(t, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +#define RC_ALLOC_ARRAY(t, n, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, (n) * sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +#define RC_ALLOC_ARRAY_SCRATCH(t, n, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, (n) * sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) + +#define RC_ALLOC_WITH_TRAILING(container_type, trailing_type, trailing_field, trailing_count, parse) ((container_type*)rc_alloc(\ + (parse)->buffer, &(parse)->offset, \ + RC_OFFSETOF((*(container_type*)NULL),trailing_field) + trailing_count * sizeof(trailing_type), \ + RC_ALIGNOF(container_type), &(parse)->scratch, 0)) +#define RC_GET_TRAILING(container_pointer, container_type, trailing_type, trailing_field) (trailing_type*)(&((container_type*)(container_pointer))->trailing_field) + +/* force alignment to 4 bytes on 32-bit systems, or 8 bytes on 64-bit systems */ +#define RC_ALIGN(n) (((n) + (sizeof(void*)-1)) & ~(sizeof(void*)-1)) + +typedef struct { + rc_buffer_t buffer; + rc_scratch_string_t* strings; + + struct objs { + rc_condition_t* __rc_condition_t; + rc_condset_t* __rc_condset_t; + rc_modified_memref_t* __rc_modified_memref_t; + rc_lboard_t* __rc_lboard_t; + rc_lboard_with_memrefs_t* __rc_lboard_with_memrefs_t; + rc_memref_t* __rc_memref_t; + rc_memref_list_t* __rc_memref_list_t; + rc_memrefs_t* __rc_memrefs_t; + rc_modified_memref_list_t* __rc_modified_memref_list_t; + rc_operand_t* __rc_operand_t; + rc_richpresence_t* __rc_richpresence_t; + rc_richpresence_display_t* __rc_richpresence_display_t; + rc_richpresence_display_part_t* __rc_richpresence_display_part_t; + rc_richpresence_lookup_t* __rc_richpresence_lookup_t; + rc_richpresence_lookup_item_t* __rc_richpresence_lookup_item_t; + rc_richpresence_with_memrefs_t* __rc_richpresence_with_memrefs_t; + rc_scratch_string_t __rc_scratch_string_t; + rc_trigger_t* __rc_trigger_t; + rc_trigger_with_memrefs_t* __rc_trigger_with_memrefs_t; + rc_value_t* __rc_value_t; + rc_value_with_memrefs_t* __rc_value_with_memrefs_t; + + /* these fields aren't actually used by the code, but they force the + * virtual enum wrapper types to exist so natvis can use them */ + union { + __rc_bool_enum_t boolean; + __rc_memsize_enum_t memsize; + __rc_memsize_enum_func_t memsize_func; + __rc_operand_enum_t operand; + __rc_value_type_enum_t value_type; + __rc_memref_type_enum_t memref_type; + __rc_condition_enum_t condition; + __rc_condition_enum_str_t condition_str; + __rc_condset_list_t condset_list; + __rc_operator_enum_t oper; + __rc_operator_enum_str_t oper_str; + __rc_operand_memref_t operand_memref; + __rc_value_list_t value_list; + __rc_trigger_state_enum_t trigger_state; + __rc_lboard_state_enum_t lboard_state; + __rc_richpresence_display_list_t richpresence_display_list; + __rc_richpresence_display_part_list_t richpresence_display_part_list; + __rc_richpresence_lookup_list_t richpresence_lookup_list; + __rc_format_enum_t format; + } natvis_extension; + } objs; +} +rc_scratch_t; + +enum { + RC_VALUE_TYPE_NONE, + RC_VALUE_TYPE_UNSIGNED, + RC_VALUE_TYPE_SIGNED, + RC_VALUE_TYPE_FLOAT +}; + +typedef struct { + union { + uint32_t u32; + int32_t i32; + float f32; + } value; + + char type; +} +rc_typed_value_t; + +enum { + RC_MEMREF_TYPE_MEMREF, /* rc_memref_t */ + RC_MEMREF_TYPE_MODIFIED_MEMREF, /* rc_modified_memref_t */ + RC_MEMREF_TYPE_VALUE /* rc_value_t */ +}; + +#define RC_MEASURED_UNKNOWN 0xFFFFFFFF +#define RC_OPERAND_NONE 0xFF + +typedef struct { + /* memory accessors */ + rc_peek_t peek; + void* peek_userdata; + + /* processing state */ + rc_typed_value_t measured_value; /* captured Measured value */ + int32_t add_hits; /* AddHits/SubHits accumulator */ + uint8_t is_true; /* true if all conditions are true */ + uint8_t is_primed; /* true if all non-Trigger conditions are true */ + uint8_t is_paused; /* true if one or more PauseIf conditions is true */ + uint8_t can_measure; /* false if the measured value should be ignored */ + uint8_t measured_from_hits; /* true if the measured_value came from a condition's hit count */ + uint8_t and_next; /* true if the previous condition was AndNext true */ + uint8_t or_next; /* true if the previous condition was OrNext true */ + uint8_t reset_next; /* true if the previous condition was ResetNextIf true */ + uint8_t stop_processing; /* true to abort the processing loop */ + + /* result state */ + uint8_t has_hits; /* true if one of more hit counts is non-zero */ + uint8_t was_reset; /* true if one or more ResetIf conditions is true */ + uint8_t was_cond_reset; /* true if one or more ResetNextIf conditions is true */ + + /* control settings */ + uint8_t can_short_curcuit; /* allows logic processing to stop as soon as a false condition is encountered */ +} +rc_eval_state_t; + +typedef struct { + int32_t offset; + + void* buffer; + rc_scratch_t scratch; + + rc_memrefs_t* memrefs; + rc_memrefs_t* existing_memrefs; + rc_value_t** variables; + + uint32_t measured_target; + int lines_read; + + rc_operand_t addsource_parent; + rc_operand_t indirect_parent; + rc_operand_t remember; + uint8_t addsource_oper; + + uint8_t is_value; + uint8_t has_required_hits; + uint8_t measured_as_percent; + uint8_t ignore_non_parse_errors; +} +rc_parse_state_t; + +typedef struct rc_preparse_state_t { + rc_parse_state_t parse; + rc_memrefs_t memrefs; +} rc_preparse_state_t; + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer); +void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs); +void rc_reset_parse_state(rc_parse_state_t* parse, void* buffer); +void rc_destroy_parse_state(rc_parse_state_t* parse); +void rc_init_preparse_state(rc_preparse_state_t* preparse); +void rc_preparse_alloc_memrefs(rc_memrefs_t* memrefs, rc_preparse_state_t* preparse); +void rc_preparse_reserve_memrefs(rc_preparse_state_t* preparse, rc_memrefs_t* memrefs); +void rc_preparse_copy_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs); +void rc_destroy_preparse_state(rc_preparse_state_t *preparse); + +void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); +void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); +char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length); + +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size); +rc_modified_memref_t* rc_alloc_modified_memref(rc_parse_state_t* parse, uint8_t size, const rc_operand_t* parent, + uint8_t modifier_type, const rc_operand_t* modifier); +int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address); +void rc_update_memref_values(rc_memrefs_t* memrefs, rc_peek_t peek, void* ud); +void rc_update_memref_value(rc_memref_value_t* memref, uint32_t value); +void rc_get_memref_value(rc_typed_value_t* value, rc_memref_t* memref, int operand_type); +uint32_t rc_get_modified_memref_value(const rc_modified_memref_t* memref, rc_peek_t peek, void* ud); +uint8_t rc_memref_shared_size(uint8_t size); +uint32_t rc_memref_mask(uint8_t size); +void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size); +uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud); + +void rc_memrefs_init(rc_memrefs_t* memrefs); +void rc_memrefs_destroy(rc_memrefs_t* memrefs); +uint32_t rc_memrefs_count_memrefs(const rc_memrefs_t* memrefs); +uint32_t rc_memrefs_count_modified_memrefs(const rc_memrefs_t* memrefs); + +void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); +int rc_trigger_state_active(int state); +rc_memrefs_t* rc_trigger_get_memrefs(rc_trigger_t* self); + +typedef struct rc_condset_with_trailing_conditions_t { + rc_condset_t condset; + rc_condition_t conditions[2]; +} rc_condset_with_trailing_conditions_t; +RC_ALLOW_ALIGN(rc_condset_with_trailing_conditions_t) + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse); +int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state); +void rc_reset_condset(rc_condset_t* self); +rc_condition_t* rc_condset_get_conditions(rc_condset_t* self); +void rc_test_condset_internal(rc_condition_t* condition, uint32_t num_conditions, rc_eval_state_t* eval_state, int can_short_circuit); + +enum { + RC_PROCESSING_COMPARE_DEFAULT = 0, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_ALWAYS_TRUE, + RC_PROCESSING_COMPARE_ALWAYS_FALSE +}; + +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse); +void rc_parse_condition_internal(rc_condition_t* self, const char** memaddr, rc_parse_state_t* parse); +void rc_condition_update_parse_state(rc_condition_t* condition, rc_parse_state_t* parse); +int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); +void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state); +int rc_condition_is_combining(const rc_condition_t* self); +void rc_condition_convert_to_operand(const rc_condition_t* condition, rc_operand_t* operand, rc_parse_state_t* parse); +const rc_operand_t* rc_condition_get_real_operand1(const rc_condition_t* self); + +int rc_parse_operand(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse); +void rc_evaluate_operand(rc_typed_value_t* value, const rc_operand_t* self, rc_eval_state_t* eval_state); +int rc_operator_is_modifying(int oper); +int rc_memsize_is_float(uint8_t size); +int rc_operand_is_float_memref(const rc_operand_t* self); +int rc_operand_is_float(const rc_operand_t* self); +int rc_operand_is_recall(const rc_operand_t* self); +int rc_operand_type_is_memref(uint8_t type); +int rc_operand_type_is_transform(uint8_t type); +int rc_operands_are_equal(const rc_operand_t* left, const rc_operand_t* right); +void rc_operand_addsource(rc_operand_t* self, rc_parse_state_t* parse, uint8_t new_size); +void rc_operand_set_const(rc_operand_t* self, uint32_t value); +void rc_operand_set_float_const(rc_operand_t* self, double value); + +int rc_is_valid_variable_character(char ch, int is_first); +void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud); +void rc_reset_value(rc_value_t* self); +int rc_value_from_hits(rc_value_t* self); +rc_value_t* rc_alloc_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse); +uint32_t rc_count_values(const rc_value_t* values); +void rc_update_values(rc_value_t* values, rc_peek_t peek, void* ud); +void rc_reset_values(rc_value_t* values); + +void rc_typed_value_convert(rc_typed_value_t* value, char new_type); +void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_modulus(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_negate(rc_typed_value_t* value); +int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper); +void rc_typed_value_combine(rc_typed_value_t* value, rc_typed_value_t* amount, uint8_t oper); +void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); + +int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format); + +void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_state_t* parse); +int rc_lboard_state_active(int state); + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse); +rc_memrefs_t* rc_richpresence_get_memrefs(rc_richpresence_t* self); +void rc_reset_richpresence_triggers(rc_richpresence_t* self); +void rc_update_richpresence_internal(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud); + +int rc_validate_memrefs_for_console(const rc_memrefs_t* memrefs, char result[], const size_t result_size, uint32_t console_id); + +RC_END_C_DECLS + +#endif /* RC_INTERNAL_H */ diff --git a/src/rcheevos/src/rcheevos/rc_runtime_types.natvis b/src/rcheevos/src/rcheevos/rc_runtime_types.natvis new file mode 100644 index 0000000000..25529c5525 --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_runtime_types.natvis @@ -0,0 +1,541 @@ + + + + + + {value.u32} (RC_VALUE_TYPE_UNSIGNED) + {value.f32} (RC_VALUE_TYPE_FLOAT) + {value.i32} (RC_VALUE_TYPE_SIGNED) + none (RC_VALUE_TYPE_NONE) + {value.i32} (unknown) + + + false + true + true ({value}) + + + {RC_MEMSIZE_8_BITS} + {RC_MEMSIZE_16_BITS} + {RC_MEMSIZE_24_BITS} + {RC_MEMSIZE_32_BITS} + {RC_MEMSIZE_LOW} + {RC_MEMSIZE_HIGH} + {RC_MEMSIZE_BIT_0} + {RC_MEMSIZE_BIT_1} + {RC_MEMSIZE_BIT_2} + {RC_MEMSIZE_BIT_3} + {RC_MEMSIZE_BIT_4} + {RC_MEMSIZE_BIT_5} + {RC_MEMSIZE_BIT_6} + {RC_MEMSIZE_BIT_7} + {RC_MEMSIZE_BITCOUNT} + {RC_MEMSIZE_16_BITS_BE} + {RC_MEMSIZE_24_BITS_BE} + {RC_MEMSIZE_32_BITS_BE} + {RC_MEMSIZE_FLOAT} + {RC_MEMSIZE_MBF32} + {RC_MEMSIZE_MBF32_LE} + {RC_MEMSIZE_FLOAT_BE} + {RC_MEMSIZE_DOUBLE32} + {RC_MEMSIZE_DOUBLE32_BE} + {RC_MEMSIZE_VARIABLE} + unknown ({value}) + + + {RC_VALUE_TYPE_NONE} + {RC_VALUE_TYPE_UNSIGNED} + {RC_VALUE_TYPE_SIGNED} + {RC_VALUE_TYPE_FLOAT} + unknown ({value}) + + + {RC_MEMREF_TYPE_MEMREF} + {RC_MEMREF_TYPE_MODIFIED_MEMREF} + RC_MEMREF_TYPE_VALUE + unknown ({value}) + + + byte + word + tbyte + dword + lower4 + upper4 + bit0 + bit1 + bit2 + bit3 + bit4 + bit5 + bit6 + bit7 + bitcount + word_be + tbyte_be + dword_be + float + mbf32 + mbf32_le + float_be + double32 + double32_be + var + unknown + + + {{value={(float)*((float*)&value)} prior={(float)*((float*)&prior)}}} + {{value={(int)value} prior={(int)prior}}} + {{value={value,x} prior={prior,x}}} + + value + prior + *((__rc_memsize_enum_t*)&size) + *((__rc_bool_enum_t*)&changed) + *((__rc_value_type_enum_t*)&type) + *((__rc_memref_type_enum_t*)&memref_type) + + + + {*(rc_modified_memref_t*)&value} + {*(rc_value_t*)&value} + var + {*((__rc_memsize_enum_func_t*)&value.size)}({address,x}) + + (rc_modified_memref_t*)&value + value + address + + + + {{count = {count}}} + + next + count + capacity + + count + items + + + + + ... {*((__rc_memsize_enum_func_t*)&operand.size)} {((rc_modified_memref_t*)operand.value.memref)->parent,na} + value {((rc_value_t*)operand.value.memref)->name,s} + {*((__rc_memsize_enum_func_t*)&operand.size)} {operand.value.memref->address,x} + + (rc_modified_memref_t*)&operand.value + operand.value.memref->value + operand.value.memref->address + + + + {RC_OPERAND_ADDRESS} + {RC_OPERAND_DELTA} + {RC_OPERAND_CONST} + {RC_OPERAND_FP} + {RC_OPERAND_FUNC} + {RC_OPERAND_PRIOR} + {RC_OPERAND_BCD} + {RC_OPERAND_INVERTED} + {RC_OPERAND_RECALL} + RC_OPERAND_NONE (255) + unknown ({value}) + + + {{{*(__rc_operand_memref_t*)&value.memref}}} + {{delta {*(__rc_operand_memref_t*)&value.memref}}} + {{prior {*(__rc_operand_memref_t*)&value.memref}}} + {{bcd {*(__rc_operand_memref_t*)&value.memref}}} + {{inverted {*(__rc_operand_memref_t*)&value.memref}}} + {{value {(int)value.num}}} + {{value {value.num,x}}} + {{value {value.num}}} + {{value {value.dbl}}} + {{recall}} + {{func @{value}}} + {{none}} + {{unknown}} + + value.num + value.dbl + value.num + value.dbl + value.memref + value.memref + (rc_modified_memref_t*)value.memref + *((__rc_operand_enum_t*)&type) + *((__rc_memsize_enum_t*)&size) + *((__rc_operand_enum_t*)&memref_access_type) + + + + {RC_CONDITION_STANDARD} + {RC_CONDITION_PAUSE_IF} + {RC_CONDITION_RESET_IF} + {RC_CONDITION_MEASURED_IF} + {RC_CONDITION_TRIGGER} + {RC_CONDITION_MEASURED} + {RC_CONDITION_ADD_SOURCE} + {RC_CONDITION_SUB_SOURCE} + {RC_CONDITION_ADD_ADDRESS} + {RC_CONDITION_REMEMBER} + {RC_CONDITION_ADD_HITS} + {RC_CONDITION_SUB_HITS} + {RC_CONDITION_RESET_NEXT_IF} + {RC_CONDITION_AND_NEXT} + {RC_CONDITION_OR_NEXT} + unknown ({value}) + + + + PauseIf + ResetIf + MeasuredIf + Trigger + Measured + AddSource + SubSource + AddAddress + Remember + AddHits + SubHits + ResetNextIf + AndNext + OrNext + {value} + + + {RC_OPERATOR_EQ} + {RC_OPERATOR_LT} + {RC_OPERATOR_LE} + {RC_OPERATOR_GT} + {RC_OPERATOR_GE} + {RC_OPERATOR_NE} + {RC_OPERATOR_NONE} + {RC_OPERATOR_MULT} + {RC_OPERATOR_DIV} + {RC_OPERATOR_AND} + {RC_OPERATOR_XOR} + {RC_OPERATOR_MOD} + {RC_OPERATOR_ADD} + {RC_OPERATOR_SUB} + {RC_OPERATOR_SUB_PARENT} + {RC_OPERATOR_INDIRECT_READ} + unknown ({value}) + + + == + < + <= + > + >= + != + + * + / + & + ^ + % + + + - + subtracted from + $ + unknown ({value}) + + + {*((__rc_condition_enum_str_t*)&type)} {operand1} + {*((__rc_condition_enum_str_t*)&type)} {operand1} ({required_hits}) + {*((__rc_condition_enum_str_t*)&type)} {operand1} {*((__rc_operator_enum_str_t*)&oper)} {operand2} + {*((__rc_condition_enum_str_t*)&type)} {operand1} {*((__rc_operator_enum_str_t*)&oper)} {operand2} ({required_hits}) + + *((__rc_condition_enum_t*)&type) + operand1 + *((__rc_operator_enum_t*)&oper) + operand2 + required_hits + current_hits + next + + + + {{count={num_pause_conditions+num_reset_conditions+num_hittarget_conditions+num_measured_conditions+num_other_conditions+num_indirect_conditions}}} + + + conditions + next + this + + + + + {{}} + + + first_condset + next + this + + + + + $({parent} + {modifier}) + ({modifier} - {parent}) + ({parent} {*((__rc_operator_enum_str_t*)&modifier_type)} {modifier}) + + memref.value + parent + *((__rc_operator_enum_t*)&modifier_type) + modifier + + + + {{count = {count}}} + + next + count + capacity + + count + items + + + + + {RC_TRIGGER_STATE_INACTIVE} + {RC_TRIGGER_STATE_WAITING} + {RC_TRIGGER_STATE_ACTIVE} + {RC_TRIGGER_STATE_PAUSED} + {RC_TRIGGER_STATE_RESET} + {RC_TRIGGER_STATE_TRIGGERED} + {RC_TRIGGER_STATE_PRIMED} + {RC_TRIGGER_STATE_DISABLED} + unknown ({value}) + + + + *((__rc_trigger_state_enum_t*)&state) + *((__rc_bool_enum_t*)&has_hits) + *((__rc_bool_enum_t*)&measured_as_percent) + requirement + *((__rc_condset_list_t*)&alternative) + + + + {value} {name,s} + + value + conditions + name + + + + {{count = {count}}} + + count + capacity + + count + items + + + + + {RC_LBOARD_STATE_INACTIVE} + {RC_LBOARD_STATE_WAITING} + {RC_LBOARD_STATE_ACTIVE} + {RC_LBOARD_STATE_STARTED} + {RC_LBOARD_STATE_CANCELED} + {RC_LBOARD_STATE_TRIGGERED} + {RC_LBOARD_STATE_DISABLED} + unknown ({value}) + + + + *((__rc_lboard_state_enum_t*)&state) + start + submit + cancel + value + + + + {RC_FORMAT_FRAMES} + {RC_FORMAT_SECONDS} + {RC_FORMAT_CENTISECS} + {RC_FORMAT_SCORE} + {RC_FORMAT_VALUE} + {RC_FORMAT_MINUTES} + {RC_FORMAT_SECONDS_AS_MINUTES} + {RC_FORMAT_FLOAT1} + {RC_FORMAT_FLOAT2} + {RC_FORMAT_FLOAT3} + {RC_FORMAT_FLOAT4} + {RC_FORMAT_FLOAT5} + {RC_FORMAT_FLOAT6} + {RC_FORMAT_FIXED1} + {RC_FORMAT_FIXED2} + {RC_FORMAT_FIXED3} + {RC_FORMAT_TENS} + {RC_FORMAT_HUNDREDS} + {RC_FORMAT_THOUSANDS} + {RC_FORMAT_UNSIGNED_VALUE} + {RC_FORMAT_UNFORMATTED} + RC_FORMAT_STRING (101) + RC_FORMAT_LOOKUP (102) + RC_FORMAT_UNKNOWN_MACRO (103) + RC_FORMAT_ASCIICHAR (104) + RC_FORMAT_UNICODECHAR (105) + unknown ({value}) + + + {text,s} + [Unknown macro]{text,sb} + @{text,sb}({value,na}) + @{lookup->name,sb}({value,na}) + + text + lookup + value + *((__rc_format_enum_t*)&display_type) + + + + {display,na} {display->next,na} {display->next->next,na} + {display,na} {display->next,na} + {display,na} + + + display + next + this + + + + + + *((__rc_richpresence_display_part_list_t*)&display) + trigger + + + + {{NULL}} + {(void*)&first_display,na} + + + first_display + next + this + + + + + {first}: {label,na} + {first}-{last}: {label,na} + + + {name,na} + + name + *((__rc_format_enum_t*)&format) + default_label + + root + left + right + this + + + + + {{NULL}} + {(void*)&first_lookup,na} + + + first_lookup + next + this + + + + + + ((__rc_richpresence_display_list_t*)&first_display) + ((__rc_richpresence_lookup_list_t*)&first_lookup) + + + + {{NULL}} + {(void*)&first_value,na} + + + first_value + next + this + + + + + {{offset={offset} addsource_parent={addsource_parent} indirect_parent={indirect_parent}}} + + offset + memrefs + existing_memrefs + variables + ((__rc_value_list_t*)&variables) + addsource_parent + *((__rc_operator_enum_t*)&addsource_oper) + indirect_parent + remember + *((__rc_bool_enum_t*)&is_value) + *((__rc_bool_enum_t*)&has_required_hits) + *((__rc_bool_enum_t*)&measured_as_percent) + + + + {{used={write-start} size={end-start}}} + + end-start + write-start + end-write + next + + + + + + + &chunk + next + this + + + + + {{count={runtime.trigger_count}}} + + + runtime.trigger_count + runtime.triggers[$i] + + + + + {{count={runtime.lboard_count}}} + + + runtime.lboard_count + runtime.lboards[$i] + + + + + {{trigger_count={trigger_count} lboard_count={lboard_count}}} + + *((__rc_runtime_trigger_list_t*)this) + *((__rc_runtime_lboard_list_t*)this) + richpresence + memrefs + + + diff --git a/src/rcheevos/src/rcheevos/rc_validate.c b/src/rcheevos/src/rcheevos/rc_validate.c new file mode 100644 index 0000000000..8e05a9227d --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_validate.c @@ -0,0 +1,1370 @@ +#include "rc_validate.h" + +#include "rc_consoles.h" +#include "rc_internal.h" + +#include "../rc_compat.h" + +#include +#include + +enum +{ + RC_VALIDATION_ERR_NONE, + + /* sorted by severity - most severe first */ + + /* errors that prevent the achievement from functioning */ + RC_VALIDATION_ERR_ADDRESS_OUT_OF_RANGE, + RC_VALIDATION_ERR_RECALL_BEFORE_REMEMBER, + RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_WITH_MAX, + RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_INTEGER_TO_FLOAT, + RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE, + RC_VALIDATION_ERR_CONFLICTING_CONDITION, + + /* warnings about logic that does nothing */ + RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_INTEGER_TO_FLOAT, + RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_WITH_MAX, + RC_VALIDATION_ERR_TRAILING_CHAINING_CONDITION, + RC_VALIDATION_ERR_MEASUREDIF_WITHOUT_MEASURED, + RC_VALIDATION_ERR_ADDHITS_WITHOUT_TARGET, + + /* warnings about pointer math */ + RC_VALIDATION_ERR_POINTER_FROM_PREVIOUS_FRAME, + RC_VALIDATION_ERR_POINTER_NON_INTEGER_OFFSET, + RC_VALIDATION_ERR_POINTER_TRANSFORMED_OFFSET, + + /* warnings about potential logic errors */ + RC_VALIDATION_ERR_COMPARING_DIFFERENT_MEMORY_SIZES, + RC_VALIDATION_ERR_MASK_RESULT_ALWAYS_ZERO, + + /* warnings that some areas of memory should be avoided */ + RC_VALIDATION_ERR_KERNAL_RAM_REQUIRES_BIOS, + RC_VALIDATION_ERR_VIRTUAL_RAM_MAY_NOT_BE_EXPOSED, + + /* warnings about redundant logic */ + RC_VALIDATION_ERR_REDUNDANT_CONDITION, + RC_VALIDATION_ERR_NO_HITS_TO_RESET, + RC_VALIDATION_ERR_RESET_HIT_TARGET_OF_ONE, + RC_VALIDATION_ERR_MASK_TOO_LARGE, + + RC_VALIDATION_ERR_COUNT +}; + +enum +{ + RC_VALIDATION_VIRTUAL_RAM_OTHER, + RC_VALIDATION_VIRTUAL_RAM_MIRROR, + RC_VALIDATION_VIRTUAL_RAM_ECHO +}; + +typedef struct rc_validation_error_t +{ + uint32_t err; + uint16_t group_index; + uint16_t cond_index; + uint32_t data1; + uint32_t data2; +} rc_validation_error_t; + +typedef struct rc_validation_state_t +{ + rc_validation_error_t errors[64]; + uint32_t error_count; + uint16_t group_index; + uint16_t cond_index; + uint32_t console_id; + uint32_t max_address; + uint8_t has_alt_groups; + uint8_t has_hit_targets; +} rc_validation_state_t; + +/* this returns a negative value if err1 is more severe than err2, or + * a positive value is err2 is more severe than err1. */ +static int rc_validation_compare_severity(const rc_validation_error_t* err1, const rc_validation_error_t* err2) +{ + /* lower err value is more severe */ + int diff = (err1->err - err2->err); + if (diff != 0) + return diff; + + /* lower group index is more severe */ + diff = (err1->group_index - err2->group_index); + if (diff != 0) + return diff; + + /* lower condition value is more severe */ + return (err1->cond_index - err2->cond_index); +} + +static const rc_validation_error_t* rc_validate_find_most_severe_error(const rc_validation_state_t* state) +{ + const rc_validation_error_t* error = &state->errors[0]; + const rc_validation_error_t* most_severe_error = error; + const rc_validation_error_t* stop = &state->errors[state->error_count]; + + while (++error < stop) { + if (rc_validation_compare_severity(error, most_severe_error) < 0) + most_severe_error = error; + } + + return most_severe_error; +} + +static rc_validation_error_t* rc_validate_find_least_severe_error(rc_validation_state_t* state) +{ + rc_validation_error_t* error = &state->errors[0]; + rc_validation_error_t* least_severe_error = error; + rc_validation_error_t* stop = &state->errors[state->error_count]; + + while (++error < stop) { + if (rc_validation_compare_severity(error, least_severe_error) > 0) + least_severe_error = error; + } + + return least_severe_error; +} + +static size_t rc_validate_format_cond_index(char buffer[], size_t buffer_size, const rc_validation_state_t* state, uint32_t group_index, uint32_t cond_index) +{ + int written = 0; + + if (cond_index == 0) + return 0; + + if (state->has_alt_groups) { + if (group_index == 0) + written = snprintf(buffer, buffer_size, "Core "); + else + written = snprintf(buffer, buffer_size, "Alt%u ", group_index); + + buffer += written; + buffer_size -= written; + } + + written += snprintf(buffer, buffer_size, "Condition %u", cond_index); + return written; +} + +static const char* rc_validate_get_condition_type_string(uint32_t type) +{ + switch (type) { + case RC_CONDITION_ADD_ADDRESS: return "AddAddress"; + case RC_CONDITION_ADD_HITS: return "AddHits"; + case RC_CONDITION_ADD_SOURCE: return "AddSource"; + case RC_CONDITION_AND_NEXT: return "AndNext"; + case RC_CONDITION_MEASURED: return "Measured"; + case RC_CONDITION_MEASURED_IF: return "MeasuredIf"; + case RC_CONDITION_OR_NEXT: return "OrNext"; + case RC_CONDITION_PAUSE_IF: return "PauseIf"; + case RC_CONDITION_REMEMBER: return "Remember"; + case RC_CONDITION_RESET_IF: return "ResetIf"; + case RC_CONDITION_RESET_NEXT_IF: return "ResetNextIf"; + case RC_CONDITION_SUB_HITS: return "SubHits"; + case RC_CONDITION_SUB_SOURCE: return "SubSource"; + case RC_CONDITION_TRIGGER: return "Trigger"; + default: return "???"; + } +} + +static void rc_validate_format_error_compare_int_to_float(char buffer[], size_t buffer_size, uint32_t encoded_data, const char* limiter) +{ + int written; + rc_typed_value_t value; + value.value.u32 = encoded_data; + + written = snprintf(buffer, buffer_size, "Comparison is %s true (integer can never be %.06f", limiter, value.value.f32); + while (buffer[written - 1] == '0') + written--; + if (buffer[written] == '.') + written++; + buffer[written] = ')'; + buffer[written + 1] = '\0'; +} + +static int rc_validate_format_error(char buffer[], size_t buffer_size, const rc_validation_state_t* state, const rc_validation_error_t* error) +{ + size_t written = rc_validate_format_cond_index(buffer, buffer_size, state, error->group_index, error->cond_index); + buffer += written; + buffer_size -= written; + + if (buffer_size < 2) + return 0; + + buffer_size -= 2; + *buffer++ = ':'; + *buffer++ = ' '; + + switch (error->err) { + case RC_VALIDATION_ERR_ADDHITS_WITHOUT_TARGET: + snprintf(buffer, buffer_size, "Final condition in AddHits chain must have a hit target"); + break; + + case RC_VALIDATION_ERR_ADDRESS_OUT_OF_RANGE: + snprintf(buffer, buffer_size, "Address %04X out of range (max %04X)", error->data1, error->data2); + break; + + case RC_VALIDATION_ERR_COMPARING_DIFFERENT_MEMORY_SIZES: + snprintf(buffer, buffer_size, "Comparing different memory sizes"); + break; + + case RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_INTEGER_TO_FLOAT: + rc_validate_format_error_compare_int_to_float(buffer, buffer_size, error->data1, "always"); + break; + + case RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_WITH_MAX: + snprintf(buffer, buffer_size, "Comparison is always true (max %u)", error->data1); + break; + + case RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE: + snprintf(buffer, buffer_size, "Comparison is never true"); + break; + + case RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_INTEGER_TO_FLOAT: + rc_validate_format_error_compare_int_to_float(buffer, buffer_size, error->data1, "never"); + break; + + case RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_WITH_MAX: + snprintf(buffer, buffer_size, "Comparison is never true (max %u)", error->data1); + break; + + case RC_VALIDATION_ERR_CONFLICTING_CONDITION: + written = snprintf(buffer, buffer_size, "Conflicts with "); + buffer += written; + buffer_size -= written; + rc_validate_format_cond_index(buffer, buffer_size, state, error->data1, error->data2); + break; + + case RC_VALIDATION_ERR_KERNAL_RAM_REQUIRES_BIOS: + snprintf(buffer, buffer_size, "Kernel RAM may not be initialized without real BIOS (address %04X)", error->data1); + break; + + case RC_VALIDATION_ERR_MASK_RESULT_ALWAYS_ZERO: + snprintf(buffer, buffer_size, "Result of mask is always 0"); + break; + + case RC_VALIDATION_ERR_MASK_TOO_LARGE: + snprintf(buffer, buffer_size, "Mask has more bits than source"); + break; + + case RC_VALIDATION_ERR_MEASUREDIF_WITHOUT_MEASURED: + snprintf(buffer, buffer_size, "MeasuredIf without Measured"); + break; + + case RC_VALIDATION_ERR_NO_HITS_TO_RESET: + snprintf(buffer, buffer_size, "No captured hits to reset"); + break; + + case RC_VALIDATION_ERR_POINTER_FROM_PREVIOUS_FRAME: + snprintf(buffer, buffer_size, "Using pointer from previous frame"); + break; + + case RC_VALIDATION_ERR_POINTER_NON_INTEGER_OFFSET: + snprintf(buffer, buffer_size, "Using non-integer value in AddAddress calculation"); + break; + + case RC_VALIDATION_ERR_POINTER_TRANSFORMED_OFFSET: + snprintf(buffer, buffer_size, "Using transformed value in AddAddress calculation"); + break; + + case RC_VALIDATION_ERR_RECALL_BEFORE_REMEMBER: + snprintf(buffer, buffer_size, "Recall used before Remember"); + break; + + case RC_VALIDATION_ERR_RESET_HIT_TARGET_OF_ONE: + snprintf(buffer, buffer_size, "Hit target of 1 is redundant on ResetIf"); + break; + + case RC_VALIDATION_ERR_REDUNDANT_CONDITION: + written = snprintf(buffer, buffer_size, "Redundant with "); + buffer += written; + buffer_size -= written; + rc_validate_format_cond_index(buffer, buffer_size, state, error->data1, error->data2); + break; + + case RC_VALIDATION_ERR_TRAILING_CHAINING_CONDITION: + snprintf(buffer, buffer_size, "%s condition type expects another condition to follow", rc_validate_get_condition_type_string(error->data1)); + break; + + case RC_VALIDATION_ERR_VIRTUAL_RAM_MAY_NOT_BE_EXPOSED: + snprintf(buffer, buffer_size, "%s RAM may not be exposed by emulator (address %04X)", + error->data2 == RC_VALIDATION_VIRTUAL_RAM_MIRROR ? "Mirror" : + error->data2 == RC_VALIDATION_VIRTUAL_RAM_ECHO ? "Echo" : "Virtual", + error->data1); + break; + } + + return 0; +} + +static void rc_validate_add_error(rc_validation_state_t* state, uint32_t error_code, uint32_t data1, uint32_t data2) +{ + rc_validation_error_t* error; + if (state->error_count == sizeof(state->errors) / sizeof(state->errors[0])) + error = rc_validate_find_least_severe_error(state); + else + error = &state->errors[state->error_count++]; + + error->err = error_code; + error->group_index = state->group_index; + error->cond_index = state->cond_index; + error->data1 = data1; + error->data2 = data2; +} + +static void rc_validate_memref(const rc_memref_t* memref, rc_validation_state_t* state) +{ + if (memref->address > state->max_address) { + rc_validate_add_error(state, RC_VALIDATION_ERR_ADDRESS_OUT_OF_RANGE, memref->address, state->max_address); + return; + } + + switch (state->console_id) { + case RC_CONSOLE_NINTENDO: + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + if (memref->address >= 0x0800 && memref->address <= 0x1FFF) + rc_validate_add_error(state, RC_VALIDATION_ERR_VIRTUAL_RAM_MAY_NOT_BE_EXPOSED, memref->address, RC_VALIDATION_VIRTUAL_RAM_MIRROR); + break; + + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_COLOR: + if (memref->address >= 0xE000 && memref->address <= 0xFDFF) + rc_validate_add_error(state, RC_VALIDATION_ERR_VIRTUAL_RAM_MAY_NOT_BE_EXPOSED, memref->address, RC_VALIDATION_VIRTUAL_RAM_ECHO); + break; + + case RC_CONSOLE_PLAYSTATION: + if (memref->address <= 0xFFFF) + rc_validate_add_error(state, RC_VALIDATION_ERR_KERNAL_RAM_REQUIRES_BIOS, memref->address, 0); + break; + } +} + +static uint32_t rc_console_max_address(uint32_t console_id) +{ + const rc_memory_regions_t* memory_regions; + memory_regions = rc_console_memory_regions(console_id); + if (memory_regions && memory_regions->num_regions > 0) + return memory_regions->region[memory_regions->num_regions - 1].end_address; + + return 0xFFFFFFFF; +} + +int rc_validate_memrefs_for_console(const rc_memrefs_t* memrefs, char result[], const size_t result_size, uint32_t console_id) +{ + const rc_memref_list_t* memref_list = &memrefs->memrefs; + + rc_validation_state_t state; + memset(&state, 0, sizeof(state)); + state.console_id = console_id; + state.max_address = rc_console_max_address(console_id); + + result[0] = '\0'; + do + { + const rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + for (; memref < memref_stop; ++memref) + { + rc_validate_memref(memref, &state); + if (state.error_count) { + rc_validate_format_error(result, result_size, &state, &state.errors[0]); + return 0; + } + } + + memref_list = memref_list->next; + } while (memref_list); + + return 1; +} + +static uint32_t rc_max_value(const rc_operand_t* operand) +{ + if (operand->type == RC_OPERAND_CONST) + return operand->value.num; + + if (!rc_operand_is_memref(operand)) + return 0xFFFFFFFF; + + switch (operand->size) { + case RC_MEMSIZE_BIT_0: + case RC_MEMSIZE_BIT_1: + case RC_MEMSIZE_BIT_2: + case RC_MEMSIZE_BIT_3: + case RC_MEMSIZE_BIT_4: + case RC_MEMSIZE_BIT_5: + case RC_MEMSIZE_BIT_6: + case RC_MEMSIZE_BIT_7: + return 1; + + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + return 0xF; + + case RC_MEMSIZE_BITCOUNT: + return 8; + + case RC_MEMSIZE_8_BITS: + /* NOTE: BCD should max out at 0x99, but because each digit can be 15, it actually maxes at 15*10 + 15 */ + return (operand->type == RC_OPERAND_BCD) ? 165 : 0xFF; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 16665 : 0xFFFF; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 1666665 : 0xFFFFFF; + + default: + return (operand->type == RC_OPERAND_BCD) ? 166666665 : 0xFFFFFFFF; + } +} + +static void rc_combine_ranges(uint32_t* min_val, uint32_t* max_val, uint8_t oper, uint32_t oper_min_val, uint32_t oper_max_val) +{ + switch (oper) { + case RC_OPERATOR_MULT: + { + unsigned long long scaled = ((unsigned long long)*min_val) * oper_min_val; + *min_val = (scaled > 0xFFFFFFFF) ? 0xFFFFFFFF : (uint32_t)scaled; + + scaled = ((unsigned long long)*max_val) * oper_max_val; + *max_val = (scaled > 0xFFFFFFFF) ? 0xFFFFFFFF : (uint32_t)scaled; + break; + } + + case RC_OPERATOR_DIV: + *min_val = (oper_max_val == 0) ? *min_val : (*min_val / oper_max_val); + *max_val = (oper_min_val == 0) ? *max_val : (*max_val / oper_min_val); + break; + + case RC_OPERATOR_AND: + *min_val = 0; + *max_val &= oper_max_val; + break; + + case RC_OPERATOR_XOR: + *min_val = 0; + *max_val |= oper_max_val; + break; + + case RC_OPERATOR_MOD: + *min_val = 0; + *max_val = (*max_val >= oper_max_val) ? oper_max_val - 1 : *max_val; + break; + + case RC_OPERATOR_ADD: + if (*min_val > *max_val) { /* underflow occurred */ + *max_val += oper_max_val; + } + else { + unsigned long long scaled = ((unsigned long long)*max_val) + oper_max_val; + *max_val = (scaled > 0xFFFFFFFF) ? 0xFFFFFFFF : (uint32_t)scaled; + } + + *min_val += oper_min_val; + break; + + case RC_OPERATOR_SUB: + *min_val -= oper_max_val; + *max_val -= oper_min_val; + break; + + case RC_OPERATOR_SUB_PARENT: + { + uint32_t temp = oper_min_val - *max_val; + *max_val = oper_max_val - *min_val; + *min_val = temp; + break; + } + + default: + break; + } +} + +static void rc_chain_get_value_range(const rc_operand_t* operand, uint32_t* min_val, uint32_t* max_val) +{ + if (rc_operand_is_memref(operand) && operand->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + const rc_modified_memref_t* modified_memref = (const rc_modified_memref_t*)operand->value.memref; + if (modified_memref->modifier_type != RC_OPERATOR_INDIRECT_READ) { + if (modified_memref->modifier_type == RC_OPERATOR_DIV && + rc_operand_is_memref(&modified_memref->modifier) && + rc_operands_are_equal(&modified_memref->modifier, &modified_memref->parent)) { + /* division by self can only return 0 or 1. */ + *min_val = 0; + *max_val = 1; + } + else { + uint32_t modifier_min_val, modifier_max_val; + rc_chain_get_value_range(&modified_memref->parent, min_val, max_val); + rc_chain_get_value_range(&modified_memref->modifier, &modifier_min_val, &modifier_max_val); + rc_combine_ranges(min_val, max_val, modified_memref->modifier_type, modifier_min_val, modifier_max_val); + } + return; + } + } + + *min_val = (operand->type == RC_OPERAND_CONST) ? operand->value.num : 0; + *max_val = rc_max_value(operand); +} + +static int rc_validate_get_condition_index(const rc_condset_t* condset, const rc_condition_t* condition) +{ + int index = 1; + const rc_condition_t* scan; + for (scan = condset->conditions; scan != NULL; scan = scan->next) + { + if (scan == condition) + return index; + + ++index; + } + + return 0; +} + +static void rc_validate_range(uint32_t min_val, uint32_t max_val, char oper, uint32_t max, rc_validation_state_t* state) +{ + switch (oper) { + case RC_OPERATOR_AND: + if (min_val > max) + rc_validate_add_error(state, RC_VALIDATION_ERR_MASK_TOO_LARGE, 0, 0); + else if (min_val == 0 && max_val == 0) + rc_validate_add_error(state, RC_VALIDATION_ERR_MASK_RESULT_ALWAYS_ZERO, 0, 0); + break; + + case RC_OPERATOR_EQ: + if (min_val > max) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_WITH_MAX, max, 0); + break; + + case RC_OPERATOR_NE: + if (min_val > max) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_WITH_MAX, max, 0); + break; + + case RC_OPERATOR_GE: + if (min_val > max) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_WITH_MAX, max, 0); + else if (max_val == 0) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_WITH_MAX, max, 0); + break; + + case RC_OPERATOR_GT: + if (min_val >= max) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_WITH_MAX, max, 0); + break; + + case RC_OPERATOR_LE: + if (min_val >= max) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_WITH_MAX, max, 0); + break; + + case RC_OPERATOR_LT: + if (min_val > max) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_WITH_MAX, max, 0); + else if (max_val == 0) + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE, 0, 0); + break; + } +} + +static int rc_validate_condset_internal(const rc_condset_t* condset, rc_validation_state_t* state) +{ + const rc_condition_t* cond; + int in_add_hits = 0; + int in_add_address = 0; + int is_combining = 0; + int has_measured = 0; + int measuredif_index = -1; + uint32_t errors_before = state->error_count; + + if (!condset) + return 1; + + state->cond_index = 1; + + for (cond = condset->conditions; cond; cond = cond->next, ++state->cond_index) { + /* validate the original operands first */ + const rc_operand_t* operand1 = rc_condition_get_real_operand1(cond); + int is_memref1 = rc_operand_is_memref(operand1); + const int is_memref2 = rc_operand_is_memref(&cond->operand2); + + if (!in_add_address) { + if (is_memref1) + rc_validate_memref(operand1->value.memref, state); + if (is_memref2) + rc_validate_memref(cond->operand2.value.memref, state); + } + else { + in_add_address = 0; + } + + /* if operand is {recall}, but memref is null, that means the Remember wasn't found */ + if (rc_operand_is_recall(operand1) && + rc_operand_type_is_memref(operand1->memref_access_type) && + !operand1->value.memref) { + rc_validate_add_error(state, RC_VALIDATION_ERR_RECALL_BEFORE_REMEMBER, 0, 0); + } + if (rc_operand_is_recall(&cond->operand2) && + rc_operand_type_is_memref(cond->operand2.memref_access_type) && + !cond->operand2.value.memref) { + rc_validate_add_error(state, RC_VALIDATION_ERR_RECALL_BEFORE_REMEMBER, 0, 0); + } + + switch (cond->type) { + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_REMEMBER: + is_combining = 1; + continue; + + case RC_CONDITION_ADD_ADDRESS: + if (operand1->type == RC_OPERAND_DELTA || operand1->type == RC_OPERAND_PRIOR) + rc_validate_add_error(state, RC_VALIDATION_ERR_POINTER_FROM_PREVIOUS_FRAME, 0, 0); + else if (rc_operand_is_float(operand1) || rc_operand_is_float(&cond->operand2)) + rc_validate_add_error(state, RC_VALIDATION_ERR_POINTER_NON_INTEGER_OFFSET, 0, 0); + else if (rc_operand_type_is_transform(operand1->type) && cond->oper != RC_OPERATOR_MULT) + rc_validate_add_error(state, RC_VALIDATION_ERR_POINTER_TRANSFORMED_OFFSET, 0, 0); + + in_add_address = 1; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_SUB_HITS: + in_add_hits = 1; + is_combining = 1; + break; + + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + is_combining = 1; + break; + + case RC_CONDITION_RESET_IF: + if (in_add_hits) { + /* ResetIf at the end of a hit chain does not require a hit target. + * It's meant to reset things if some subset of conditions have been true. */ + in_add_hits = 0; + is_combining = 0; + break; + } + if (!state->has_hit_targets) + rc_validate_add_error(state, RC_VALIDATION_ERR_NO_HITS_TO_RESET, 0, 0); + else if (cond->required_hits == 1) + rc_validate_add_error(state, RC_VALIDATION_ERR_RESET_HIT_TARGET_OF_ONE, 0, 0); + /* fallthrough */ /* to default */ + default: + if (in_add_hits) { + if (cond->required_hits == 0) + rc_validate_add_error(state, RC_VALIDATION_ERR_ADDHITS_WITHOUT_TARGET, 0, 0); + + in_add_hits = 0; + } + + has_measured |= (cond->type == RC_CONDITION_MEASURED); + if (cond->type == RC_CONDITION_MEASURED_IF && measuredif_index == -1) + measuredif_index = state->cond_index; + + is_combining = 0; + break; + } + + /* original operands are valid. now switch to the derived operands for logic + * combining/comparing them */ + operand1 = &cond->operand1; + is_memref1 = rc_operand_is_memref(operand1); + + /* check for comparing two differently sized memrefs */ + if (is_memref1 && is_memref2 && + operand1->value.memref->value.memref_type == RC_MEMREF_TYPE_MEMREF && + cond->operand2.value.memref->value.memref_type == RC_MEMREF_TYPE_MEMREF && + rc_max_value(operand1) != rc_max_value(&cond->operand2)) { + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARING_DIFFERENT_MEMORY_SIZES, 0, 0); + } + + if (is_memref1 && rc_operand_is_float(operand1)) { + /* if left side is a float, right side will be converted to a float, so don't do range validation */ + } + else if (is_memref1 || is_memref2) { + /* if either side is a memref, check for impossible comparisons */ + const rc_operand_t* operand2 = &cond->operand2; + uint8_t oper = cond->oper; + uint32_t min, max; + uint32_t max_val = rc_max_value(operand2); + uint32_t min_val; + rc_typed_value_t typed_value; + + rc_chain_get_value_range(operand1, &min, &max); + if (min > max) { /* underflow */ + min = 0; + max = 0xFFFFFFFF; + } + + if (!is_memref1) { + /* pretend constant was on right side */ + operand2 = operand1; + operand1 = &cond->operand2; + max_val = max; + max = rc_max_value(&cond->operand2); + + switch (oper) { + case RC_OPERATOR_LT: oper = RC_OPERATOR_GT; break; + case RC_OPERATOR_LE: oper = RC_OPERATOR_GE; break; + case RC_OPERATOR_GT: oper = RC_OPERATOR_LT; break; + case RC_OPERATOR_GE: oper = RC_OPERATOR_LE; break; + } + } + + switch (operand2->type) { + case RC_OPERAND_CONST: + min_val = operand2->value.num; + break; + + case RC_OPERAND_FP: + min_val = (int)operand2->value.dbl; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + if (!rc_operand_is_float_memref(operand1) && (float)min_val != operand2->value.dbl) { + switch (oper) { + case RC_OPERATOR_EQ: + typed_value.value.f32 = (float)operand2->value.dbl; + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_NEVER_TRUE_INTEGER_TO_FLOAT, typed_value.value.u32, 0); + break; + + case RC_OPERATOR_NE: + typed_value.value.f32 = (float)operand2->value.dbl; + rc_validate_add_error(state, RC_VALIDATION_ERR_COMPARISON_ALWAYS_TRUE_INTEGER_TO_FLOAT, typed_value.value.u32, 0); + break; + + case RC_OPERATOR_GT: /* value could be greater than floor(float) */ + case RC_OPERATOR_LE: /* value could be less than or equal to floor(float) */ + break; + + case RC_OPERATOR_GE: /* value could be greater than or equal to ceil(float) */ + case RC_OPERATOR_LT: /* value could be less than ceil(float) */ + ++min_val; + break; + } + } + + break; + + default: /* right side is memref or add source chain */ + min_val = 0; + break; + } + + /* min_val and max_val are the range allowed by operand2. max is the upper value from operand1. */ + rc_validate_range(min_val, max_val, oper, max, state); + } + } + + if (is_combining) { + /* find the final condition so we can extract the type */ + state->cond_index--; + cond = condset->conditions; + while (cond->next) + cond = cond->next; + + rc_validate_add_error(state, RC_VALIDATION_ERR_TRAILING_CHAINING_CONDITION, cond->type, 0); + } + + if (measuredif_index != -1 && !has_measured) { + state->cond_index = measuredif_index; + rc_validate_add_error(state, RC_VALIDATION_ERR_MEASUREDIF_WITHOUT_MEASURED, 0, 0); + } + + return (state->error_count == errors_before); +} + +static int rc_condset_has_hittargets(const rc_condset_t* condset) +{ + if (condset->num_hittarget_conditions > 0) + return 1; + + /* pause and reset conditions may have hittargets and won't be classified as hittarget conditions. + * measured conditions may also have hittargets. + * other conditions may have hittarget conditions in an AndNext/OrNext chain. + * basically, check everything other than hittarget (explicitly known) and indirect (cannot have). + */ + if (condset->num_pause_conditions || condset->num_reset_conditions || condset->num_measured_conditions || condset->num_other_conditions) { + const rc_condition_t* condition = rc_condset_get_conditions((rc_condset_t*)condset); + /* ASSERT: don't need to add num_hittarget_conditions because it must be 0 per earlier check */ + const rc_condition_t* stop = condition + condset->num_pause_conditions + + condset->num_reset_conditions + condset->num_measured_conditions + + condset->num_other_conditions; + for (; condition < stop; ++condition) { + if (condition->required_hits) + return 1; + } + } + + return 0; +} + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t max_address) +{ + rc_validation_state_t state; + memset(&state, 0, sizeof(state)); + state.has_hit_targets = rc_condset_has_hittargets(condset); + state.max_address = max_address; + + result[0] = '\0'; + if (rc_validate_condset_internal(condset, &state)) { + const rc_validation_error_t* most_severe_error = rc_validate_find_most_severe_error(&state); + return rc_validate_format_error(result, result_size, &state, most_severe_error); + } + + return 1; +} + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t console_id) +{ + rc_validation_state_t state; + memset(&state, 0, sizeof(state)); + state.console_id = console_id; + state.max_address = rc_console_max_address(console_id); + state.has_hit_targets = rc_condset_has_hittargets(condset); + + result[0] = '\0'; + if (rc_validate_condset_internal(condset, &state)) { + const rc_validation_error_t* most_severe_error = rc_validate_find_most_severe_error(&state); + return rc_validate_format_error(result, result_size, &state, most_severe_error); + } + + return 1; +} + +/* rc_condition_is_combining doesn't look at conditions that build a memref (like AddSource) */ +static int rc_validate_is_combining_condition(const rc_condition_t* condition) +{ + switch (condition->type) + { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_REMEMBER: + return 1; + + default: + return 0; + } +} + +static int rc_validate_get_opposite_comparison(int oper) +{ + switch (oper) + { + case RC_OPERATOR_EQ: return RC_OPERATOR_NE; + case RC_OPERATOR_NE: return RC_OPERATOR_EQ; + case RC_OPERATOR_LT: return RC_OPERATOR_GE; + case RC_OPERATOR_LE: return RC_OPERATOR_GT; + case RC_OPERATOR_GT: return RC_OPERATOR_LE; + case RC_OPERATOR_GE: return RC_OPERATOR_LT; + default: return oper; + } +} + +static const rc_operand_t* rc_validate_get_comparison(const rc_condition_t* condition, int* comparison, unsigned* value) +{ + if (rc_operand_is_memref(&condition->operand1)) + { + if (condition->operand2.type != RC_OPERAND_CONST) + return NULL; + + *comparison = condition->oper; + *value = condition->operand2.value.num; + return &condition->operand1; + } + + if (condition->operand1.type != RC_OPERAND_CONST) + return NULL; + + if (!rc_operand_is_memref(&condition->operand2)) + return NULL; + + *comparison = rc_validate_get_opposite_comparison(condition->oper); + *value = condition->operand1.value.num; + return &condition->operand2; +} + +enum { + RC_OVERLAP_NONE = 0, + RC_OVERLAP_CONFLICTING, + RC_OVERLAP_REDUNDANT, + RC_OVERLAP_DEFER +}; + +static int rc_validate_comparison_overlap(int comparison1, uint32_t value1, int comparison2, uint32_t value2) +{ + /* NOTE: this only cares if comp2 conflicts with comp1. + * If comp1 conflicts with comp2, we'll catch that later (return RC_OVERLAP_NONE for now) */ + switch (comparison2) + { + case RC_OPERATOR_EQ: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a == 1 | a == 1 && a == 2 | a == 2 && a == 1 */ + /* redundant conflict conflict */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a == 1 | a <= 1 && a == 2 | a <= 2 && a == 1 */ + /* defer conflict defer */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a == 1 | a >= 1 && a == 2 | a >= 2 && a == 1 */ + /* defer defer conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a == 1 | a != 1 && a == 2 | a != 2 && a == 1 */ + /* conflict defer defer */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_LT: /* a < 1 && a == 1 | a < 1 && a == 2 | a < 2 && a == 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a == 1 | a > 1 && a == 2 | a > 2 && a == 1 */ + /* conflict defer conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_NE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a != 1 | a == 1 && a != 2 | a == 2 && a != 1 */ + /* conflict redundant redundant */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_REDUNDANT; + case RC_OPERATOR_LE: /* a <= 1 && a != 1 | a <= 1 && a != 2 | a <= 2 && a != 1 */ + /* none redundant none */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a != 1 | a >= 1 && a != 2 | a >= 2 && a != 1 */ + /* none none redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a != 1 | a != 1 && a != 2 | a != 2 && a != 1 */ + /* redundant none none */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a != 1 | a < 1 && a != 2 | a < 2 && a != 1 */ + /* redundant redundant none */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a != 1 | a > 1 && a != 2 | a > 2 && a != 1 */ + /* redundant none redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a < 1 | a == 1 && a < 2 | a == 2 && a < 1 */ + /* conflict redundant conflict */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a < 1 | a <= 1 && a < 2 | a <= 2 && a < 1 */ + /* defer redundant defer */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a < 1 | a >= 1 && a < 2 | a >= 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a < 1 | a != 1 && a < 2 | a != 2 && a < 1 */ + /* defer none defer */ + return (value1 >= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a < 1 | a < 1 && a < 2 | a < 2 && a < 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a < 1 | a > 1 && a < 2 | a > 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a <= 1 | a == 1 && a <= 2 | a == 2 && a <= 1 */ + /* redundant redundant conflict */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a <= 1 | a <= 1 && a <= 2 | a <= 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a <= 1 | a >= 1 && a <= 2 | a >= 2 && a <= 1 */ + /* none none conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a <= 1 | a != 1 && a <= 2 | a != 2 && a <= 1 */ + /* none none defer */ + return (value1 > value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a <= 1 | a < 1 && a <= 2 | a < 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a <= 1 | a > 1 && a <= 2 | a > 2 && a <= 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_GT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a > 1 | a == 1 && a > 2 | a == 2 && a > 1 */ + /* conflict conflict redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a > 1 | a <= 1 && a > 2 | a <= 2 && a > 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a > 1 | a >= 1 && a > 2 | a >= 2 && a > 1 */ + /* defer defer redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a > 1 | a != 1 && a > 2 | a != 2 && a > 1 */ + /* defer defer none */ + return (value1 <= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a > 1 | a < 1 && a > 2 | a < 2 && a > 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a > 1 | a > 1 && a > 2 | a > 2 && a > 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_GE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a >= 1 | a == 1 && a >= 2 | a == 2 && a >= 1 */ + /* redundant conflict redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a >= 1 | a <= 1 && a >= 2 | a <= 2 && a >= 1 */ + /* none conflict none */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a >= 1 | a >= 1 && a >= 2 | a >= 2 && a >= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a >= 1 | a != 1 && a >= 2 | a != 2 && a >= 1 */ + /* none defer none */ + return (value1 < value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a >= 1 | a < 1 && a >= 2 | a < 2 && a >= 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a >= 1 | a > 1 && a >= 2 | a > 2 && a >= 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + } + + return RC_OVERLAP_NONE; +} + +static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, const rc_condset_t* compare_conditions, + uint32_t group_index, rc_validation_state_t* state) +{ + int comparison1, comparison2; + uint32_t value1, value2; + const rc_operand_t* operand1; + const rc_operand_t* operand2; + const rc_condition_t* compare_condition; + const rc_condition_t* condition; + const rc_condition_t* condition_chain_start; + uint32_t errors_before = state->error_count; + int overlap; + int chain_matches; + + /* empty group */ + if (conditions == NULL || compare_conditions == NULL) + return 1; + + /* outer loop is the source conditions */ + for (condition = conditions->conditions; condition != NULL; condition = condition->next, ++state->cond_index) { + condition_chain_start = condition; + while (condition && rc_condition_is_combining(condition)) { + condition = condition->next; + } + if (!condition) + break; + + /* hits can be captured at any time, so any potential conflict will not be conflicting at another time */ + if (condition->required_hits) + continue; + + operand1 = rc_validate_get_comparison(condition, &comparison1, &value1); + if (!operand1) + continue; + + switch (condition->type) { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison1 = rc_validate_get_opposite_comparison(comparison1); + break; + default: + if (rc_validate_is_combining_condition(condition)) + continue; + break; + } + + /* inner loop is the potentially conflicting conditions */ + state->cond_index = 1; + for (compare_condition = compare_conditions->conditions; compare_condition != NULL; compare_condition = compare_condition->next, ++state->cond_index) { + if (compare_condition == condition_chain_start) { + /* skip condition we're already looking at */ + while (compare_condition != condition) { + ++state->cond_index; + compare_condition = compare_condition->next; + } + + continue; + } + + /* if combining conditions exist, make sure the same combining conditions exist in the + * compare logic. conflicts can only occur if the combinining conditions match. */ + chain_matches = 1; + if (condition_chain_start != condition) { + const rc_condition_t* condition_chain_iter = condition_chain_start; + while (condition_chain_iter != condition) { + if (compare_condition->type != condition_chain_iter->type || + compare_condition->oper != condition_chain_iter->oper || + compare_condition->required_hits != condition_chain_iter->required_hits || + !rc_operands_are_equal(&compare_condition->operand1, &condition_chain_iter->operand1)) + { + chain_matches = 0; + break; + } + + if (compare_condition->oper != RC_OPERATOR_NONE && + !rc_operands_are_equal(&compare_condition->operand2, &condition_chain_iter->operand2)) + { + chain_matches = 0; + break; + } + + if (!compare_condition->next) { + chain_matches = 0; + break; + } + + if (compare_condition->type != RC_CONDITION_ADD_ADDRESS && + compare_condition->type != RC_CONDITION_ADD_SOURCE && + compare_condition->type != RC_CONDITION_SUB_SOURCE && + compare_condition->type != RC_CONDITION_AND_NEXT) + { + /* things like AddHits and OrNext are hard to definitively detect conflicts. ignore them. */ + chain_matches = 0; + break; + } + + ++state->cond_index; + compare_condition = compare_condition->next; + condition_chain_iter = condition_chain_iter->next; + } + } + + /* combining field didn't match, or there's more unmatched combining fields. ignore this condition */ + if (!chain_matches || rc_validate_is_combining_condition(compare_condition)) { + while (compare_condition->next && rc_validate_is_combining_condition(compare_condition)) + compare_condition = compare_condition->next; + continue; + } + + if (compare_condition->required_hits) + continue; + + operand2 = rc_validate_get_comparison(compare_condition, &comparison2, &value2); + if (!operand2 || !rc_operands_are_equal(operand1, operand2)) + continue; + + switch (compare_condition->type) { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) /* PauseIf only affects conditions in same group */ + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison2 = rc_validate_get_opposite_comparison(comparison2); + break; + default: + if (rc_validate_is_combining_condition(compare_condition)) + continue; + break; + } + + overlap = rc_validate_comparison_overlap(comparison1, value1, comparison2, value2); + switch (overlap) + { + case RC_OVERLAP_CONFLICTING: + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf conflicts between groups, unless both conditions are PauseIfs */ + if (conditions != compare_conditions && compare_condition->type != condition->type) + continue; + } + break; + + case RC_OVERLAP_REDUNDANT: + if (group_index != state->group_index && state->group_index == 0) + { + /* if the alt condition is more restrictive than the core condition, ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + } + + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + /* if the PauseIf is less restrictive than the other condition, it's just a guard. ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + + /* PauseIf redundant with ResetIf is a conflict (both are inverted comparisons) */ + if (compare_condition->type == RC_CONDITION_RESET_IF || condition->type == RC_CONDITION_RESET_IF) + overlap = RC_OVERLAP_CONFLICTING; + } + else if (compare_condition->type == RC_CONDITION_RESET_IF && condition->type != RC_CONDITION_RESET_IF) + { + /* only ever report the redundancy on the non-ResetIf condition. The ResetIf is allowed to + * fire when the non-ResetIf condition is not true. */ + if (state->has_hit_targets) + continue; + } + else if (condition->type == RC_CONDITION_RESET_IF && compare_condition->type != RC_CONDITION_RESET_IF) + { + /* if the ResetIf condition is more restrictive than the non-ResetIf condition, + and there aren't any hits to clear, ignore it */ + if (state->has_hit_targets) + continue; + } + else if (compare_condition->type == RC_CONDITION_MEASURED_IF || condition->type == RC_CONDITION_MEASURED_IF) + { + /* ignore MeasuredIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + if (compare_condition->type == RC_CONDITION_MEASURED_IF && condition->type != RC_CONDITION_MEASURED_IF) + { + /* only ever report the redundancy on the non-MeasuredIf condition. The MeasuredIf provides + * additional functionality. */ + continue; + } + } + else if (condition->type == RC_CONDITION_TRIGGER && compare_condition->type != RC_CONDITION_TRIGGER) + { + /* Trigger is allowed to be redundant with non-trigger conditions as there may be limits that start a + * challenge that are further reduced for the completion of the challenge */ + continue; + } + break; + + default: + continue; + } + + /* if condition A conflicts with condition B, condition B will also conflict with + * condition A. don't report both. */ + { + int already_reported = 0; + const rc_validation_error_t* error = state->errors; + const rc_validation_error_t* stop = state->errors + state->error_count; + for (; error < stop; ++error) { + if (error->data2 == state->cond_index && error->data1 == state->group_index) { + if (error->err == RC_VALIDATION_ERR_REDUNDANT_CONDITION || + error->err == RC_VALIDATION_ERR_CONFLICTING_CONDITION) { + already_reported = 1; + } + } + } + + if (already_reported) + continue; + } + + rc_validate_add_error(state, (overlap == RC_OVERLAP_REDUNDANT) ? + RC_VALIDATION_ERR_REDUNDANT_CONDITION : RC_VALIDATION_ERR_CONFLICTING_CONDITION, + group_index, rc_validate_get_condition_index(conditions, condition)); + } + } + + return (state->error_count == errors_before); +} + +static int rc_validate_trigger_internal(const rc_trigger_t* trigger, rc_validation_state_t* state) +{ + rc_condset_t* alt; + uint32_t index; + + state->has_alt_groups = (trigger->alternative != NULL); + + state->has_hit_targets = trigger->requirement && rc_condset_has_hittargets(trigger->requirement); + if (!state->has_hit_targets) { + for (alt = trigger->alternative; alt; alt = alt->next) { + if (rc_condset_has_hittargets(alt)) { + state->has_hit_targets = 1; + break; + } + } + } + + state->group_index = 0; + if (rc_validate_condset_internal(trigger->requirement, state)) { + /* compare core to itself */ + rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, 0, state); + } + + index = 1; + for (alt = trigger->alternative; alt; alt = alt->next, ++index) { + state->group_index = index; + if (rc_validate_condset_internal(alt, state)) { + /* compare alt to itself */ + if (!rc_validate_conflicting_conditions(alt, alt, index, state)) + continue; + + /* compare alt to core */ + if (!rc_validate_conflicting_conditions(trigger->requirement, alt, 0, state)) + continue; + + /* compare core to alt */ + state->group_index = 0; + if (!rc_validate_conflicting_conditions(alt, trigger->requirement, index, state)) + continue; + } + } + + return (state->error_count == 0); +} + +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t max_address) +{ + rc_validation_state_t state; + memset(&state, 0, sizeof(state)); + state.max_address = max_address; + + result[0] = '\0'; + if (!rc_validate_trigger_internal(trigger, &state)) { + const rc_validation_error_t* most_severe_error = rc_validate_find_most_severe_error(&state); + return rc_validate_format_error(result, result_size, &state, most_severe_error); + } + + return 1; +} + +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t console_id) +{ + rc_validation_state_t state; + memset(&state, 0, sizeof(state)); + state.console_id = console_id; + state.max_address = rc_console_max_address(console_id); + + result[0] = '\0'; + if (!rc_validate_trigger_internal(trigger, &state)) { + const rc_validation_error_t* most_severe_error = rc_validate_find_most_severe_error(&state); + return rc_validate_format_error(result, result_size, &state, most_severe_error); + } + + return 1; +} diff --git a/src/rcheevos/src/rcheevos/rc_validate.h b/src/rcheevos/src/rcheevos/rc_validate.h new file mode 100644 index 0000000000..cfda94075d --- /dev/null +++ b/src/rcheevos/src/rcheevos/rc_validate.h @@ -0,0 +1,18 @@ +#ifndef RC_VALIDATE_H +#define RC_VALIDATE_H + +#include "rc_runtime_types.h" + +#include + +RC_BEGIN_C_DECLS + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t max_address); +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t max_address); + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, uint32_t console_id); +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, uint32_t console_id); + +RC_END_C_DECLS + +#endif /* RC_VALIDATE_H */ diff --git a/src/rcheevos/src/rcheevos/richpresence.c b/src/rcheevos/src/rcheevos/richpresence.c new file mode 100644 index 0000000000..b2f85c69e1 --- /dev/null +++ b/src/rcheevos/src/rcheevos/richpresence.c @@ -0,0 +1,922 @@ +#include "rc_internal.h" + +#include "../rc_compat.h" + +#include + +/* special formats only used by rc_richpresence_display_part_t.display_type. must not overlap other RC_FORMAT values */ +enum { + RC_FORMAT_STRING = 101, + RC_FORMAT_LOOKUP = 102, + RC_FORMAT_UNKNOWN_MACRO = 103, + RC_FORMAT_ASCIICHAR = 104, + RC_FORMAT_UNICODECHAR = 105 +}; + +static void rc_alloc_helper_variable_memref_value(rc_richpresence_display_part_t* part, const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { + rc_preparse_state_t preparse; + const char* test_memaddr = memaddr; + rc_condset_t* condset; + rc_value_t* value; + int32_t size; + + part->value.type = RC_OPERAND_NONE; + + /* if the expression can be represented as just a memory reference, do so */ + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = parse->memrefs; + value = RC_ALLOC(rc_value_t, &preparse.parse); + rc_parse_value_internal(value, &test_memaddr, &preparse.parse); + + size = preparse.parse.offset; + if (size < 0) { + parse->offset = size; + rc_destroy_preparse_state(&preparse); + return; + } + + /* ensure new needed memrefs are allocated in the primary buffer */ + rc_preparse_copy_memrefs(parse, &preparse.memrefs); + + /* parse the value into the scratch buffer so we can look at it */ + rc_reset_parse_state(&preparse.parse, rc_buffer_alloc(&preparse.parse.scratch.buffer, (size_t)size)); + preparse.parse.memrefs = parse->memrefs; + preparse.parse.existing_memrefs = parse->existing_memrefs; + value = RC_ALLOC(rc_value_t, &preparse.parse); + test_memaddr = memaddr; + rc_parse_value_internal(value, &test_memaddr, &preparse.parse); + + condset = value->conditions; + if (condset && !condset->next) { + /* single value - if it's a single Measured clause (including any AddSource/AddAddress helpers), we can + * simplify to a memref. If there are supporting clauses like MeasuredIf or ResetIf, we can't */ + if (condset->num_measured_conditions == 1 && + !condset->num_pause_conditions && !condset->num_reset_conditions && + !condset->num_other_conditions && !condset->num_hittarget_conditions) { + rc_condition_t* condition = condset->conditions; + for (; condition; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED && condition->required_hits == 0) { + memcpy(&part->value, &condition->operand1, sizeof(condition->operand1)); + break; + } + } + } + } + + rc_destroy_preparse_state(&preparse); + + /* could not express value with just a memory reference, create a helper variable */ + if (part->value.type == RC_OPERAND_NONE) { + value = rc_alloc_variable(memaddr, memaddr_len, parse); + if (value) { + part->value.value.memref = (rc_memref_t*)&value->value; + part->value.type = RC_OPERAND_ADDRESS; + part->value.size = RC_MEMSIZE_32_BITS; + part->value.memref_access_type = RC_OPERAND_ADDRESS; + } + } +} + +static const char* rc_parse_line(const char* line, const char** end, rc_parse_state_t* parse) { + const char* nextline; + const char* endline; + + /* get a single line */ + nextline = line; + while (*nextline && *nextline != '\n') + ++nextline; + + /* if a trailing comment marker (//) exists, the line stops there */ + endline = line; + while (endline < nextline && (endline[0] != '/' || endline[1] != '/' || (endline > line && endline[-1] == '\\'))) + ++endline; + + if (endline == nextline) { + /* trailing whitespace on a line without a comment marker may be significant, just remove the line ending */ + if (endline > line && endline[-1] == '\r') + --endline; + } else { + /* remove trailing whitespace before the comment marker */ + while (endline > line && isspace((int)((unsigned char*)endline)[-1])) + --endline; + } + + /* point end at the first character to ignore, it makes subtraction for length easier */ + *end = endline; + + /* tally the line */ + ++parse->lines_read; + + /* skip the newline character so we're pointing at the next line */ + if (*nextline == '\n') + ++nextline; + + return nextline; +} + +typedef struct rc_richpresence_builtin_macro_t { + const char* name; + size_t name_len; + uint8_t display_type; +} rc_richpresence_builtin_macro_t; + +static rc_richpresence_display_t* rc_parse_richpresence_display_internal(const char* line, const char* endline, rc_parse_state_t* parse, rc_richpresence_lookup_t* first_lookup) { + rc_richpresence_display_t* self; + rc_richpresence_display_part_t* part; + rc_richpresence_display_part_t** next; + rc_richpresence_lookup_t* lookup; + const char* ptr; + const char* in; + char* out; + + if (endline - line < 1) { + parse->offset = RC_MISSING_DISPLAY_STRING; + return 0; + } + + { + self = RC_ALLOC(rc_richpresence_display_t, parse); + memset(self, 0, sizeof(rc_richpresence_display_t)); + next = &self->display; + } + + /* break the string up on macros: text @macro() moretext */ + do { + ptr = line; + while (ptr < endline) { + if (*ptr == '@' && (ptr == line || ptr[-1] != '\\')) /* ignore escaped @s */ + break; + + ++ptr; + } + + if (ptr > line) { + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + /* handle string part */ + part->display_type = RC_FORMAT_STRING; + part->text = rc_alloc_str(parse, line, (int)(ptr - line)); + if (part->text) { + /* remove backslashes used for escaping */ + in = part->text; + while (*in && *in != '\\') + ++in; + + if (*in == '\\') { + out = (char*)in++; + while (*in) { + *out++ = *in++; + if (*in == '\\') + ++in; + } + *out = '\0'; + } + } + } + + if (*ptr == '@') { + /* handle macro part */ + size_t macro_len; + + line = ++ptr; + while (ptr < endline && *ptr != '(') + ++ptr; + + if (ptr == endline) { + parse->offset = RC_MISSING_VALUE; + return 0; + } + + macro_len = ptr - line; + + part = RC_ALLOC(rc_richpresence_display_part_t, parse); + memset(part, 0, sizeof(rc_richpresence_display_part_t)); + *next = part; + next = &part->next; + + part->display_type = RC_FORMAT_UNKNOWN_MACRO; + + /* find the lookup and hook it up */ + lookup = first_lookup; + while (lookup) { + if (strncmp(lookup->name, line, macro_len) == 0 && lookup->name[macro_len] == '\0') { + part->text = lookup->name; + part->lookup = lookup; + part->display_type = lookup->format; + break; + } + + lookup = lookup->next; + } + + if (!lookup) { + static const rc_richpresence_builtin_macro_t builtin_macros[] = { + {"Number", 6, RC_FORMAT_VALUE}, + {"Score", 5, RC_FORMAT_SCORE}, + {"Centiseconds", 12, RC_FORMAT_CENTISECS}, + {"Seconds", 7, RC_FORMAT_SECONDS}, + {"Minutes", 7, RC_FORMAT_MINUTES}, + {"SecondsAsMinutes", 16, RC_FORMAT_SECONDS_AS_MINUTES}, + {"ASCIIChar", 9, RC_FORMAT_ASCIICHAR}, + {"UnicodeChar", 11, RC_FORMAT_UNICODECHAR}, + {"Float1", 6, RC_FORMAT_FLOAT1}, + {"Float2", 6, RC_FORMAT_FLOAT2}, + {"Float3", 6, RC_FORMAT_FLOAT3}, + {"Float4", 6, RC_FORMAT_FLOAT4}, + {"Float5", 6, RC_FORMAT_FLOAT5}, + {"Float6", 6, RC_FORMAT_FLOAT6}, + {"Fixed1", 6, RC_FORMAT_FIXED1}, + {"Fixed2", 6, RC_FORMAT_FIXED2}, + {"Fixed3", 6, RC_FORMAT_FIXED3}, + {"Unsigned", 8, RC_FORMAT_UNSIGNED_VALUE}, + {"Unformatted", 11, RC_FORMAT_UNFORMATTED} + }; + size_t i; + + for (i = 0; i < sizeof(builtin_macros) / sizeof(builtin_macros[0]); ++i) { + if (macro_len == builtin_macros[i].name_len && + memcmp(builtin_macros[i].name, line, builtin_macros[i].name_len) == 0) { + part->text = builtin_macros[i].name; + part->lookup = NULL; + part->display_type = builtin_macros[i].display_type; + break; + } + } + } + + /* find the closing parenthesis */ + in = line; + line = ++ptr; + while (ptr < endline && *ptr != ')') + ++ptr; + + if (*ptr != ')') { + /* non-terminated macro, dump the macro and the remaining portion of the line */ + --in; /* already skipped over @ */ + part->display_type = RC_FORMAT_STRING; + part->text = rc_alloc_str(parse, in, (int)(ptr - in)); + } + else if (part->display_type != RC_FORMAT_UNKNOWN_MACRO) { + rc_alloc_helper_variable_memref_value(part, line, (int)(ptr - line), parse); + if (parse->offset < 0) + return 0; + + ++ptr; + } + else { + /* assert: the allocated string is going to be smaller than the memory used for the parameter of the macro */ + ++ptr; + part->text = rc_alloc_str(parse, in, (int)(ptr - in)); + } + } + + line = ptr; + } while (line < endline); + + *next = 0; + + return self; +} + +static int rc_richpresence_lookup_item_count(rc_richpresence_lookup_item_t* item) +{ + if (item == NULL) + return 0; + + return (rc_richpresence_lookup_item_count(item->left) + rc_richpresence_lookup_item_count(item->right) + 1); +} + +static void rc_rebalance_richpresence_lookup_get_items(rc_richpresence_lookup_item_t* root, + rc_richpresence_lookup_item_t** items, int* index) +{ + if (root->left != NULL) + rc_rebalance_richpresence_lookup_get_items(root->left, items, index); + + items[*index] = root; + ++(*index); + + if (root->right != NULL) + rc_rebalance_richpresence_lookup_get_items(root->right, items, index); +} + +static void rc_rebalance_richpresence_lookup_rebuild(rc_richpresence_lookup_item_t** root, + rc_richpresence_lookup_item_t** items, int first, int last) +{ + int mid = (first + last) / 2; + rc_richpresence_lookup_item_t* item = items[mid]; + *root = item; + + if (mid == first) + item->left = NULL; + else + rc_rebalance_richpresence_lookup_rebuild(&item->left, items, first, mid - 1); + + if (mid == last) + item->right = NULL; + else + rc_rebalance_richpresence_lookup_rebuild(&item->right, items, mid + 1, last); +} + +static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** root, rc_parse_state_t* parse) +{ + rc_richpresence_lookup_item_t** items; + int index; + int size; + + /* don't bother rebalancing one or two items */ + int count = rc_richpresence_lookup_item_count(*root); + if (count < 3) + return; + + /* allocate space for the flattened list in scratch memory */ + size = count * sizeof(rc_richpresence_lookup_item_t*); + items = (rc_richpresence_lookup_item_t**)rc_buffer_alloc(&parse->scratch.buffer, size); + + /* if allocation fails, we can still use the unbalanced tree, so just bail out */ + if (items == NULL) + return; + + /* flatten the list */ + index = 0; + rc_rebalance_richpresence_lookup_get_items(*root, items, &index); + + /* and rebuild it as a balanced tree */ + rc_rebalance_richpresence_lookup_rebuild(root, items, 0, count - 1); +} + +static void rc_insert_richpresence_lookup_item(rc_richpresence_lookup_t* lookup, + uint32_t first, uint32_t last, const char* label, size_t label_len, rc_parse_state_t* parse) +{ + rc_richpresence_lookup_item_t** next; + rc_richpresence_lookup_item_t* item; + + next = &lookup->root; + while ((item = *next) != NULL) { + if (first > item->last) { + if (first == item->last + 1 && + strncmp(label, item->label, label_len) == 0 && item->label[label_len] == '\0') { + item->last = last; + return; + } + + next = &item->right; + } + else if (last < item->first) { + if (last == item->first - 1 && + strncmp(label, item->label, label_len) == 0 && item->label[label_len] == '\0') { + item->first = first; + return; + } + + next = &item->left; + } + else { + parse->offset = RC_DUPLICATED_VALUE; + return; + } + } + + item = RC_ALLOC_SCRATCH(rc_richpresence_lookup_item_t, parse); + item->first = first; + item->last = last; + item->label = rc_alloc_str(parse, label, label_len); + item->left = item->right = NULL; + + *next = item; +} + +static const char* rc_parse_richpresence_lookup(rc_richpresence_lookup_t* lookup, const char* nextline, rc_parse_state_t* parse) +{ + const char* line; + const char* endline; + const char* label; + char* endptr = 0; + uint32_t first, last; + int base; + + do + { + line = nextline; + if (line == NULL) + break; + + nextline = rc_parse_line(line, &endline, parse); + + if (endline - line < 2) { + /* ignore full line comments inside a lookup */ + if (line[0] == '/' && line[1] == '/') + continue; + + /* empty line indicates end of lookup */ + if (lookup->root) + rc_rebalance_richpresence_lookup(&lookup->root, parse); + break; + } + + /* "*=XXX" specifies default label if lookup does not provide a mapping for the value */ + if (line[0] == '*' && line[1] == '=') { + line += 2; + lookup->default_label = rc_alloc_str(parse, line, (int)(endline - line)); + continue; + } + + label = line; + while (label < endline && *label != '=') + ++label; + + if (label == endline) { + parse->offset = RC_MISSING_VALUE; + break; + } + ++label; + + do { + /* get the value for the mapping */ + if (line[0] == '0' && line[1] == 'x') { + line += 2; + base = 16; + } else { + base = 10; + } + + first = (unsigned)strtoul(line, &endptr, base); + + /* check for a range */ + if (*endptr != '-') { + /* no range, just set last to first */ + last = first; + } + else { + /* range, get last value */ + line = endptr + 1; + + if (line[0] == '0' && line[1] == 'x') { + line += 2; + base = 16; + } else { + base = 10; + } + + last = (unsigned)strtoul(line, &endptr, base); + } + + /* ignore spaces after the number - was previously ignored as string was split on equals */ + while (*endptr == ' ') + ++endptr; + + /* if we've found the equal sign, this is the last item */ + if (*endptr == '=') { + rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + break; + } + + /* otherwise, if it's not a comma, it's an error */ + if (*endptr != ',') { + parse->offset = RC_INVALID_CONST_OPERAND; + break; + } + + /* insert the current item and continue scanning the next one */ + rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + if (parse->offset < 0) + break; + + line = endptr + 1; + } while (line < endline); + + } while (parse->offset > 0); + + return nextline; +} + +void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse) { + rc_richpresence_display_t** nextdisplay; + rc_richpresence_lookup_t* firstlookup = NULL; + rc_richpresence_lookup_t** nextlookup = &firstlookup; + rc_richpresence_lookup_t* lookup; + rc_trigger_t* trigger; + char format[64]; + const char* display = 0; + const char* line; + const char* nextline; + const char* endline; + const char* ptr; + int hasdisplay = 0; + int display_line = 0; + int chars; + + self->values = NULL; + + /* special case for empty script to return 1 line read */ + if (!*script) { + parse->lines_read = 1; + parse->offset = RC_MISSING_DISPLAY_STRING; + return; + } + + /* first pass: process macro initializers */ + line = script; + while (*line) { + nextline = rc_parse_line(line, &endline, parse); + if (strncmp(line, "Lookup:", 7) == 0) { + line += 7; + + lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); + lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); + lookup->format = RC_FORMAT_LOOKUP; + lookup->root = NULL; + lookup->default_label = ""; + *nextlookup = lookup; + nextlookup = &lookup->next; + + nextline = rc_parse_richpresence_lookup(lookup, nextline, parse); + if (parse->offset < 0) + return; + + } else if (strncmp(line, "Format:", 7) == 0) { + line += 7; + if (endline - line == 11 && memcmp(line, "Unformatted", 11) == 0) { + /* for backwards compatibility with the comma rollout, we allow old scripts + * to define an Unformatted type mapped to VALUE, and new versions will ignore + * the definition and use the built-in macro. skip the next line (FormatType=) */ + line = rc_parse_line(nextline, &endline, parse); + continue; + } + + lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); + lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); + lookup->root = NULL; + lookup->default_label = ""; + *nextlookup = lookup; + nextlookup = &lookup->next; + + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + if (parse->buffer && strncmp(line, "FormatType=", 11) == 0) { + line += 11; + + chars = (int)(endline - line); + if (chars > 63) + chars = 63; + memcpy(format, line, chars); + format[chars] = '\0'; + + lookup->format = (uint8_t)rc_parse_format(format); + } else { + lookup->format = RC_FORMAT_VALUE; + } + } else if (strncmp(line, "Display:", 8) == 0) { + display = nextline; + display_line = parse->lines_read; + + /* scan as long as we find conditional lines or full line comments */ + do { + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + } while (*line == '?' || (line[0] == '/' && line[1] == '/')); + } + + line = nextline; + } + + *nextlookup = 0; + self->first_lookup = firstlookup; + + nextdisplay = &self->first_display; + + /* second pass, process display string*/ + if (display) { + /* point the parser back at the display strings */ + int lines_read = parse->lines_read; + parse->lines_read = display_line; + line = display; + + nextline = rc_parse_line(line, &endline, parse); + + do { + if (line[0] == '?') { + /* conditional display: ?trigger?string */ + ptr = ++line; + while (ptr < endline && *ptr != '?') + ++ptr; + + if (ptr < endline) { + *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); + if (parse->offset < 0) + return; + + trigger = &((*nextdisplay)->trigger); + rc_parse_trigger_internal(trigger, &line, parse); + if (parse->offset < 0) + return; + + if (line != ptr) { + /* incomplete read */ + parse->offset = RC_INVALID_OPERATOR; + return; + } + + (*nextdisplay)->has_required_hits = parse->has_required_hits; + + if (parse->buffer) + nextdisplay = &((*nextdisplay)->next); + } + } + else if (line[0] != '/' || line[1] != '/') { + break; + } + + line = nextline; + nextline = rc_parse_line(line, &endline, parse); + } while (1); + + /* non-conditional display: string */ + *nextdisplay = rc_parse_richpresence_display_internal(line, endline, parse, firstlookup); + if (*nextdisplay) { + hasdisplay = 1; + nextdisplay = &((*nextdisplay)->next); + + /* restore the parser state */ + parse->lines_read = lines_read; + } + else { + /* this should only happen if the line is blank. + * expect parse->offset to be RC_MISSING_DISPLAY_STRING and leave parse->lines_read + * on the current line for error tracking. */ + } + } + + /* finalize */ + *nextdisplay = 0; + self->has_memrefs = 0; + + if (!hasdisplay && parse->offset > 0) { + parse->offset = RC_MISSING_DISPLAY_STRING; + } +} + +int rc_richpresence_size_lines(const char* script, int* lines_read) { + rc_richpresence_with_memrefs_t* richpresence; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); + + richpresence = RC_ALLOC(rc_richpresence_with_memrefs_t, &preparse.parse); + preparse.parse.variables = &richpresence->richpresence.values; + rc_parse_richpresence_internal(&richpresence->richpresence, script, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); + + if (lines_read) + *lines_read = preparse.parse.lines_read; + + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; +} + +int rc_richpresence_size(const char* script) { + return rc_richpresence_size_lines(script, NULL); +} + +rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, void* unused_L, int unused_funcs_idx) { + rc_richpresence_with_memrefs_t* richpresence; + rc_preparse_state_t preparse; + + (void)unused_L; + (void)unused_funcs_idx; + + if (!buffer || !script) + return NULL; + + rc_init_preparse_state(&preparse); + richpresence = RC_ALLOC(rc_richpresence_with_memrefs_t, &preparse.parse); + preparse.parse.variables = &richpresence->richpresence.values; + rc_parse_richpresence_internal(&richpresence->richpresence, script, &preparse.parse); + + rc_reset_parse_state(&preparse.parse, buffer); + richpresence = RC_ALLOC(rc_richpresence_with_memrefs_t, &preparse.parse); + preparse.parse.variables = &richpresence->richpresence.values; + rc_preparse_alloc_memrefs(&richpresence->memrefs, &preparse); + + rc_parse_richpresence_internal(&richpresence->richpresence, script, &preparse.parse); + richpresence->richpresence.has_memrefs = 1; + + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &richpresence->richpresence : NULL; +} + +static void rc_update_richpresence_memrefs(rc_richpresence_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_richpresence_with_memrefs_t* richpresence = (rc_richpresence_with_memrefs_t*)self; + rc_update_memref_values(&richpresence->memrefs, peek, ud); + } +} + +rc_memrefs_t* rc_richpresence_get_memrefs(rc_richpresence_t* self) { + if (self->has_memrefs) { + rc_richpresence_with_memrefs_t* richpresence = (rc_richpresence_with_memrefs_t*)self; + return &richpresence->memrefs; + } + + return NULL; +} + +void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, void* unused_L) { + (void)unused_L; + + rc_update_richpresence_memrefs(richpresence, peek, peek_ud); + rc_update_values(richpresence->values, peek, peek_ud); + rc_update_richpresence_internal(richpresence, peek, peek_ud); +} + +void rc_update_richpresence_internal(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud) { + rc_richpresence_display_t* display; + + for (display = richpresence->first_display; display; display = display->next) { + if (display->has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, NULL); + } +} + +static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part, char* buffer, size_t buffersize) +{ + rc_richpresence_lookup_item_t* item; + rc_typed_value_t value; + char tmp[256]; + char* ptr = buffer; + const char* text; + size_t chars; + + *ptr = '\0'; + while (part) { + switch (part->display_type) { + case RC_FORMAT_STRING: + text = part->text; + chars = strlen(text); + break; + + case RC_FORMAT_LOOKUP: + rc_evaluate_operand(&value, &part->value, NULL); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + + text = part->lookup->default_label; + item = part->lookup->root; + while (item) { + if (value.value.u32 > item->last) { + item = item->right; + } + else if (value.value.u32 < item->first) { + item = item->left; + } + else { + text = item->label; + break; + } + } + + chars = strlen(text); + break; + + case RC_FORMAT_ASCIICHAR: + chars = 0; + text = tmp; + value.type = RC_VALUE_TYPE_UNSIGNED; + + do { + rc_evaluate_operand(&value, &part->value, NULL); + if (value.value.u32 == 0) { + /* null terminator - skip over remaining character macros */ + while (part->next && part->next->display_type == RC_FORMAT_ASCIICHAR) + part = part->next; + break; + } + + if (value.value.u32 < 32 || value.value.u32 >= 127) + value.value.u32 = '?'; + + tmp[chars++] = (char)value.value.u32; + if (chars == sizeof(tmp) || !part->next || part->next->display_type != RC_FORMAT_ASCIICHAR) + break; + + part = part->next; + } while (1); + + tmp[chars] = '\0'; + break; + + case RC_FORMAT_UNICODECHAR: + chars = 0; + text = tmp; + value.type = RC_VALUE_TYPE_UNSIGNED; + + do { + rc_evaluate_operand(&value, &part->value, NULL); + if (value.value.u32 == 0) { + /* null terminator - skip over remaining character macros */ + while (part->next && part->next->display_type == RC_FORMAT_UNICODECHAR) + part = part->next; + break; + } + + if (value.value.u32 < 32 || value.value.u32 > 65535) + value.value.u32 = 0xFFFD; /* unicode replacement char */ + + if (value.value.u32 < 0x80) { + tmp[chars++] = (char)value.value.u32; + } + else if (value.value.u32 < 0x0800) { + tmp[chars + 1] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars] = (char)(0xC0 | (value.value.u32 & 0x1F)); + chars += 2; + } + else { + /* surrogate pair not supported, convert to replacement char */ + if (value.value.u32 >= 0xD800 && value.value.u32 < 0xE000) + value.value.u32 = 0xFFFD; + + tmp[chars + 2] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars + 1] = (char)(0x80 | (value.value.u32 & 0x3F)); value.value.u32 >>= 6; + tmp[chars] = (char)(0xE0 | (value.value.u32 & 0x1F)); + chars += 3; + } + + if (chars >= sizeof(tmp) - 3 || !part->next || part->next->display_type != RC_FORMAT_UNICODECHAR) + break; + + part = part->next; + } while (1); + + tmp[chars] = '\0'; + break; + + case RC_FORMAT_UNKNOWN_MACRO: + chars = snprintf(tmp, sizeof(tmp), "[Unknown macro]%s", part->text); + text = tmp; + break; + + default: + rc_evaluate_operand(&value, &part->value, NULL); + chars = rc_format_typed_value(tmp, sizeof(tmp), &value, part->display_type); + text = tmp; + break; + } + + if (chars > 0 && buffersize > 0) { + if ((unsigned)chars >= buffersize) { + /* prevent write past end of buffer */ + memcpy(ptr, text, buffersize - 1); + ptr[buffersize - 1] = '\0'; + buffersize = 0; + } + else { + memcpy(ptr, text, chars); + ptr[chars] = '\0'; + buffersize -= (unsigned)chars; + } + } + + ptr += chars; + part = part->next; + } + + return (int)(ptr - buffer); +} + +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L) { + rc_richpresence_display_t* display; + + for (display = richpresence->first_display; display; display = display->next) { + /* if we've reached the end of the condition list, process it */ + if (!display->next) + return rc_evaluate_richpresence_display(display->display, buffer, buffersize); + + /* triggers with required hits will be updated in rc_update_richpresence */ + if (!display->has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, unused_L); + + /* if we've found a valid condition, process it */ + if (display->trigger.state == RC_TRIGGER_STATE_TRIGGERED) + return rc_evaluate_richpresence_display(display->display, buffer, buffersize); + } + + buffer[0] = '\0'; + return 0; +} + +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L) { + rc_update_richpresence(richpresence, peek, peek_ud, unused_L); + return rc_get_richpresence_display_string(richpresence, buffer, buffersize, peek, peek_ud, unused_L); +} + +void rc_reset_richpresence_triggers(rc_richpresence_t* self) { + rc_richpresence_display_t* display; + + for (display = self->first_display; display; display = display->next) + rc_reset_trigger(&display->trigger); +} + +void rc_reset_richpresence(rc_richpresence_t* self) { + rc_reset_richpresence_triggers(self); + rc_reset_values(self->values); +} diff --git a/src/rcheevos/src/rcheevos/runtime.c b/src/rcheevos/src/rcheevos/runtime.c new file mode 100644 index 0000000000..9c05985ee8 --- /dev/null +++ b/src/rcheevos/src/rcheevos/runtime.c @@ -0,0 +1,852 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +#define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 + +/* ===== natvis extensions ===== */ + +typedef struct __rc_runtime_trigger_list_t { rc_runtime_t runtime; } __rc_runtime_trigger_list_t; +typedef struct __rc_runtime_lboard_list_t { rc_runtime_t runtime; } __rc_runtime_lboard_list_t; + +static void rc_runtime_natvis_helper(const rc_runtime_event_t* runtime_event) +{ + struct natvis_extensions { + __rc_runtime_trigger_list_t trigger_list; + __rc_runtime_lboard_list_t lboard_list; + } natvis; + + memset(&natvis, 0, sizeof(natvis)); + (void)runtime_event; + + natvis.lboard_list.runtime.lboard_count = 0; +} + +/* ============================= */ + +rc_runtime_t* rc_runtime_alloc(void) { + rc_runtime_t* self; + + /* create a reference to rc_runtime_natvis_helper so the extensions get compiled in. */ + rc_runtime_event_handler_t unused = &rc_runtime_natvis_helper; + (void)unused; + + self = (rc_runtime_t*)malloc(sizeof(rc_runtime_t)); + + if (self) { + rc_runtime_init(self); + self->owns_self = 1; + } + + return self; +} + +void rc_runtime_init(rc_runtime_t* self) { + memset(self, 0, sizeof(rc_runtime_t)); + + self->memrefs = (rc_memrefs_t*)malloc(sizeof(*self->memrefs)); + rc_memrefs_init(self->memrefs); +} + +void rc_runtime_destroy(rc_runtime_t* self) { + uint32_t i; + + if (self->triggers) { + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].buffer) + free(self->triggers[i].buffer); + } + + free(self->triggers); + self->triggers = NULL; + + self->trigger_count = self->trigger_capacity = 0; + } + + if (self->lboards) { + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].buffer) + free(self->lboards[i].buffer); + } + + free(self->lboards); + self->lboards = NULL; + + self->lboard_count = self->lboard_capacity = 0; + } + + if (self->richpresence) { + if (self->richpresence->buffer) + free(self->richpresence->buffer); + free(self->richpresence); + } + + if (self->memrefs) + rc_memrefs_destroy(self->memrefs); + + if (self->owns_self) + free(self); +} + +void rc_runtime_checksum(const char* memaddr, uint8_t* md5) { + md5_state_t state; + md5_init(&state); + md5_append(&state, (unsigned char*)memaddr, (int)strlen(memaddr)); + md5_finish(&state, md5); +} + +static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, uint32_t index) { + /* free the trigger, then replace it with the last trigger */ + free(self->triggers[index].buffer); + + if (--self->trigger_count > index) + memcpy(&self->triggers[index], &self->triggers[self->trigger_count], sizeof(rc_runtime_trigger_t)); +} + +void rc_runtime_deactivate_achievement(rc_runtime_t* self, uint32_t id) { + uint32_t i; + + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) + rc_runtime_deactivate_trigger_by_index(self, i); + } +} + +int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx) { + void* trigger_buffer; + rc_trigger_t* trigger; + rc_runtime_trigger_t* runtime_trigger; + rc_preparse_state_t preparse; + const char* preparse_memaddr = memaddr; + uint8_t md5[16]; + int32_t size; + uint32_t i; + + (void)unused_L; + (void)unused_funcs_idx; + + if (memaddr == NULL) + return RC_INVALID_MEMORY_OPERAND; + + rc_runtime_checksum(memaddr, md5); + + /* check to see if the id is already registered with an active trigger */ + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) { + if (memcmp(self->triggers[i].md5, md5, 16) == 0) { + /* if the checksum hasn't changed, we can reuse the existing item */ + rc_reset_trigger(self->triggers[i].trigger); + return RC_OK; + } + + /* checksum has changed, deactivate the the item */ + rc_runtime_deactivate_trigger_by_index(self, i); + + /* deactivate may reorder the list so we should continue from the current index. however, we + * assume that only one trigger is active per id, so having found that, just stop scanning. + */ + break; + } + } + + /* check to see if a disabled trigger for the specific id matches the trigger being registered */ + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].id == id && memcmp(self->triggers[i].md5, md5, 16) == 0) { + /* retrieve the trigger pointer from the buffer */ + size = 0; + trigger = (rc_trigger_t*)rc_alloc(self->triggers[i].buffer, &size, sizeof(rc_trigger_t), RC_ALIGNOF(rc_trigger_t), NULL, -1); + self->triggers[i].trigger = trigger; + + rc_reset_trigger(trigger); + return RC_OK; + } + } + + /* item has not been previously registered, determine how much space we need for it, and allocate it */ + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = self->memrefs; + trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + rc_parse_trigger_internal(trigger, &preparse_memaddr, &preparse.parse); + + size = preparse.parse.offset; + if (size < 0) + return size; + + trigger_buffer = malloc(size); + if (!trigger_buffer) + return RC_OUT_OF_MEMORY; + + /* populate the item, using the communal memrefs pool */ + rc_reset_parse_state(&preparse.parse, trigger_buffer); + rc_preparse_reserve_memrefs(&preparse, self->memrefs); + trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + rc_parse_trigger_internal(trigger, &memaddr, &preparse.parse); + rc_destroy_preparse_state(&preparse); + + if (preparse.parse.offset < 0) { + free(trigger_buffer); + return preparse.parse.offset; + } + + /* grow the trigger buffer if necessary */ + if (self->trigger_count == self->trigger_capacity) { + self->trigger_capacity += 32; + if (!self->triggers) + self->triggers = (rc_runtime_trigger_t*)malloc(self->trigger_capacity * sizeof(rc_runtime_trigger_t)); + else + self->triggers = (rc_runtime_trigger_t*)realloc(self->triggers, self->trigger_capacity * sizeof(rc_runtime_trigger_t)); + + if (!self->triggers) { + free(trigger_buffer); + return RC_OUT_OF_MEMORY; + } + } + + /* assign the new trigger */ + runtime_trigger = &self->triggers[self->trigger_count]; + runtime_trigger->id = id; + runtime_trigger->trigger = trigger; + runtime_trigger->buffer = trigger_buffer; + runtime_trigger->invalid_memref = NULL; + memcpy(runtime_trigger->md5, md5, 16); + runtime_trigger->serialized_size = 0; + ++self->trigger_count; + + /* reset it, and return it */ + rc_reset_trigger(trigger); + return RC_OK; +} + +rc_trigger_t* rc_runtime_get_achievement(const rc_runtime_t* self, uint32_t id) +{ + uint32_t i; + + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].id == id && self->triggers[i].trigger != NULL) + return self->triggers[i].trigger; + } + + return NULL; +} + +int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target) +{ + const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); + if (!measured_value || !measured_target) + return 0; + + if (!trigger) { + *measured_value = *measured_target = 0; + return 0; + } + + if (rc_trigger_state_active(trigger->state)) { + *measured_value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; + *measured_target = trigger->measured_target; + } + else { + /* don't report measured information for inactive triggers */ + *measured_value = *measured_target = 0; + } + + return 1; +} + +int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char* buffer, size_t buffer_size) +{ + const rc_trigger_t* trigger = rc_runtime_get_achievement(runtime, id); + uint32_t value; + if (!buffer || !buffer_size) + return 0; + + if (!trigger || /* no trigger */ + trigger->measured_target == 0 || /* not measured */ + !rc_trigger_state_active(trigger->state)) { /* don't report measured value for inactive triggers */ + *buffer = '\0'; + return 0; + } + + /* cap the value at the target so we can count past the target: "107 >= 100" */ + value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; + if (value > trigger->measured_target) + value = trigger->measured_target; + + if (trigger->measured_as_percent) { + const uint32_t percent = (uint32_t)(((unsigned long long)value * 100) / trigger->measured_target); + return snprintf(buffer, buffer_size, "%u%%", percent); + } + + return snprintf(buffer, buffer_size, "%u/%u", value, trigger->measured_target); +} + +static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, uint32_t index) { + /* free the lboard, then replace it with the last lboard */ + free(self->lboards[index].buffer); + + if (--self->lboard_count > index) + memcpy(&self->lboards[index], &self->lboards[self->lboard_count], sizeof(rc_runtime_lboard_t)); +} + +void rc_runtime_deactivate_lboard(rc_runtime_t* self, uint32_t id) { + uint32_t i; + + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) + rc_runtime_deactivate_lboard_by_index(self, i); + } +} + +int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx) { + void* lboard_buffer; + uint8_t md5[16]; + rc_lboard_t* lboard; + rc_preparse_state_t preparse; + rc_runtime_lboard_t* runtime_lboard; + int32_t size; + uint32_t i; + + (void)unused_L; + (void)unused_funcs_idx; + + if (memaddr == 0) + return RC_INVALID_MEMORY_OPERAND; + + rc_runtime_checksum(memaddr, md5); + + /* check to see if the id is already registered with an active lboard */ + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) { + if (memcmp(self->lboards[i].md5, md5, 16) == 0) { + /* if the checksum hasn't changed, we can reuse the existing item */ + rc_reset_lboard(self->lboards[i].lboard); + return RC_OK; + } + + /* checksum has changed, deactivate the the item */ + rc_runtime_deactivate_lboard_by_index(self, i); + + /* deactivate may reorder the list so we should continue from the current index. however, we + * assume that only one trigger is active per id, so having found that, just stop scanning. + */ + break; + } + } + + /* check to see if a disabled lboard for the specific id matches the lboard being registered */ + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].id == id && memcmp(self->lboards[i].md5, md5, 16) == 0) { + /* retrieve the lboard pointer from the buffer */ + size = 0; + lboard = (rc_lboard_t*)rc_alloc(self->lboards[i].buffer, &size, sizeof(rc_lboard_t), RC_ALIGNOF(rc_lboard_t), NULL, -1); + self->lboards[i].lboard = lboard; + + rc_reset_lboard(lboard); + return RC_OK; + } + } + + /* item has not been previously registered, determine how much space we need for it, and allocate it */ + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = self->memrefs; + lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(lboard, memaddr, &preparse.parse); + + size = preparse.parse.offset; + if (size < 0) + return size; + + lboard_buffer = malloc(size); + if (!lboard_buffer) + return RC_OUT_OF_MEMORY; + + /* populate the item, using the communal memrefs pool */ + rc_reset_parse_state(&preparse.parse, lboard_buffer); + rc_preparse_reserve_memrefs(&preparse, self->memrefs); + lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(lboard, memaddr, &preparse.parse); + rc_destroy_preparse_state(&preparse); + + if (preparse.parse.offset < 0) { + free(lboard_buffer); + return preparse.parse.offset; + } + + /* grow the lboard buffer if necessary */ + if (self->lboard_count == self->lboard_capacity) { + self->lboard_capacity += 16; + if (!self->lboards) + self->lboards = (rc_runtime_lboard_t*)malloc(self->lboard_capacity * sizeof(rc_runtime_lboard_t)); + else + self->lboards = (rc_runtime_lboard_t*)realloc(self->lboards, self->lboard_capacity * sizeof(rc_runtime_lboard_t)); + + if (!self->lboards) { + free(lboard_buffer); + return RC_OUT_OF_MEMORY; + } + } + + /* assign the new lboard */ + runtime_lboard = &self->lboards[self->lboard_count++]; + runtime_lboard->id = id; + runtime_lboard->value = 0; + runtime_lboard->lboard = lboard; + runtime_lboard->buffer = lboard_buffer; + runtime_lboard->invalid_memref = NULL; + memcpy(runtime_lboard->md5, md5, 16); + runtime_lboard->serialized_size = 0; + + /* reset it, and return it */ + rc_reset_lboard(lboard); + return RC_OK; +} + +rc_lboard_t* rc_runtime_get_lboard(const rc_runtime_t* self, uint32_t id) +{ + uint32_t i; + + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].id == id && self->lboards[i].lboard != NULL) + return self->lboards[i].lboard; + } + + return NULL; +} + +int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format) +{ + return rc_format_value(buffer, size, value, format); +} + +int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, void* unused_L, int unused_funcs_idx) { + rc_richpresence_t* richpresence; + rc_preparse_state_t preparse; + uint8_t md5[16]; + int size; + + (void)unused_L; + (void)unused_funcs_idx; + + if (script == NULL) + return RC_MISSING_DISPLAY_STRING; + + rc_runtime_checksum(script, md5); + + /* look for existing match */ + if (self->richpresence && self->richpresence->richpresence && memcmp(self->richpresence->md5, md5, 16) == 0) { + /* unchanged. reset all of the conditions */ + rc_reset_richpresence(self->richpresence->richpresence); + + /* return success*/ + return RC_OK; + } + + /* no existing match found, parse script */ + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = self->memrefs; + richpresence = RC_ALLOC(rc_richpresence_t, &preparse.parse); + preparse.parse.variables = &richpresence->values; + rc_parse_richpresence_internal(richpresence, script, &preparse.parse); + + size = preparse.parse.offset; + if (size < 0) + return size; + + /* if there's a previous script, free it */ + if (self->richpresence) { + free(self->richpresence->buffer); + free(self->richpresence); + } + + /* allocate and process the new script */ + self->richpresence = (rc_runtime_richpresence_t*)malloc(sizeof(rc_runtime_richpresence_t)); + if (!self->richpresence) + return RC_OUT_OF_MEMORY; + + memcpy(self->richpresence->md5, md5, sizeof(md5)); + + self->richpresence->buffer = malloc(size); + if (!self->richpresence->buffer) + return RC_OUT_OF_MEMORY; + + rc_reset_parse_state(&preparse.parse, self->richpresence->buffer); + rc_preparse_reserve_memrefs(&preparse, self->memrefs); + richpresence = RC_ALLOC(rc_richpresence_t, &preparse.parse); + preparse.parse.variables = &richpresence->values; + rc_parse_richpresence_internal(richpresence, script, &preparse.parse); + rc_destroy_preparse_state(&preparse); + + if (preparse.parse.offset < 0) { + free(self->richpresence->buffer); + free(self->richpresence); + self->richpresence = NULL; + return preparse.parse.offset; + } + + if (!richpresence->first_display || !richpresence->first_display->display) { + /* non-existant rich presence */ + self->richpresence->richpresence = NULL; + } + else { + /* reset all of the conditions */ + rc_reset_richpresence(richpresence); + self->richpresence->richpresence = richpresence; + } + + return RC_OK; +} + +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, void* unused_L) { + if (self->richpresence && self->richpresence->richpresence) + return rc_get_richpresence_display_string(self->richpresence->richpresence, buffer, buffersize, peek, peek_ud, unused_L); + + *buffer = '\0'; + return 0; +} + +void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, void* unused_L) { + rc_runtime_event_t runtime_event; + int i; + + runtime_event.value = 0; + + rc_update_memref_values(self->memrefs, peek, ud); + + for (i = self->trigger_count - 1; i >= 0; --i) { + rc_trigger_t* trigger = self->triggers[i].trigger; + int old_state, new_state; + uint32_t old_measured_value; + + if (!trigger) + continue; + + if (self->triggers[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED; + runtime_event.id = self->triggers[i].id; + runtime_event.value = self->triggers[i].invalid_memref->address; + + trigger->state = RC_TRIGGER_STATE_DISABLED; + self->triggers[i].invalid_memref = NULL; + + event_handler(&runtime_event); + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + continue; + } + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, peek, ud, unused_L); + + /* trigger->state doesn't actually change to RESET, RESET just serves as a notification. + * handle the notification, then look at the actual state */ + if (new_state == RC_TRIGGER_STATE_RESET) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_RESET; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + + new_state = trigger->state; + } + + /* if the measured value changed and the achievement hasn't triggered, send a notification */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_target != 0 && trigger->measured_value <= trigger->measured_target && + new_state != RC_TRIGGER_STATE_TRIGGERED && + new_state != RC_TRIGGER_STATE_INACTIVE && new_state != RC_TRIGGER_STATE_WAITING) { + + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED; + runtime_event.id = self->triggers[i].id; + + if (trigger->measured_as_percent) { + /* if reporting measured value as a percentage, only send the notification if the percentage changes */ + const int32_t old_percent = (int32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const int32_t new_percent = (int32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent != new_percent) { + runtime_event.value = new_percent; + event_handler(&runtime_event); + } + } + else { + runtime_event.value = trigger->measured_value; + event_handler(&runtime_event); + } + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise an UNPRIMED event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + } + + /* raise events for each of the possible new states */ + switch (new_state) + { + case RC_TRIGGER_STATE_TRIGGERED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_PAUSED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_PRIMED: + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + break; + + case RC_TRIGGER_STATE_ACTIVE: + /* only raise ACTIVATED event when transitioning from an inactive state. + * note that inactive in this case means active but cannot trigger. */ + if (old_state == RC_TRIGGER_STATE_WAITING || old_state == RC_TRIGGER_STATE_PAUSED) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED; + runtime_event.id = self->triggers[i].id; + event_handler(&runtime_event); + } + break; + } + } + + for (i = self->lboard_count - 1; i >= 0; --i) { + rc_lboard_t* lboard = self->lboards[i].lboard; + int lboard_state; + + if (!lboard) + continue; + + if (self->lboards[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_DISABLED; + runtime_event.id = self->lboards[i].id; + runtime_event.value = self->lboards[i].invalid_memref->address; + + lboard->state = RC_LBOARD_STATE_DISABLED; + self->lboards[i].invalid_memref = NULL; + + event_handler(&runtime_event); + continue; + } + + lboard_state = lboard->state; + switch (rc_evaluate_lboard(lboard, &runtime_event.value, peek, ud, unused_L)) + { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (lboard_state != RC_LBOARD_STATE_STARTED) { + self->lboards[i].value = runtime_event.value; + + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_STARTED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + else if (runtime_event.value != self->lboards[i].value) { + self->lboards[i].value = runtime_event.value; + + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_UPDATED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (lboard_state != RC_LBOARD_STATE_CANCELED) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_CANCELED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (lboard_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_TRIGGERED; + runtime_event.id = self->lboards[i].id; + event_handler(&runtime_event); + } + break; + } + } + + if (self->richpresence && self->richpresence->richpresence) + rc_update_richpresence(self->richpresence->richpresence, peek, ud, unused_L); +} + +void rc_runtime_reset(rc_runtime_t* self) { + uint32_t i; + + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].trigger) + rc_reset_trigger(self->triggers[i].trigger); + } + + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].lboard) + rc_reset_lboard(self->lboards[i].lboard); + } + + if (self->richpresence && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); +} + +static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memref_t* memref) { + rc_condition_t* cond; + if (!condset) + return 0; + + for (cond = condset->conditions; cond; cond = cond->next) { + if (rc_operand_is_memref(&cond->operand1) && cond->operand1.value.memref == memref) + return 1; + if (rc_operand_is_memref(&cond->operand2) && cond->operand2.value.memref == memref) + return 1; + } + + return 0; +} + +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { + rc_condset_t* condset; + if (!value) + return 0; + + for (condset = value->conditions; condset; condset = condset->next) { + if (rc_condset_contains_memref(condset, memref)) + return 1; + } + + return 0; +} + +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { + rc_condset_t* condset; + if (!trigger) + return 0; + + if (rc_condset_contains_memref(trigger->requirement, memref)) + return 1; + + for (condset = trigger->alternative; condset; condset = condset->next) { + if (rc_condset_contains_memref(condset, memref)) + return 1; + } + + return 0; +} + +static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref) { + uint32_t i; + + /* disable any achievements dependent on the address */ + for (i = 0; i < self->trigger_count; ++i) { + if (!self->triggers[i].invalid_memref && rc_trigger_contains_memref(self->triggers[i].trigger, memref)) + self->triggers[i].invalid_memref = memref; + } + + /* disable any leaderboards dependent on the address */ + for (i = 0; i < self->lboard_count; ++i) { + if (!self->lboards[i].invalid_memref) { + rc_lboard_t* lboard = self->lboards[i].lboard; + if (lboard) { + if (rc_trigger_contains_memref(&lboard->start, memref)) { + lboard->start.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_trigger_contains_memref(&lboard->cancel, memref)) { + lboard->cancel.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_trigger_contains_memref(&lboard->submit, memref)) { + lboard->submit.state = RC_TRIGGER_STATE_DISABLED; + self->lboards[i].invalid_memref = memref; + } + + if (rc_value_contains_memref(&lboard->value, memref)) + self->lboards[i].invalid_memref = memref; + } + } + } +} + +void rc_runtime_invalidate_address(rc_runtime_t* self, uint32_t address) { + rc_memref_list_t* memref_list = &self->memrefs->memrefs; + do { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + + for (; memref < memref_stop; ++memref) { + if (memref->address == address) { + memref->value.type = RC_VALUE_TYPE_NONE; + rc_runtime_invalidate_memref(self, memref); + } + } + + memref_list = memref_list->next; + } while (memref_list); +} + +void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, + rc_runtime_validate_address_t validate_handler) { + int num_invalid = 0; + rc_memref_list_t* memref_list = &self->memrefs->memrefs; + do { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + + for (; memref < memref_stop; ++memref) { + if (!validate_handler(memref->address)) { + memref->value.type = RC_VALUE_TYPE_NONE; + rc_runtime_invalidate_memref(self, memref); + + ++num_invalid; + } + } + + memref_list = memref_list->next; + } while (memref_list); + + if (num_invalid) { + rc_runtime_event_t runtime_event; + int i; + + for (i = self->trigger_count - 1; i >= 0; --i) { + rc_trigger_t* trigger = self->triggers[i].trigger; + if (trigger && self->triggers[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED; + runtime_event.id = self->triggers[i].id; + runtime_event.value = self->triggers[i].invalid_memref->address; + + trigger->state = RC_TRIGGER_STATE_DISABLED; + self->triggers[i].invalid_memref = NULL; + + event_handler(&runtime_event); + } + } + + for (i = self->lboard_count - 1; i >= 0; --i) { + rc_lboard_t* lboard = self->lboards[i].lboard; + if (lboard && self->lboards[i].invalid_memref) { + runtime_event.type = RC_RUNTIME_EVENT_LBOARD_DISABLED; + runtime_event.id = self->lboards[i].id; + runtime_event.value = self->lboards[i].invalid_memref->address; + + lboard->state = RC_LBOARD_STATE_DISABLED; + self->lboards[i].invalid_memref = NULL; + + event_handler(&runtime_event); + } + } + } +} diff --git a/src/rcheevos/src/rcheevos/runtime_progress.c b/src/rcheevos/src/rcheevos/runtime_progress.c new file mode 100644 index 0000000000..51f7e4b6f9 --- /dev/null +++ b/src/rcheevos/src/rcheevos/runtime_progress.c @@ -0,0 +1,1073 @@ +#include "rc_runtime.h" +#include "rc_internal.h" + +#include "rc_util.h" +#include "../rhash/md5.h" + +#include +#include +#include + +#define RC_RUNTIME_MARKER 0x0A504152 /* RAP\n */ + +#define RC_RUNTIME_CHUNK_MEMREFS 0x4645524D /* MREF */ +#define RC_RUNTIME_CHUNK_VARIABLES 0x53524156 /* VARS */ +#define RC_RUNTIME_CHUNK_ACHIEVEMENT 0x56484341 /* ACHV */ +#define RC_RUNTIME_CHUNK_LEADERBOARD 0x4452424C /* LBRD */ +#define RC_RUNTIME_CHUNK_RICHPRESENCE 0x48434952 /* RICH */ + +#define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ + +#define RC_RUNTIME_MIN_BUFFER_SIZE 4 + 8 + 16 /* RUNTIME_MARKER, CHUNK_DONE, MD5 */ + +typedef struct rc_runtime_progress_t { + const rc_runtime_t* runtime; + + uint32_t offset; + uint8_t* buffer; + uint32_t buffer_size; + + uint32_t chunk_size_offset; +} rc_runtime_progress_t; + +#define assert_chunk_size(expected_size) assert((uint32_t)(progress->offset - progress->chunk_size_offset - 4) == (uint32_t)(expected_size)) + +#define RC_TRIGGER_STATE_UNUPDATED 0x7F + +#define RC_MEMREF_FLAG_CHANGED_THIS_FRAME 0x00010000 + +#define RC_VAR_FLAG_HAS_COND_DATA 0x01000000 + +#define RC_COND_FLAG_IS_TRUE_MASK 0x00000003 +#define RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF 0x00010000 +#define RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME 0x00020000 +#define RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF 0x00100000 +#define RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME 0x00200000 + +static void rc_runtime_progress_write_uint(rc_runtime_progress_t* progress, uint32_t value) +{ + if (progress->buffer) { + progress->buffer[progress->offset + 0] = value & 0xFF; value >>= 8; + progress->buffer[progress->offset + 1] = value & 0xFF; value >>= 8; + progress->buffer[progress->offset + 2] = value & 0xFF; value >>= 8; + progress->buffer[progress->offset + 3] = value & 0xFF; + } + + progress->offset += 4; +} + +static uint32_t rc_runtime_progress_read_uint(rc_runtime_progress_t* progress) +{ + uint32_t value = progress->buffer[progress->offset + 0] | + (progress->buffer[progress->offset + 1] << 8) | + (progress->buffer[progress->offset + 2] << 16) | + (progress->buffer[progress->offset + 3] << 24); + + progress->offset += 4; + return value; +} + +static void rc_runtime_progress_write_md5(rc_runtime_progress_t* progress, uint8_t* md5) +{ + if (progress->buffer) + memcpy(&progress->buffer[progress->offset], md5, 16); + + progress->offset += 16; +} + +static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, uint8_t* md5) +{ + int result = 0; + if (progress->buffer) + result = (memcmp(&progress->buffer[progress->offset], md5, 16) == 0); + + progress->offset += 16; + + return result; +} + +static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, uint32_t chunk_id) +{ + rc_runtime_progress_write_uint(progress, chunk_id); + + progress->chunk_size_offset = progress->offset; + + progress->offset += 4; +} + +static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) +{ + uint32_t length; + uint32_t offset; + + progress->offset = (progress->offset + 3) & ~0x03; /* align to 4 byte boundary */ + + if (progress->buffer) { + /* ignore chunk size field when calculating chunk size */ + length = (uint32_t)(progress->offset - progress->chunk_size_offset - 4); + + /* temporarily update the write pointer to write the chunk size field */ + offset = progress->offset; + progress->offset = progress->chunk_size_offset; + rc_runtime_progress_write_uint(progress, length); + progress->offset = offset; + } +} + +static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime) +{ + memset(progress, 0, sizeof(rc_runtime_progress_t)); + progress->runtime = runtime; +} + +#define RC_RUNTIME_SERIALIZED_MEMREF_SIZE 16 /* 4x uint: address, flags, value, prior */ + +static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) +{ + uint32_t count = rc_memrefs_count_memrefs(progress->runtime->memrefs); + if (count == 0) + return RC_OK; + + if (progress->offset + 8 + count * RC_RUNTIME_SERIALIZED_MEMREF_SIZE > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_MEMREFS); + + if (!progress->buffer) { + progress->offset += count * RC_RUNTIME_SERIALIZED_MEMREF_SIZE; + } + else { + uint32_t flags = 0; + const rc_memref_list_t* memref_list = &progress->runtime->memrefs->memrefs; + const rc_memref_t* memref; + + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref_end; + + memref = memref_list->items; + memref_end = memref + memref_list->count; + for (; memref < memref_end; ++memref) { + flags = memref->value.size; + if (memref->value.changed) + flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + + rc_runtime_progress_write_uint(progress, memref->address); + rc_runtime_progress_write_uint(progress, flags); + rc_runtime_progress_write_uint(progress, memref->value.value); + rc_runtime_progress_write_uint(progress, memref->value.prior); + } + } + } + + assert_chunk_size(count * RC_RUNTIME_SERIALIZED_MEMREF_SIZE); + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static void rc_runtime_progress_update_modified_memrefs(rc_runtime_progress_t* progress) +{ + rc_typed_value_t value, prior_value, modifier, prior_modifier; + rc_modified_memref_list_t* modified_memref_list; + rc_modified_memref_t* modified_memref; + rc_operand_t prior_parent_operand, prior_modifier_operand; + rc_memref_t prior_parent_memref, prior_modifier_memref; + + modified_memref_list = &progress->runtime->memrefs->modified_memrefs; + for (; modified_memref_list; modified_memref_list = modified_memref_list->next) { + const rc_modified_memref_t* modified_memref_end; + modified_memref = modified_memref_list->items; + modified_memref_end = modified_memref + modified_memref_list->count; + for (; modified_memref < modified_memref_end; ++modified_memref) { + modified_memref->memref.value.changed = 0; + + /* indirect memref values are stored in conditions */ + if (modified_memref->modifier_type == RC_OPERATOR_INDIRECT_READ) + continue; + + /* non-indirect memref values can be reconstructed from the parents */ + memcpy(&prior_parent_operand, &modified_memref->parent, sizeof(prior_parent_operand)); + if (rc_operand_is_memref(&prior_parent_operand)) { + memcpy(&prior_parent_memref, modified_memref->parent.value.memref, sizeof(prior_parent_memref)); + prior_parent_memref.value.value = prior_parent_memref.value.prior; + modified_memref->memref.value.changed |= prior_parent_memref.value.changed; + prior_parent_operand.value.memref = &prior_parent_memref; + } + + memcpy(&prior_modifier_operand, &modified_memref->modifier, sizeof(prior_modifier_operand)); + if (rc_operand_is_memref(&prior_modifier_operand)) { + memcpy(&prior_modifier_memref, modified_memref->modifier.value.memref, sizeof(prior_modifier_memref)); + prior_modifier_memref.value.value = prior_modifier_memref.value.prior; + modified_memref->memref.value.changed |= prior_modifier_memref.value.changed; + prior_modifier_operand.value.memref = &prior_modifier_memref; + } + + rc_evaluate_operand(&value, &modified_memref->parent, NULL); + rc_evaluate_operand(&modifier, &modified_memref->modifier, NULL); + rc_evaluate_operand(&prior_value, &prior_parent_operand, NULL); + rc_evaluate_operand(&prior_modifier, &prior_modifier_operand, NULL); + + if (modified_memref->modifier_type == RC_OPERATOR_SUB_PARENT) { + rc_typed_value_negate(&value); + rc_typed_value_add(&value, &modifier); + + rc_typed_value_negate(&prior_value); + rc_typed_value_add(&prior_value, &prior_modifier); + } + else { + rc_typed_value_combine(&value, &modifier, modified_memref->modifier_type); + rc_typed_value_combine(&prior_value, &prior_modifier, modified_memref->modifier_type); + } + + rc_typed_value_convert(&value, modified_memref->memref.value.type); + modified_memref->memref.value.value = value.value.u32; + + rc_typed_value_convert(&prior_value, modified_memref->memref.value.type); + modified_memref->memref.value.prior = prior_value.value.u32; + } + } +} + +static int rc_runtime_progress_read_memrefs(rc_runtime_progress_t* progress) +{ + uint32_t entries; + uint32_t address, flags, value, prior; + uint8_t size; + rc_memref_list_t* unmatched_memref_list = &progress->runtime->memrefs->memrefs; + rc_memref_t* first_unmatched_memref = unmatched_memref_list->items; + rc_memref_t* memref; + + /* re-read the chunk size to determine how many memrefs are present */ + progress->offset -= 4; + entries = rc_runtime_progress_read_uint(progress) / RC_RUNTIME_SERIALIZED_MEMREF_SIZE; + + while (entries != 0) { + address = rc_runtime_progress_read_uint(progress); + flags = rc_runtime_progress_read_uint(progress); + value = rc_runtime_progress_read_uint(progress); + prior = rc_runtime_progress_read_uint(progress); + + size = flags & 0xFF; + + memref = first_unmatched_memref; + if (memref->address == address && memref->value.size == size) { + memref->value.value = value; + memref->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + memref->value.prior = prior; + + first_unmatched_memref++; + if (first_unmatched_memref >= unmatched_memref_list->items + unmatched_memref_list->count) { + unmatched_memref_list = unmatched_memref_list->next; + if (!unmatched_memref_list) + break; + first_unmatched_memref = unmatched_memref_list->items; + } + } + else { + rc_memref_list_t* memref_list = unmatched_memref_list; + do { + ++memref; + if (memref >= memref_list->items + memref_list->count) { + memref_list = memref_list->next; + if (!memref_list) + break; + + memref = memref_list->items; + } + + if (memref->address == address && memref->value.size == size) { + memref->value.value = value; + memref->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + memref->value.prior = prior; + break; + } + + } while (1); + } + + --entries; + } + + rc_runtime_progress_update_modified_memrefs(progress); + + return RC_OK; +} + +static int rc_runtime_progress_is_indirect_memref(rc_operand_t* oper) +{ + switch (oper->type) + { + case RC_OPERAND_CONST: + case RC_OPERAND_FP: + case RC_OPERAND_RECALL: + case RC_OPERAND_FUNC: + return 0; + + default: + if (oper->value.memref->value.memref_type != RC_MEMREF_TYPE_MODIFIED_MEMREF) + return 0; + + return ((const rc_modified_memref_t*)oper->value.memref)->modifier_type == RC_OPERATOR_INDIRECT_READ; + } +} + +static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) +{ + rc_condition_t* cond; + uint32_t flags; + + if (progress->offset + 4 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, condset->is_paused); + + cond = condset->conditions; + while (cond) { + flags = (cond->is_true & RC_COND_FLAG_IS_TRUE_MASK); + + if (rc_runtime_progress_is_indirect_memref(&cond->operand1)) { + flags |= RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF; + if (cond->operand1.value.memref->value.changed) + flags |= RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME; + } + + if (rc_runtime_progress_is_indirect_memref(&cond->operand2)) { + flags |= RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF; + if (cond->operand2.value.memref->value.changed) + flags |= RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME; + } + + if (progress->offset + 8 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, cond->current_hits); + rc_runtime_progress_write_uint(progress, flags); + + if (flags & RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF) { + if (progress->offset + 8 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, cond->operand1.value.memref->value.value); + rc_runtime_progress_write_uint(progress, cond->operand1.value.memref->value.prior); + } + + if (flags & RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF) { + if (progress->offset + 8 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, cond->operand2.value.memref->value.value); + rc_runtime_progress_write_uint(progress, cond->operand2.value.memref->value.prior); + } + + cond = cond->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_condset_t* condset) +{ + rc_condition_t* cond; + uint32_t flags; + + condset->is_paused = (char)rc_runtime_progress_read_uint(progress); + + cond = condset->conditions; + while (cond) { + cond->current_hits = rc_runtime_progress_read_uint(progress); + flags = rc_runtime_progress_read_uint(progress); + + cond->is_true = (flags & RC_COND_FLAG_IS_TRUE_MASK); + + if (flags & RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF) { + if (!rc_operand_is_memref(&cond->operand1)) /* this should never happen, but better safe than sorry */ + return RC_INVALID_STATE; + + cond->operand1.value.memref->value.value = rc_runtime_progress_read_uint(progress); + cond->operand1.value.memref->value.prior = rc_runtime_progress_read_uint(progress); + cond->operand1.value.memref->value.changed = (flags & RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME) ? 1 : 0; + } + + if (flags & RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF) { + if (!rc_operand_is_memref(&cond->operand2)) /* this should never happen, but better safe than sorry */ + return RC_INVALID_STATE; + + cond->operand2.value.memref->value.value = rc_runtime_progress_read_uint(progress); + cond->operand2.value.memref->value.prior = rc_runtime_progress_read_uint(progress); + cond->operand2.value.memref->value.changed = (flags & RC_COND_FLAG_OPERAND2_MEMREF_CHANGED_THIS_FRAME) ? 1 : 0; + } + + cond = cond->next; + } + + return RC_OK; +} + +static uint32_t rc_runtime_progress_should_serialize_variable_condset(const rc_condset_t* conditions) +{ + const rc_condition_t* condition; + + /* predetermined presence of pause flag - must serialize */ + if (conditions->has_pause) + return RC_VAR_FLAG_HAS_COND_DATA; + + /* if any conditions has required hits, must serialize */ + /* ASSERT: Measured with comparison and no explicit target will set hit target to 0xFFFFFFFF */ + for (condition = conditions->conditions; condition; condition = condition->next) { + if (condition->required_hits > 0) + return RC_VAR_FLAG_HAS_COND_DATA; + } + + /* can safely be reset without affecting behavior */ + return 0; +} + +static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, const rc_value_t* variable) +{ + uint32_t flags; + + if (progress->offset + 12 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + flags = rc_runtime_progress_should_serialize_variable_condset(variable->conditions); + if (variable->value.changed) + flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + + rc_runtime_progress_write_uint(progress, flags); + rc_runtime_progress_write_uint(progress, variable->value.value); + rc_runtime_progress_write_uint(progress, variable->value.prior); + + if (flags & RC_VAR_FLAG_HAS_COND_DATA) { + int result = rc_runtime_progress_write_condset(progress, variable->conditions); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) +{ + uint32_t count; + const rc_value_t* value; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + value = progress->runtime->richpresence->richpresence->values; + count = rc_count_values(value); + if (count == 0) + return RC_OK; + + /* header + count + count(djb2,flags,value,prior,?cond) */ + if (progress->offset + 8 + 4 + count * 16 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_VARIABLES); + rc_runtime_progress_write_uint(progress, count); + + for (; value; value = value->next) { + const uint32_t djb2 = rc_djb2(value->name); + if (progress->offset + 16 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, djb2); + + result = rc_runtime_progress_write_variable(progress, value); + if (result != RC_OK) + return result; + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_variable(rc_runtime_progress_t* progress, rc_value_t* variable) +{ + uint32_t flags = rc_runtime_progress_read_uint(progress); + variable->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + variable->value.value = rc_runtime_progress_read_uint(progress); + variable->value.prior = rc_runtime_progress_read_uint(progress); + + if (flags & RC_VAR_FLAG_HAS_COND_DATA) { + int result = rc_runtime_progress_read_condset(progress, variable->conditions); + if (result != RC_OK) + return result; + } + else { + rc_reset_condset(variable->conditions); + } + + return RC_OK; +} + +static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) +{ + struct rc_pending_value_t + { + rc_value_t* variable; + uint32_t djb2; + }; + struct rc_pending_value_t local_pending_variables[32]; + struct rc_pending_value_t* pending_variables; + rc_value_t* value; + uint32_t count, serialized_count; + int result; + int32_t i; + + serialized_count = rc_runtime_progress_read_uint(progress); + if (serialized_count == 0) + return RC_OK; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + value = progress->runtime->richpresence->richpresence->values; + count = rc_count_values(value); + if (count == 0) + return RC_OK; + + if (count <= sizeof(local_pending_variables) / sizeof(local_pending_variables[0])) { + pending_variables = local_pending_variables; + } + else { + pending_variables = (struct rc_pending_value_t*)malloc(count * sizeof(struct rc_pending_value_t)); + if (pending_variables == NULL) + return RC_OUT_OF_MEMORY; + } + + i = (int32_t)count; + for (; value; value = value->next) { + --i; + pending_variables[i].variable = value; + pending_variables[i].djb2 = rc_djb2(value->name); + } + + result = RC_OK; + for (; serialized_count > 0 && result == RC_OK; --serialized_count) { + uint32_t djb2 = rc_runtime_progress_read_uint(progress); + for (i = (int32_t)count - 1; i >= 0; --i) { + if (pending_variables[i].djb2 == djb2) { + value = pending_variables[i].variable; + result = rc_runtime_progress_read_variable(progress, value); + if (result == RC_OK) { + if (i < (int32_t)count - 1) + memcpy(&pending_variables[i], &pending_variables[count - 1], sizeof(struct rc_pending_value_t)); + count--; + } + break; + } + } + } + + /* VS raises a C6385 warning here because it thinks count can exceed the size of the local_pending_variables array. + * When count is larger, pending_variables points to allocated memory, so the warning is wrong. */ +#if defined (_MSC_VER) + #pragma warning(push) + #pragma warning(disable:6385) +#endif + while (count > 0) + rc_reset_value(pending_variables[--count].variable); +#if defined (_MSC_VER) + #pragma warning(pop) +#endif + + if (pending_variables != local_pending_variables) + free(pending_variables); + + return result; +} + +static int rc_runtime_progress_write_trigger(rc_runtime_progress_t* progress, const rc_trigger_t* trigger) +{ + rc_condset_t* condset; + int result; + + rc_runtime_progress_write_uint(progress, trigger->state); + rc_runtime_progress_write_uint(progress, trigger->measured_value); + + if (trigger->requirement) { + result = rc_runtime_progress_write_condset(progress, trigger->requirement); + if (result != RC_OK) + return result; + } + + condset = trigger->alternative; + while (condset) { + result = rc_runtime_progress_write_condset(progress, condset); + if (result != RC_OK) + return result; + + condset = condset->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_trigger(rc_runtime_progress_t* progress, rc_trigger_t* trigger) +{ + rc_condset_t* condset; + int result; + + trigger->state = (char)rc_runtime_progress_read_uint(progress); + trigger->measured_value = rc_runtime_progress_read_uint(progress); + + if (trigger->requirement) { + result = rc_runtime_progress_read_condset(progress, trigger->requirement); + if (result != RC_OK) + return result; + } + + condset = trigger->alternative; + while (condset) { + result = rc_runtime_progress_read_condset(progress, condset); + if (result != RC_OK) + return result; + + condset = condset->next; + } + + return RC_OK; +} + +static int rc_runtime_progress_write_achievements(rc_runtime_progress_t* progress) +{ + uint32_t i; + int initial_offset = 0; + int result; + + for (i = 0; i < progress->runtime->trigger_count; ++i) { + rc_runtime_trigger_t* runtime_trigger = &progress->runtime->triggers[i]; + if (!runtime_trigger->trigger) + continue; + + /* don't store state for inactive or triggered achievements */ + if (!rc_trigger_state_active(runtime_trigger->trigger->state)) + continue; + + if (!progress->buffer) { + if (runtime_trigger->serialized_size) { + progress->offset += runtime_trigger->serialized_size; + continue; + } + + initial_offset = progress->offset; + } else { + if (progress->offset + runtime_trigger->serialized_size > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + } + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_ACHIEVEMENT); + rc_runtime_progress_write_uint(progress, runtime_trigger->id); + rc_runtime_progress_write_md5(progress, runtime_trigger->md5); + + result = rc_runtime_progress_write_trigger(progress, runtime_trigger->trigger); + if (result != RC_OK) + return result; + + if (runtime_trigger->serialized_size) { + /* runtime_trigger->serialized_size includes the header */ + assert_chunk_size(runtime_trigger->serialized_size - 8); + } + + rc_runtime_progress_end_chunk(progress); + + if (!progress->buffer) + runtime_trigger->serialized_size = progress->offset - initial_offset; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_achievement(rc_runtime_progress_t* progress) +{ + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; + + for (i = 0; i < progress->runtime->trigger_count; ++i) { + rc_runtime_trigger_t* runtime_trigger = &progress->runtime->triggers[i]; + if (runtime_trigger->id == id && runtime_trigger->trigger != NULL) { + /* ignore triggered and waiting achievements */ + if (runtime_trigger->trigger->state == RC_TRIGGER_STATE_UNUPDATED) { + /* only update state if definition hasn't changed (md5 matches) */ + if (rc_runtime_progress_match_md5(progress, runtime_trigger->md5)) + return rc_runtime_progress_read_trigger(progress, runtime_trigger->trigger); + break; + } + } + } + + return RC_OK; +} + +static int rc_runtime_progress_write_leaderboards(rc_runtime_progress_t* progress) +{ + uint32_t i; + uint32_t flags; + int initial_offset = 0; + int result; + + for (i = 0; i < progress->runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &progress->runtime->lboards[i]; + if (!runtime_lboard->lboard) + continue; + + /* don't store state for inactive leaderboards */ + if (!rc_lboard_state_active(runtime_lboard->lboard->state)) + continue; + + if (!progress->buffer) { + if (runtime_lboard->serialized_size) { + progress->offset += runtime_lboard->serialized_size; + continue; + } + + initial_offset = progress->offset; + } else { + if (progress->offset + runtime_lboard->serialized_size > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + } + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_LEADERBOARD); + rc_runtime_progress_write_uint(progress, runtime_lboard->id); + rc_runtime_progress_write_md5(progress, runtime_lboard->md5); + + flags = runtime_lboard->lboard->state; + rc_runtime_progress_write_uint(progress, flags); + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->start); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->submit); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_trigger(progress, &runtime_lboard->lboard->cancel); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_write_variable(progress, &runtime_lboard->lboard->value); + if (result != RC_OK) + return result; + + if (runtime_lboard->serialized_size) { + /* runtime_lboard->serialized_size includes the header */ + assert_chunk_size(runtime_lboard->serialized_size - 8); + } + + rc_runtime_progress_end_chunk(progress); + + if (!progress->buffer) + runtime_lboard->serialized_size = progress->offset - initial_offset; + } + + return RC_OK; +} + +static int rc_runtime_progress_read_leaderboard(rc_runtime_progress_t* progress) +{ + uint32_t id = rc_runtime_progress_read_uint(progress); + uint32_t i; + int result; + + for (i = 0; i < progress->runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &progress->runtime->lboards[i]; + if (runtime_lboard->id == id && runtime_lboard->lboard != NULL) { + /* ignore triggered and waiting achievements */ + if (runtime_lboard->lboard->state == RC_TRIGGER_STATE_UNUPDATED) { + /* only update state if definition hasn't changed (md5 matches) */ + if (rc_runtime_progress_match_md5(progress, runtime_lboard->md5)) { + uint32_t flags = rc_runtime_progress_read_uint(progress); + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->start); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->submit); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_trigger(progress, &runtime_lboard->lboard->cancel); + if (result != RC_OK) + return result; + + result = rc_runtime_progress_read_variable(progress, &runtime_lboard->lboard->value); + if (result != RC_OK) + return result; + + runtime_lboard->lboard->state = (char)(flags & 0x7F); + } + break; + } + } + } + + return RC_OK; +} + +static int rc_runtime_progress_write_rich_presence(rc_runtime_progress_t* progress) +{ + const rc_richpresence_display_t* display; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + /* if there are no conditional display strings, there's nothing to capture */ + display = progress->runtime->richpresence->richpresence->first_display; + if (!display->next) + return RC_OK; + + if (progress->offset + 8 + 16 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_RICHPRESENCE); + rc_runtime_progress_write_md5(progress, progress->runtime->richpresence->md5); + + for (; display->next; display = display->next) { + result = rc_runtime_progress_write_trigger(progress, &display->trigger); + if (result != RC_OK) + return result; + } + + rc_runtime_progress_end_chunk(progress); + return RC_OK; +} + +static int rc_runtime_progress_read_rich_presence(rc_runtime_progress_t* progress) +{ + rc_richpresence_display_t* display; + int result; + + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + if (!rc_runtime_progress_match_md5(progress, progress->runtime->richpresence->md5)) { + rc_reset_richpresence_triggers(progress->runtime->richpresence->richpresence); + return RC_OK; + } + + display = progress->runtime->richpresence->richpresence->first_display; + for (; display->next; display = display->next) { + result = rc_runtime_progress_read_trigger(progress, &display->trigger); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +static int rc_runtime_progress_serialize_internal(rc_runtime_progress_t* progress) +{ + md5_state_t state; + uint8_t md5[16]; + int result; + + if (progress->buffer_size < RC_RUNTIME_MIN_BUFFER_SIZE) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, RC_RUNTIME_MARKER); + + if ((result = rc_runtime_progress_write_memrefs(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_variables(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_achievements(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_leaderboards(progress)) != RC_OK) + return result; + + if ((result = rc_runtime_progress_write_rich_presence(progress)) != RC_OK) + return result; + + if (progress->offset + 8 + 16 > progress->buffer_size) + return RC_INSUFFICIENT_BUFFER; + + rc_runtime_progress_write_uint(progress, RC_RUNTIME_CHUNK_DONE); + rc_runtime_progress_write_uint(progress, 16); + + if (progress->buffer) { + md5_init(&state); + md5_append(&state, progress->buffer, progress->offset); + md5_finish(&state, md5); + } + + rc_runtime_progress_write_md5(progress, md5); + + return RC_OK; +} + +uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, void* unused_L) +{ + rc_runtime_progress_t progress; + int result; + + (void)unused_L; + + rc_runtime_progress_init(&progress, runtime); + progress.buffer_size = 0xFFFFFFFF; + + result = rc_runtime_progress_serialize_internal(&progress); + if (result != RC_OK) + return result; + + return progress.offset; +} + +int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, void* unused_L) +{ + return rc_runtime_serialize_progress_sized((uint8_t*)buffer, 0xFFFFFFFF, runtime, unused_L); +} + +int rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, void* unused_L) +{ + rc_runtime_progress_t progress; + + (void)unused_L; + + if (!buffer) + return RC_INVALID_STATE; + + rc_runtime_progress_init(&progress, runtime); + progress.buffer = (uint8_t*)buffer; + progress.buffer_size = buffer_size; + + return rc_runtime_progress_serialize_internal(&progress); +} + +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, void* unused_L) +{ + return rc_runtime_deserialize_progress_sized(runtime, serialized, 0xFFFFFFFF, unused_L); +} + +int rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* serialized, uint32_t serialized_size, void* unused_L) +{ + rc_runtime_progress_t progress; + md5_state_t state; + uint8_t md5[16]; + uint32_t chunk_id; + uint32_t chunk_size; + uint32_t next_chunk_offset; + uint32_t i; + int seen_rich_presence = 0; + int result = RC_OK; + + (void)unused_L; + + if (!serialized || serialized_size < RC_RUNTIME_MIN_BUFFER_SIZE) { + rc_runtime_reset(runtime); + return RC_INSUFFICIENT_BUFFER; + } + + rc_runtime_progress_init(&progress, runtime); + progress.buffer = (uint8_t*)serialized; + + if (rc_runtime_progress_read_uint(&progress) != RC_RUNTIME_MARKER) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + + for (i = 0; i < runtime->trigger_count; ++i) { + rc_runtime_trigger_t* runtime_trigger = &runtime->triggers[i]; + if (runtime_trigger->trigger) { + /* don't update state for inactive or triggered achievements */ + if (rc_trigger_state_active(runtime_trigger->trigger->state)) { + /* mark active achievements as unupdated. anything that's still unupdated + * after deserializing the progress will be reset to waiting */ + runtime_trigger->trigger->state = RC_TRIGGER_STATE_UNUPDATED; + } + } + } + + for (i = 0; i < runtime->lboard_count; ++i) { + rc_runtime_lboard_t* runtime_lboard = &runtime->lboards[i]; + if (runtime_lboard->lboard) { + /* don't update state for inactive or triggered achievements */ + if (rc_lboard_state_active(runtime_lboard->lboard->state)) { + /* mark active achievements as unupdated. anything that's still unupdated + * after deserializing the progress will be reset to waiting */ + runtime_lboard->lboard->state = RC_TRIGGER_STATE_UNUPDATED; + } + } + } + + do { + if (progress.offset + 8 >= serialized_size) { + result = RC_INSUFFICIENT_BUFFER; + break; + } + + chunk_id = rc_runtime_progress_read_uint(&progress); + chunk_size = rc_runtime_progress_read_uint(&progress); + next_chunk_offset = progress.offset + chunk_size; + + if (next_chunk_offset > serialized_size) { + result = RC_INSUFFICIENT_BUFFER; + break; + } + + switch (chunk_id) { + case RC_RUNTIME_CHUNK_MEMREFS: + result = rc_runtime_progress_read_memrefs(&progress); + break; + + case RC_RUNTIME_CHUNK_VARIABLES: + result = rc_runtime_progress_read_variables(&progress); + break; + + case RC_RUNTIME_CHUNK_ACHIEVEMENT: + result = rc_runtime_progress_read_achievement(&progress); + break; + + case RC_RUNTIME_CHUNK_LEADERBOARD: + result = rc_runtime_progress_read_leaderboard(&progress); + break; + + case RC_RUNTIME_CHUNK_RICHPRESENCE: + seen_rich_presence = 1; + result = rc_runtime_progress_read_rich_presence(&progress); + break; + + case RC_RUNTIME_CHUNK_DONE: + md5_init(&state); + md5_append(&state, progress.buffer, progress.offset); + md5_finish(&state, md5); + if (!rc_runtime_progress_match_md5(&progress, md5)) + result = RC_INVALID_STATE; + break; + + default: + if (chunk_size & 0xFFFF0000) + result = RC_INVALID_STATE; /* assume unknown chunk > 64KB is invalid */ + break; + } + + progress.offset = next_chunk_offset; + } while (result == RC_OK && chunk_id != RC_RUNTIME_CHUNK_DONE); + + if (result != RC_OK) { + rc_runtime_reset(runtime); + } + else { + for (i = 0; i < runtime->trigger_count; ++i) { + rc_trigger_t* trigger = runtime->triggers[i].trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_UNUPDATED) + rc_reset_trigger(trigger); + } + + for (i = 0; i < runtime->lboard_count; ++i) { + rc_lboard_t* lboard = runtime->lboards[i].lboard; + if (lboard && lboard->state == RC_TRIGGER_STATE_UNUPDATED) + rc_reset_lboard(lboard); + } + + if (!seen_rich_presence && runtime->richpresence && runtime->richpresence->richpresence) + rc_reset_richpresence_triggers(runtime->richpresence->richpresence); + } + + return result; +} diff --git a/src/rcheevos/src/rcheevos/trigger.c b/src/rcheevos/src/rcheevos/trigger.c new file mode 100644 index 0000000000..063a14f5c9 --- /dev/null +++ b/src/rcheevos/src/rcheevos/trigger.c @@ -0,0 +1,344 @@ +#include "rc_internal.h" + +#include +#include /* memset */ + +void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_t** next; + const char* aux; + + aux = *memaddr; + next = &self->alternative; + + /* reset in case multiple triggers are parsed by the same parse_state */ + parse->measured_target = 0; + parse->has_required_hits = 0; + parse->measured_as_percent = 0; + + if (*aux == 's' || *aux == 'S') { + self->requirement = NULL; + } + else { + self->requirement = rc_parse_condset(&aux, parse); + + if (parse->offset < 0) + return; + + self->requirement->next = NULL; + } + + while (*aux == 's' || *aux == 'S') { + aux++; + *next = rc_parse_condset(&aux, parse); + + if (parse->offset < 0) { + return; + } + + next = &(*next)->next; + } + + *next = NULL; + *memaddr = aux; + + self->measured_target = parse->measured_target; + self->measured_value = parse->measured_target ? RC_MEASURED_UNKNOWN : 0; + self->measured_as_percent = parse->measured_as_percent; + self->state = RC_TRIGGER_STATE_WAITING; + self->has_hits = 0; + self->has_memrefs = 0; +} + +int rc_trigger_size(const char* memaddr) { + rc_trigger_with_memrefs_t* trigger; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); + + trigger = RC_ALLOC(rc_trigger_with_memrefs_t, &preparse.parse); + rc_parse_trigger_internal(&trigger->trigger, &memaddr, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); + + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; +} + +rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx) { + rc_trigger_with_memrefs_t* trigger; + rc_preparse_state_t preparse; + const char* preparse_memaddr = memaddr; + + (void)unused_L; + (void)unused_funcs_idx; + + if (!buffer || !memaddr) + return NULL; + + /* first pass : determine how many memrefs are needed */ + rc_init_preparse_state(&preparse); + trigger = RC_ALLOC(rc_trigger_with_memrefs_t, &preparse.parse); + rc_parse_trigger_internal(&trigger->trigger, &preparse_memaddr, &preparse.parse); + + /* allocate the trigger and memrefs */ + rc_reset_parse_state(&preparse.parse, buffer); + trigger = RC_ALLOC(rc_trigger_with_memrefs_t, &preparse.parse); + rc_preparse_alloc_memrefs(&trigger->memrefs, &preparse); + + /* parse the trigger */ + rc_parse_trigger_internal(&trigger->trigger, &memaddr, &preparse.parse); + trigger->trigger.has_memrefs = 1; + + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &trigger->trigger : NULL; +} + +int rc_trigger_state_active(int state) +{ + switch (state) + { + case RC_TRIGGER_STATE_DISABLED: + case RC_TRIGGER_STATE_INACTIVE: + case RC_TRIGGER_STATE_TRIGGERED: + return 0; + + default: + return 1; + } +} + +static int rc_condset_is_measured_from_hitcount(const rc_condset_t* condset, uint32_t measured_value) +{ + const rc_condition_t* condition; + for (condition = condset->conditions; condition; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED && condition->required_hits && + condition->current_hits == measured_value) { + return 1; + } + } + + return 0; +} + +static void rc_reset_trigger_hitcounts(rc_trigger_t* self) { + rc_condset_t* condset; + + if (self->requirement) { + rc_reset_condset(self->requirement); + } + + condset = self->alternative; + + while (condset) { + rc_reset_condset(condset); + condset = condset->next; + } +} + +static void rc_update_trigger_memrefs(rc_trigger_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_trigger_with_memrefs_t* trigger = (rc_trigger_with_memrefs_t*)self; + rc_update_memref_values(&trigger->memrefs, peek, ud); + } +} + +rc_memrefs_t* rc_trigger_get_memrefs(rc_trigger_t* self) { + if (self->has_memrefs) { + rc_trigger_with_memrefs_t* trigger = (rc_trigger_with_memrefs_t*)self; + return &trigger->memrefs; + } + + return NULL; +} + +int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, void* unused_L) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + rc_typed_value_t measured_value; + int measured_from_hits = 0; + int ret; + char is_paused; + char is_primed; + + (void)unused_L; + + switch (self->state) + { + case RC_TRIGGER_STATE_TRIGGERED: + /* previously triggered. do nothing - return INACTIVE so caller doesn't think it triggered again */ + return RC_TRIGGER_STATE_INACTIVE; + + case RC_TRIGGER_STATE_DISABLED: + /* unsupported. do nothing - return INACTIVE */ + return RC_TRIGGER_STATE_INACTIVE; + + case RC_TRIGGER_STATE_INACTIVE: + /* not yet active. update the memrefs so deltas are correct when it becomes active, then return INACTIVE */ + rc_update_trigger_memrefs(self, peek, ud); + return RC_TRIGGER_STATE_INACTIVE; + + default: + break; + } + + /* update the memory references */ + rc_update_trigger_memrefs(self, peek, ud); + + /* process the trigger */ + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = ud; + + measured_value.type = RC_VALUE_TYPE_NONE; + + if (self->requirement != NULL) { + ret = rc_test_condset(self->requirement, &eval_state); + is_paused = eval_state.is_paused; + is_primed = eval_state.is_primed; + + if (eval_state.measured_value.type != RC_VALUE_TYPE_NONE) { + memcpy(&measured_value, &eval_state.measured_value, sizeof(measured_value)); + measured_from_hits = eval_state.measured_from_hits; + } + } else { + ret = 1; + is_paused = 0; + is_primed = 1; + } + + condset = self->alternative; + if (condset) { + int sub = 0; + char sub_paused = 1; + char sub_primed = 0; + + do { + sub |= rc_test_condset(condset, &eval_state); + sub_paused &= eval_state.is_paused; + sub_primed |= eval_state.is_primed; + + if (eval_state.measured_value.type != RC_VALUE_TYPE_NONE) { + /* if no previous Measured value was captured, or the new one is greater, keep the new one */ + if (measured_value.type == RC_VALUE_TYPE_NONE || + rc_typed_value_compare(&eval_state.measured_value, &measured_value, RC_OPERATOR_GT)) { + memcpy(&measured_value, &eval_state.measured_value, sizeof(measured_value)); + measured_from_hits = eval_state.measured_from_hits; + } + } + + condset = condset->next; + } while (condset); + + /* to trigger, the core must be true and at least one alt must be true */ + ret &= sub; + is_primed &= sub_primed; + + /* if the core is not paused, all alts must be paused to count as a paused trigger */ + is_paused |= sub_paused; + } + + if (is_paused) { + /* if the trigger is fully paused, ignore any updates to the measured value */ + } + else if (measured_value.type == RC_VALUE_TYPE_NONE) { + /* if a measured value was not captured, keep the old value (it's possible to pause + * an alt that is generating the measured value without fully pausing the trigger) */ + } + else { + rc_typed_value_convert(&measured_value, RC_VALUE_TYPE_UNSIGNED); + self->measured_value = measured_value.value.u32; + } + + /* if any ResetIf condition was true, reset the hit counts */ + if (eval_state.was_reset) { + /* if the measured value came from a hit count, reset it. do this before calling + * rc_reset_trigger_hitcounts in case we need to call rc_condset_is_measured_from_hitcount */ + if (measured_from_hits) { + self->measured_value = 0; + } + else if (is_paused && self->measured_value) { + /* if the measured value is in a paused group, measured_from_hits won't have been set. + * attempt to determine if it should have been */ + if (self->requirement && self->requirement->is_paused && + rc_condset_is_measured_from_hitcount(self->requirement, self->measured_value)) { + self->measured_value = 0; + } + else { + for (condset = self->alternative; condset; condset = condset->next) { + if (condset->is_paused && rc_condset_is_measured_from_hitcount(condset, self->measured_value)) { + self->measured_value = 0; + break; + } + } + } + } + + rc_reset_trigger_hitcounts(self); + + /* if there were hit counts to clear, return RESET, but don't change the state */ + if (self->has_hits) { + self->has_hits = 0; + + /* cannot be PRIMED while ResetIf is true */ + if (self->state == RC_TRIGGER_STATE_PRIMED) + self->state = RC_TRIGGER_STATE_ACTIVE; + + return RC_TRIGGER_STATE_RESET; + } + + /* any hits that were tallied were just reset */ + eval_state.has_hits = 0; + is_primed = 0; + } + else if (ret) { + /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ + if (self->state == RC_TRIGGER_STATE_WAITING) { + rc_reset_trigger(self); + self->has_hits = 0; + return RC_TRIGGER_STATE_WAITING; + } + + /* trigger was triggered */ + self->state = RC_TRIGGER_STATE_TRIGGERED; + return RC_TRIGGER_STATE_TRIGGERED; + } + + /* did not trigger this frame - update the information we'll need for next time */ + self->has_hits = eval_state.has_hits; + + if (is_paused) { + self->state = RC_TRIGGER_STATE_PAUSED; + } + else if (is_primed) { + self->state = RC_TRIGGER_STATE_PRIMED; + } + else { + self->state = RC_TRIGGER_STATE_ACTIVE; + } + + /* if an individual condition was reset, notify the caller */ + if (eval_state.was_cond_reset) + return RC_TRIGGER_STATE_RESET; + + /* otherwise, just return the current state */ + return self->state; +} + +int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, void* unused_L) { + /* for backwards compatibilty, rc_test_trigger always assumes the achievement is active */ + self->state = RC_TRIGGER_STATE_ACTIVE; + + return (rc_evaluate_trigger(self, peek, ud, unused_L) == RC_TRIGGER_STATE_TRIGGERED); +} + +void rc_reset_trigger(rc_trigger_t* self) { + if (!self) + return; + + rc_reset_trigger_hitcounts(self); + + self->state = RC_TRIGGER_STATE_WAITING; + + if (self->measured_target) + self->measured_value = RC_MEASURED_UNKNOWN; + + self->has_hits = 0; +} diff --git a/src/rcheevos/src/rcheevos/value.c b/src/rcheevos/src/rcheevos/value.c new file mode 100644 index 0000000000..865e31253d --- /dev/null +++ b/src/rcheevos/src/rcheevos/value.c @@ -0,0 +1,933 @@ +#include "rc_internal.h" + +#include /* memset */ +#include /* isdigit */ +#include /* FLT_EPSILON */ +#include /* fmod */ + +int rc_is_valid_variable_character(char ch, int is_first) { + if (is_first) { + if (!isalpha((unsigned char)ch)) + return 0; + } + else { + if (!isalnum((unsigned char)ch)) + return 0; + } + return 1; +} + +static void rc_parse_cond_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_t** next_clause; + + next_clause = &self->conditions; + + do + { + parse->measured_target = 0; /* passing is_value=1 should prevent any conflicts, but clear it out anyway */ + *next_clause = rc_parse_condset(memaddr, parse); + if (parse->offset < 0) { + return; + } + + if (**memaddr == 'S' || **memaddr == 's') { + /* alt groups not supported */ + parse->offset = RC_INVALID_VALUE_FLAG; + } + else if (parse->measured_target == 0) { + parse->offset = RC_MISSING_VALUE_MEASURED; + } + else if (**memaddr == '$') { + /* maximum of */ + ++(*memaddr); + next_clause = &(*next_clause)->next; + continue; + } + + break; + } while (1); + + (*next_clause)->next = 0; +} + +static void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_with_trailing_conditions_t* condset_with_conditions; + rc_condition_t** next; + rc_condset_t** next_clause; + rc_condset_t* condset; + rc_condition_t local_cond; + rc_condition_t* cond; + uint32_t num_measured_conditions; + char buffer[64] = "A:"; + const char* buffer_ptr; + char* ptr; + int done; + + /* convert legacy format into condset */ + next_clause = &self->conditions; + do { + /* count the number of joiners and add one to determine the number of clauses. */ + buffer[0] = 'A'; /* reset to AddSource */ + done = 0; + num_measured_conditions = 1; + buffer_ptr = *memaddr; + do { + switch (*buffer_ptr++) { + case '_': /* add next */ + ++num_measured_conditions; + buffer[0] = 'A'; /* reset to AddSource */ + break; + + case '*': /* multiply */ + if (*buffer_ptr == '-') { + /* multiplication by a negative number will convert to SubSource */ + ++buffer_ptr; + buffer[0] = 'B'; + } + break; + + case '\0': /* end of string */ + case '$': /* maximum of */ + case ':': /* end of leaderboard clause */ + case ')': /* end of rich presence macro */ + done = 1; + break; + + default: /* assume everything else is valid - bad stuff will be filtered out later */ + break; + } + } while (!done); + + /* if last condition is not AddSource, we'll need to add a dummy condition for the Measured */ + if (buffer[0] != 'A') + ++num_measured_conditions; + + condset_with_conditions = RC_ALLOC_WITH_TRAILING(rc_condset_with_trailing_conditions_t, + rc_condition_t, conditions, num_measured_conditions, parse); + if (parse->offset < 0) + return; + + condset = (rc_condset_t*)condset_with_conditions; + memset(condset, 0, sizeof(*condset)); + condset->num_measured_conditions = num_measured_conditions; + cond = &condset_with_conditions->conditions[0]; + + next = &condset->conditions; + + for (;; ++(*memaddr)) { + buffer[0] = 'A'; /* reset to AddSource */ + ptr = &buffer[2]; + + /* extract the next clause */ + for (;; ++(*memaddr)) { + switch (**memaddr) { + case '_': /* add next */ + *ptr = '\0'; + break; + + case '$': /* maximum of */ + case '\0': /* end of string */ + case ':': /* end of leaderboard clause */ + case ')': /* end of rich presence macro */ + /* the last condition needs to be Measured - AddSource can be changed here, + * SubSource will be handled later */ + if (buffer[0] == 'A') + buffer[0] = 'M'; + + *ptr = '\0'; + break; + + case '*': + *ptr++ = '*'; + + buffer_ptr = *memaddr + 1; + if (*buffer_ptr == '-') { + buffer[0] = 'B'; /* change to SubSource */ + ++(*memaddr); /* don't copy sign */ + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + else if (*buffer_ptr == '+') { + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + + /* if it looks like a floating point number, add the 'f' prefix */ + while (isdigit((unsigned char)*buffer_ptr)) + ++buffer_ptr; + if (*buffer_ptr == '.') + *ptr++ = 'f'; + continue; + + default: + *ptr++ = **memaddr; + continue; + } + + break; + } + + /* process the clause */ + if (!parse->buffer) + cond = &local_cond; + + buffer_ptr = buffer; + rc_parse_condition_internal(cond, &buffer_ptr, parse); + if (parse->offset < 0) + return; + + if (*buffer_ptr) { + /* whatever we copied as a single condition was not fully consumed */ + parse->offset = RC_INVALID_COMPARISON; + return; + } + + if (!rc_operator_is_modifying(cond->oper)) { + parse->offset = RC_INVALID_OPERATOR; + return; + } + + rc_condition_update_parse_state(cond, parse); + + *next = cond; + next = &cond->next; + + if (**memaddr != '_') /* add next */ + break; + + ++cond; + } + + /* -- end of clause -- */ + + /* clause must end in a Measured. if it doesn't, append one */ + if (cond->type != RC_CONDITION_MEASURED) { + if (!parse->buffer) + cond = &local_cond; + else + ++cond; + + buffer_ptr = "M:0"; + rc_parse_condition_internal(cond, &buffer_ptr, parse); + *next = cond; + next = &cond->next; + rc_condition_update_parse_state(cond, parse); + } + + *next = NULL; + + /* finalize clause */ + *next_clause = condset; + next_clause = &condset->next; + + if (**memaddr != '$') { + /* end of valid string */ + *next_clause = NULL; + break; + } + + /* max of ($), start a new clause */ + ++(*memaddr); + } while (1); +} + +void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + const uint8_t was_value = parse->is_value; + const rc_condition_t* condition; + parse->is_value = 1; + + /* if it starts with a condition flag (M: A: B: C:), parse the conditions */ + if ((*memaddr)[1] == ':') + rc_parse_cond_value(self, memaddr, parse); + else + rc_parse_legacy_value(self, memaddr, parse); + + if (parse->offset >= 0 && parse->buffer) { + self->name = "(unnamed)"; + self->value.value = self->value.prior = 0; + self->value.memref_type = RC_MEMREF_TYPE_VALUE; + self->value.changed = 0; + self->has_memrefs = 0; + + for (condition = self->conditions->conditions; condition; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) { + if (rc_operand_is_float(&condition->operand1)) { + self->value.size = RC_MEMSIZE_FLOAT; + self->value.type = RC_VALUE_TYPE_FLOAT; + } + else { + self->value.size = RC_MEMSIZE_32_BITS; + self->value.type = RC_VALUE_TYPE_UNSIGNED; + } + break; + } + } + } + + parse->is_value = was_value; +} + +int rc_value_size(const char* memaddr) { + rc_value_with_memrefs_t* value; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); + + value = RC_ALLOC(rc_value_with_memrefs_t, &preparse.parse); + rc_parse_value_internal(&value->value, &memaddr, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); + + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; +} + +rc_value_t* rc_parse_value(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx) { + rc_value_with_memrefs_t* value; + rc_preparse_state_t preparse; + const char* preparse_memaddr = memaddr; + + (void)unused_L; + (void)unused_funcs_idx; + + if (!buffer || !memaddr) + return NULL; + + rc_init_preparse_state(&preparse); + value = RC_ALLOC(rc_value_with_memrefs_t, &preparse.parse); + rc_parse_value_internal(&value->value, &preparse_memaddr, &preparse.parse); + + rc_reset_parse_state(&preparse.parse, buffer); + value = RC_ALLOC(rc_value_with_memrefs_t, &preparse.parse); + rc_preparse_alloc_memrefs(&value->memrefs, &preparse); + + rc_parse_value_internal(&value->value, &memaddr, &preparse.parse); + value->value.has_memrefs = 1; + + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &value->value : NULL; +} + +static void rc_update_value_memrefs(rc_value_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_value_with_memrefs_t* value = (rc_value_with_memrefs_t*)self; + rc_update_memref_values(&value->memrefs, peek, ud); + } +} + +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud) { + rc_eval_state_t eval_state; + rc_condset_t* condset; + int valid = 0; + + rc_update_value_memrefs(self, peek, ud); + + value->value.i32 = 0; + value->type = RC_VALUE_TYPE_SIGNED; + + for (condset = self->conditions; condset != NULL; condset = condset->next) { + memset(&eval_state, 0, sizeof(eval_state)); + eval_state.peek = peek; + eval_state.peek_userdata = ud; + + rc_test_condset(condset, &eval_state); + + if (condset->is_paused) + continue; + + if (eval_state.was_reset) { + /* if any ResetIf condition was true, reset the hit counts + * NOTE: ResetIf only affects the current condset when used in values! + */ + rc_reset_condset(condset); + } + + if (eval_state.measured_value.type != RC_VALUE_TYPE_NONE) { + if (!valid) { + /* capture the first valid measurement, which may be negative */ + memcpy(value, &eval_state.measured_value, sizeof(*value)); + valid = 1; + } + else { + /* multiple condsets are currently only used for the MAX_OF operation. + * only keep the condset's value if it's higher than the current highest value. + */ + if (rc_typed_value_compare(&eval_state.measured_value, value, RC_OPERATOR_GT)) + memcpy(value, &eval_state.measured_value, sizeof(*value)); + } + } + } + + return valid; +} + +int32_t rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, void* unused_L) { + rc_typed_value_t result; + int valid = rc_evaluate_value_typed(self, &result, peek, ud); + + (void)unused_L; + + if (valid) { + /* if not paused, store the value so that it's available when paused. */ + rc_typed_value_convert(&result, RC_VALUE_TYPE_UNSIGNED); + rc_update_memref_value(&self->value, result.value.u32); + } + else { + /* when paused, the Measured value will not be captured, use the last captured value. */ + result.value.u32 = self->value.value; + result.type = RC_VALUE_TYPE_UNSIGNED; + } + + rc_typed_value_convert(&result, RC_VALUE_TYPE_SIGNED); + return result.value.i32; +} + +void rc_reset_value(rc_value_t* self) { + rc_condset_t* condset = self->conditions; + while (condset != NULL) { + rc_reset_condset(condset); + condset = condset->next; + } + + self->value.value = self->value.prior = 0; + self->value.changed = 0; +} + +int rc_value_from_hits(rc_value_t* self) +{ + rc_condset_t* condset = self->conditions; + for (; condset != NULL; condset = condset->next) { + rc_condition_t* condition = condset->conditions; + for (; condition != NULL; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) + return (condition->required_hits != 0); + } + } + + return 0; +} + +rc_value_t* rc_alloc_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse) { + rc_value_t** value_ptr = parse->variables; + rc_value_t* value; + const char* name; + uint32_t measured_target; + + if (!value_ptr) + return NULL; + + while (*value_ptr) { + value = *value_ptr; + if (strncmp(value->name, memaddr, memaddr_len) == 0 && value->name[memaddr_len] == 0) + return value; + + value_ptr = &value->next; + } + + /* capture name before calling parse as parse will update memaddr pointer */ + name = rc_alloc_str(parse, memaddr, memaddr_len); + if (!name) + return NULL; + + /* no match found, create a new entry */ + value = RC_ALLOC_SCRATCH(rc_value_t, parse); + memset(value, 0, sizeof(value->value)); + value->value.size = RC_MEMSIZE_VARIABLE; + value->next = NULL; + + /* the helper variable likely has a Measured condition. capture the current measured_target so we can restore it + * after generating the variable so the variable's Measured target doesn't conflict with the rest of the trigger. */ + measured_target = parse->measured_target; + rc_parse_value_internal(value, &memaddr, parse); + parse->measured_target = measured_target; + + /* store name after calling parse as parse will set name to (unnamed) */ + value->name = name; + + *value_ptr = value; + return value; +} + +uint32_t rc_count_values(const rc_value_t* values) { + uint32_t count = 0; + while (values) { + ++count; + values = values->next; + } + + return count; +} + +void rc_update_values(rc_value_t* values, rc_peek_t peek, void* ud) { + rc_typed_value_t result; + + rc_value_t* value = values; + for (; value; value = value->next) { + if (rc_evaluate_value_typed(value, &result, peek, ud)) { + /* store the raw bytes and type to be restored by rc_typed_value_from_memref_value */ + rc_update_memref_value(&value->value, result.value.u32); + value->value.type = result.type; + } + } +} + +void rc_reset_values(rc_value_t* values) { + rc_value_t* value = values; + + for (; value; value = value->next) + rc_reset_value(value); +} + +void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref) { + /* raw value is always u32, type can mark it as something else */ + value->value.u32 = memref->value; + value->type = memref->type; +} + +void rc_typed_value_convert(rc_typed_value_t* value, char new_type) { + switch (new_type) { + case RC_VALUE_TYPE_UNSIGNED: + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: + return; + case RC_VALUE_TYPE_SIGNED: + value->value.u32 = (unsigned)value->value.i32; + break; + case RC_VALUE_TYPE_FLOAT: + value->value.u32 = (unsigned)value->value.f32; + break; + default: + value->value.u32 = 0; + break; + } + break; + + case RC_VALUE_TYPE_SIGNED: + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: + return; + case RC_VALUE_TYPE_UNSIGNED: + value->value.i32 = (int)value->value.u32; + break; + case RC_VALUE_TYPE_FLOAT: + value->value.i32 = (int)value->value.f32; + break; + default: + value->value.i32 = 0; + break; + } + break; + + case RC_VALUE_TYPE_FLOAT: + switch (value->type) { + case RC_VALUE_TYPE_FLOAT: + return; + case RC_VALUE_TYPE_UNSIGNED: + value->value.f32 = (float)value->value.u32; + break; + case RC_VALUE_TYPE_SIGNED: + value->value.f32 = (float)value->value.i32; + break; + default: + value->value.f32 = 0.0; + break; + } + break; + + default: + break; + } + + value->type = new_type; +} + +static rc_typed_value_t* rc_typed_value_convert_into(rc_typed_value_t* dest, const rc_typed_value_t* source, char new_type) { + memcpy(dest, source, sizeof(rc_typed_value_t)); + rc_typed_value_convert(dest, new_type); + return dest; +} + +void rc_typed_value_negate(rc_typed_value_t* value) { + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + rc_typed_value_convert(value, RC_VALUE_TYPE_SIGNED); + /* fallthrough */ /* to RC_VALUE_TYPE_SIGNED */ + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = -(value->value.i32); + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = -(value->value.f32); + break; + + default: + break; + } +} + +void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + if (amount->type != value->type && value->type != RC_VALUE_TYPE_NONE) { + if (amount->type == RC_VALUE_TYPE_FLOAT) + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + else + amount = rc_typed_value_convert_into(&converted, amount, value->type); + } + + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + value->value.u32 += amount->value.u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 += amount->value.i32; + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 += amount->value.f32; + break; + + case RC_VALUE_TYPE_NONE: + memcpy(value, amount, sizeof(rc_typed_value_t)); + break; + + default: + break; + } +} + +void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + /* the c standard for unsigned multiplication is well defined as non-overflowing truncation + * to the type's size. this allows negative multiplication through twos-complements. i.e. + * 1 * -1 (0xFFFFFFFF) = 0xFFFFFFFF = -1 + * 3 * -2 (0xFFFFFFFE) = 0x2FFFFFFFA & 0xFFFFFFFF = 0xFFFFFFFA = -6 + * 10 * -5 (0xFFFFFFFB) = 0x9FFFFFFCE & 0xFFFFFFFF = 0xFFFFFFCE = -50 + */ + value->value.u32 *= amount->value.u32; + break; + + case RC_VALUE_TYPE_SIGNED: + value->value.u32 *= (unsigned)amount->value.i32; + break; + + case RC_VALUE_TYPE_FLOAT: + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } + break; + + case RC_VALUE_TYPE_SIGNED: + switch (amount->type) + { + case RC_VALUE_TYPE_SIGNED: + value->value.i32 *= amount->value.i32; + break; + + case RC_VALUE_TYPE_UNSIGNED: + value->value.i32 *= (int)amount->value.u32; + break; + + case RC_VALUE_TYPE_FLOAT: + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } + break; + + case RC_VALUE_TYPE_FLOAT: + if (amount->type == RC_VALUE_TYPE_NONE) { + value->type = RC_VALUE_TYPE_NONE; + } + else { + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + value->value.f32 *= amount->value.f32; + } + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + break; + } +} + +void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + if (amount->value.u32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 /= amount->value.u32; + return; + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 /= (int)amount->value.u32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_SIGNED: + if (amount->value.i32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 /= amount->value.i32; + return; + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 /= (unsigned)amount->value.i32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_FLOAT: + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + + if (amount->value.f32 == 0.0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 /= amount->value.f32; +} + +void rc_typed_value_modulus(rc_typed_value_t* value, const rc_typed_value_t* amount) { + rc_typed_value_t converted; + + switch (amount->type) + { + case RC_VALUE_TYPE_UNSIGNED: + if (amount->value.u32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 %= amount->value.u32; + return; + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 %= (int)amount->value.u32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_SIGNED: + if (amount->value.i32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 %= amount->value.i32; + return; + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 %= (unsigned)amount->value.i32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_FLOAT: + break; + + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + + if (amount->value.f32 == 0.0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + value->value.f32 = (float)fmod(value->value.f32, amount->value.f32); +} + +void rc_typed_value_combine(rc_typed_value_t* value, rc_typed_value_t* amount, uint8_t oper) { + switch (oper) { + case RC_OPERATOR_MULT: + rc_typed_value_multiply(value, amount); + break; + + case RC_OPERATOR_DIV: + rc_typed_value_divide(value, amount); + break; + + case RC_OPERATOR_AND: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 &= amount->value.u32; + break; + + case RC_OPERATOR_XOR: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 ^= amount->value.u32; + break; + + case RC_OPERATOR_MOD: + rc_typed_value_modulus(value, amount); + break; + + case RC_OPERATOR_ADD: + rc_typed_value_add(value, amount); + break; + + case RC_OPERATOR_SUB: + rc_typed_value_negate(amount); + rc_typed_value_add(value, amount); + break; + } +} + + +static int rc_typed_value_compare_floats(float f1, float f2, char oper) { + if (f1 == f2) { + /* exactly equal */ + } + else { + /* attempt to match 7 significant digits (24-bit mantissa supports just over 7 significant decimal digits) */ + /* https://stackoverflow.com/questions/17333/what-is-the-most-effective-way-for-float-and-double-comparison */ + const float abs1 = (f1 < 0) ? -f1 : f1; + const float abs2 = (f2 < 0) ? -f2 : f2; + const float threshold = ((abs1 < abs2) ? abs1 : abs2) * FLT_EPSILON; + const float diff = f1 - f2; + const float abs_diff = (diff < 0) ? -diff : diff; + + if (abs_diff <= threshold) { + /* approximately equal */ + } + else if (diff > threshold) { + /* greater */ + switch (oper) { + case RC_OPERATOR_NE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + return 1; + + default: + return 0; + } + } + else { + /* lesser */ + switch (oper) { + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } + } + } + + /* exactly or approximately equal */ + switch (oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper) { + rc_typed_value_t converted_value; + if (value2->type != value1->type) { + /* if either side is a float, convert both sides to float. otherwise, assume the signed-ness of the left side. */ + if (value2->type == RC_VALUE_TYPE_FLOAT) + value1 = rc_typed_value_convert_into(&converted_value, value1, value2->type); + else + value2 = rc_typed_value_convert_into(&converted_value, value2, value1->type); + } + + switch (value1->type) { + case RC_VALUE_TYPE_UNSIGNED: + switch (oper) { + case RC_OPERATOR_EQ: return value1->value.u32 == value2->value.u32; + case RC_OPERATOR_NE: return value1->value.u32 != value2->value.u32; + case RC_OPERATOR_LT: return value1->value.u32 < value2->value.u32; + case RC_OPERATOR_LE: return value1->value.u32 <= value2->value.u32; + case RC_OPERATOR_GT: return value1->value.u32 > value2->value.u32; + case RC_OPERATOR_GE: return value1->value.u32 >= value2->value.u32; + default: return 1; + } + + case RC_VALUE_TYPE_SIGNED: + switch (oper) { + case RC_OPERATOR_EQ: return value1->value.i32 == value2->value.i32; + case RC_OPERATOR_NE: return value1->value.i32 != value2->value.i32; + case RC_OPERATOR_LT: return value1->value.i32 < value2->value.i32; + case RC_OPERATOR_LE: return value1->value.i32 <= value2->value.i32; + case RC_OPERATOR_GT: return value1->value.i32 > value2->value.i32; + case RC_OPERATOR_GE: return value1->value.i32 >= value2->value.i32; + default: return 1; + } + + case RC_VALUE_TYPE_FLOAT: + return rc_typed_value_compare_floats(value1->value.f32, value2->value.f32, oper); + + default: + return 1; + } +} diff --git a/src/rcheevos/src/rhash/aes.c b/src/rcheevos/src/rhash/aes.c new file mode 100644 index 0000000000..54ed10bce9 --- /dev/null +++ b/src/rcheevos/src/rhash/aes.c @@ -0,0 +1,480 @@ +/* This file is sourced from https://github.com/kokke/tiny-AES-c, with unused code excised. + * This code is licensed under the Unlicense license, effectively public domain. + * https://github.com/kokke/tiny-AES-c/blob/f06ac37/unlicense.txt + */ + +/* + +This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. +Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. + +The implementation is verified against the test vectors in: + National Institute of Standards and Technology Special Publication 800-38A 2001 ED + +ECB-AES128 +---------- + + plain-text: + 6bc1bee22e409f96e93d7e117393172a + ae2d8a571e03ac9c9eb76fac45af8e51 + 30c81c46a35ce411e5fbc1191a0a52ef + f69f2445df4f9b17ad2b417be66c3710 + + key: + 2b7e151628aed2a6abf7158809cf4f3c + + resulting cipher + 3ad77bb40d7a3660a89ecaf32466ef97 + f5d3d58503b9699de785895a96fdbaaf + 43b1cd7f598ece23881b00e3ed030688 + 7b0c785e27e8ad3f8223207104725dd4 + + +NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0) + You should pad the end of the string with zeros if this is not the case. + For AES192/256 the key size is proportionally larger. + +*/ + + +/*****************************************************************************/ +/* Includes: */ +/*****************************************************************************/ +#include /* CBC mode, for memset */ +#include "aes.h" + +/*****************************************************************************/ +/* Defines: */ +/*****************************************************************************/ + +/* The number of columns comprising a state in AES. This is a constant in AES. Value=4 */ +#define Nb 4 + +#define Nk 4 /* The number of 32 bit words in a key. */ +#define Nr 10 /* The number of rounds in AES Cipher. */ + +/*****************************************************************************/ +/* Private variables: */ +/*****************************************************************************/ + +/* state - array holding the intermediate results during decryption. */ +typedef uint8_t state_t[4][4]; + +/* The lookup-tables are marked const so they can be placed in read-only storage instead of RAM + * The numbers below can be computed dynamically trading ROM for RAM - + * This can be useful in (embedded) bootloader applications, where ROM is often limited. + */ +static const uint8_t sbox[256] = { + /*0 1 2 3 4 5 6 7 8 9 A B C D E F */ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; + +static const uint8_t rsbox[256] = { + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d }; + +/* The round constant word array, Rcon[i], contains the values given by + * x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8) + */ +static const uint8_t Rcon[11] = { + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 }; + +/* + * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES-C/pull/12), + * that you can remove most of the elements in the Rcon array, because they are unused. + * + * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon + * + * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed), + * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm." + */ + + +/*****************************************************************************/ +/* Private functions: */ +/*****************************************************************************/ + +#define getSBoxValue(num) (sbox[(num)]) + +/* This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states. */ +static void KeyExpansion(uint8_t RoundKey[AES_keyExpSize], const uint8_t Key[AES_KEYLEN]) +{ + unsigned i, j, k; + uint8_t tempa[4]; /* Used for the column/row operations */ + + /* The first round key is the key itself. */ + for (i = 0; i < Nk; ++i) + { + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + } + + /* All other round keys are found from the previous round keys. */ + for (i = Nk; i < Nb * (Nr + 1); ++i) + { + { + k = (i - 1) * 4; + tempa[0]=RoundKey[k + 0]; + tempa[1]=RoundKey[k + 1]; + tempa[2]=RoundKey[k + 2]; + tempa[3]=RoundKey[k + 3]; + + } + + if (i % Nk == 0) + { + /* This function shifts the 4 bytes in a word to the left once. */ + /* [a0,a1,a2,a3] becomes [a1,a2,a3,a0] */ + + /* Function RotWord() */ + { + const uint8_t u8tmp = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = u8tmp; + } + + /* SubWord() is a function that takes a four-byte input word and + * applies the S-box to each of the four bytes to produce an output word. + */ + + /* Function Subword() */ + { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + + tempa[0] = tempa[0] ^ Rcon[i/Nk]; + } + + j = i * 4; k=(i - Nk) * 4; + RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; + RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; + RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; + RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; + } +} + +void AES_init_ctx(struct AES_ctx* ctx, const uint8_t key[AES_KEYLEN]) +{ + KeyExpansion(ctx->RoundKey, key); +} + +void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t key[AES_KEYLEN], const uint8_t iv[AES_BLOCKLEN]) +{ + KeyExpansion(ctx->RoundKey, key); + memcpy (ctx->Iv, iv, AES_BLOCKLEN); +} + +void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t iv[AES_BLOCKLEN]) +{ + memcpy (ctx->Iv, iv, AES_BLOCKLEN); +} + +/* This function adds the round key to state. + * The round key is added to the state by an XOR function. + */ +static void AddRoundKey(uint8_t round, state_t* state, const uint8_t RoundKey[AES_keyExpSize]) +{ + uint8_t i,j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; + } + } +} + +/* The SubBytes Function Substitutes the values in the + * state matrix with values in an S-box. + */ +static void SubBytes(state_t* state) +{ + uint8_t i, j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[j][i] = getSBoxValue((*state)[j][i]); + } + } +} + +/* The ShiftRows() function shifts the rows in the state to the left. + * Each row is shifted with different offset. + * Offset = Row number. So the first row is not shifted. + */ +static void ShiftRows(state_t* state) +{ + uint8_t temp; + + /* Rotate first row 1 columns to left */ + temp = (*state)[0][1]; + (*state)[0][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[3][1]; + (*state)[3][1] = temp; + + /* Rotate second row 2 columns to left */ + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + /* Rotate third row 3 columns to left */ + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[3][3]; + (*state)[3][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[1][3]; + (*state)[1][3] = temp; +} + +static uint8_t xtime(uint8_t x) +{ + return ((x<<1) ^ (((x>>7) & 1) * 0x1b)); +} + +/* MixColumns function mixes the columns of the state matrix */ +static void MixColumns(state_t* state) +{ + uint8_t i; + uint8_t Tmp, Tm, t; + for (i = 0; i < 4; ++i) + { + t = (*state)[i][0]; + Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ; + Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ; + Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ; + Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ; + Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ; + } +} + +/* Multiply is used to multiply numbers in the field GF(2^8) + * Note: The last call to xtime() is unneeded, but often ends up generating a smaller binary + * The compiler seems to be able to vectorize the operation better this way. + * See https://github.com/kokke/tiny-AES-c/pull/34 + */ + +#define Multiply(x, y) \ + ( ((y & 1) * x) ^ \ + ((y>>1 & 1) * xtime(x)) ^ \ + ((y>>2 & 1) * xtime(xtime(x))) ^ \ + ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \ + ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \ + +#define getSBoxInvert(num) (rsbox[(num)]) + +/* MixColumns function mixes the columns of the state matrix. + * The method used to multiply may be difficult to understand for the inexperienced. + * Please use the references to gain more information. + */ +static void InvMixColumns(state_t* state) +{ + int i; + uint8_t a, b, c, d; + for (i = 0; i < 4; ++i) + { + a = (*state)[i][0]; + b = (*state)[i][1]; + c = (*state)[i][2]; + d = (*state)[i][3]; + + (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09); + (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d); + (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b); + (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e); + } +} + + +/* The SubBytes Function Substitutes the values in the + * state matrix with values in an S-box. + */ +static void InvSubBytes(state_t* state) +{ + uint8_t i, j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[j][i] = getSBoxInvert((*state)[j][i]); + } + } +} + +static void InvShiftRows(state_t* state) +{ + uint8_t temp; + + /* Rotate first row 1 columns to right */ + temp = (*state)[3][1]; + (*state)[3][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[0][1]; + (*state)[0][1] = temp; + + /* Rotate second row 2 columns to right */ + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + /* Rotate third row 3 columns to right */ + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[1][3]; + (*state)[1][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[3][3]; + (*state)[3][3] = temp; +} + +/* Cipher is the main function that encrypts the PlainText. */ +static void Cipher(state_t* state, const uint8_t RoundKey[AES_keyExpSize]) +{ + uint8_t round = 0; + + /* Add the First round key to the state before starting the rounds. */ + AddRoundKey(0, state, RoundKey); + + /* There will be Nr rounds. + * The first Nr-1 rounds are identical. + * These Nr rounds are executed in the loop below. + * Last one without MixColumns() + */ + for (round = 1; ; ++round) + { + SubBytes(state); + ShiftRows(state); + if (round == Nr) { + break; + } + MixColumns(state); + AddRoundKey(round, state, RoundKey); + } + /* Add round key to last round */ + AddRoundKey(Nr, state, RoundKey); +} + +static void InvCipher(state_t* state, const uint8_t RoundKey[AES_keyExpSize]) +{ + uint8_t round = 0; + + /* Add the First round key to the state before starting the rounds. */ + AddRoundKey(Nr, state, RoundKey); + + /* There will be Nr rounds. + * The first Nr-1 rounds are identical. + * These Nr rounds are executed in the loop below. + * Last one without InvMixColumn() + */ + for (round = (Nr - 1); ; --round) + { + InvShiftRows(state); + InvSubBytes(state); + AddRoundKey(round, state, RoundKey); + if (round == 0) { + break; + } + InvMixColumns(state); + } +} + +/*****************************************************************************/ +/* Public functions: */ +/*****************************************************************************/ + +static void XorWithIv(uint8_t* buf, const uint8_t Iv[AES_BLOCKLEN]) +{ + uint8_t i; + for (i = 0; i < AES_BLOCKLEN; ++i) /* The block in AES is always 128bit no matter the key size */ + { + buf[i] ^= Iv[i]; + } +} + +void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) +{ + size_t i; + uint8_t storeNextIv[AES_BLOCKLEN]; + for (i = 0; i < length; i += AES_BLOCKLEN) + { + memcpy(storeNextIv, buf, AES_BLOCKLEN); + InvCipher((state_t*)buf, ctx->RoundKey); + XorWithIv(buf, ctx->Iv); + memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN); + buf += AES_BLOCKLEN; + } +} + +/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ +void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) +{ + uint8_t buffer[AES_BLOCKLEN]; + + size_t i; + int bi; + for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) + { + if (bi == AES_BLOCKLEN) /* we need to regen xor compliment in buffer */ + { + memcpy(buffer, ctx->Iv, AES_BLOCKLEN); + Cipher((state_t*)buffer, ctx->RoundKey); + + /* Increment Iv and handle overflow */ + for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) + { + /* inc will overflow */ + if (ctx->Iv[bi] == 255) + { + ctx->Iv[bi] = 0; + continue; + } + ctx->Iv[bi] += 1; + break; + } + bi = 0; + } + + buf[i] = (buf[i] ^ buffer[bi]); + } +} diff --git a/src/rcheevos/src/rhash/aes.h b/src/rcheevos/src/rhash/aes.h new file mode 100644 index 0000000000..13e679ee14 --- /dev/null +++ b/src/rcheevos/src/rhash/aes.h @@ -0,0 +1,49 @@ +#ifndef AES_H +#define AES_H + +/* This file is sourced from https://github.com/kokke/tiny-AES-c, with unused code excised. + * This code is licensed under the Unlicense license, effectively public domain. + * https://github.com/kokke/tiny-AES-c/blob/f06ac37/unlicense.txt + */ + +#include +#include + +#define AES_BLOCKLEN 16 /* Block length in bytes - AES is 128b block only */ +#define AES_KEYLEN 16 /* Key length in bytes */ +#define AES_keyExpSize 176 + +struct AES_ctx +{ + uint8_t RoundKey[AES_keyExpSize]; + uint8_t Iv[AES_BLOCKLEN]; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +void AES_init_ctx(struct AES_ctx* ctx, const uint8_t key[AES_KEYLEN]); +void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t key[AES_KEYLEN], const uint8_t iv[AES_BLOCKLEN]); +void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t iv[AES_BLOCKLEN]); + +/* buffer size MUST be mutile of AES_BLOCKLEN; + * Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme + * NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv() + * no IV should ever be reused with the same key + */ +void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); + +/* Same function for encrypting as for decrypting. + * IV is incremented for every block, and used after encryption as XOR-compliment for output + * Suggesting https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme + * NOTES: you need to set IV in ctx with AES_init_ctx_iv() or AES_ctx_set_iv() + * no IV should ever be reused with the same key + */ +void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); + +#ifdef __cplusplus +} +#endif + +#endif /* AES_H */ diff --git a/src/rcheevos/src/rhash/cdreader.c b/src/rcheevos/src/rhash/cdreader.c new file mode 100644 index 0000000000..9ffcc55bcb --- /dev/null +++ b/src/rcheevos/src/rhash/cdreader.c @@ -0,0 +1,838 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include +#include +#include + +static int cdreader_get_sector(uint8_t header[16]) +{ + int minutes = (header[12] >> 4) * 10 + (header[12] & 0x0F); + int seconds = (header[13] >> 4) * 10 + (header[13] & 0x0F); + int frames = (header[14] >> 4) * 10 + (header[14] & 0x0F); + + /* convert the MSF value to a sector index, and subtract 150 (2 seconds) per: + * For data and mixed mode media (those conforming to ISO/IEC 10149), logical block address + * zero shall be assigned to the block at MSF address 00/02/00 */ + return ((minutes * 60) + seconds) * 75 + frames - 150; +} + +static void cdreader_determine_sector_size(rc_hash_cdrom_track_t* cdrom) +{ + /* Attempt to determine the sector and header sizes. The CUE file may be lying. + * Look for the sync pattern using each of the supported sector sizes. + * Then check for the presence of "CD001", which is gauranteed to be in either the + * boot record or primary volume descriptor, one of which is always at sector 16. + */ + const uint8_t sync_pattern[] = { + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + }; + + uint8_t header[32]; + const int64_t toc_sector = 16 + cdrom->track_pregap_sectors; + + cdrom->sector_size = 0; + cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2048; + + cdrom->file_reader->seek(cdrom->file_handle, toc_sector * 2352 + cdrom->file_track_offset, SEEK_SET); + if (cdrom->file_reader->read(cdrom->file_handle, header, sizeof(header)) < sizeof(header)) + return; + + if (memcmp(header, sync_pattern, 12) == 0) + { + cdrom->sector_size = 2352; + + if (memcmp(&header[25], "CD001", 5) == 0) + cdrom->sector_header_size = 24; + else + cdrom->sector_header_size = 16; + + cdrom->track_first_sector = cdreader_get_sector(header) - (int)toc_sector; + } + else + { + cdrom->file_reader->seek(cdrom->file_handle, toc_sector * 2336 + cdrom->file_track_offset, SEEK_SET); + cdrom->file_reader->read(cdrom->file_handle, header, sizeof(header)); + + if (memcmp(header, sync_pattern, 12) == 0) + { + cdrom->sector_size = 2336; + + if (memcmp(&header[25], "CD001", 5) == 0) + cdrom->sector_header_size = 24; + else + cdrom->sector_header_size = 16; + + cdrom->track_first_sector = cdreader_get_sector(header) - (int)toc_sector; + } + else + { + cdrom->file_reader->seek(cdrom->file_handle, toc_sector * 2048 + cdrom->file_track_offset, SEEK_SET); + cdrom->file_reader->read(cdrom->file_handle, header, sizeof(header)); + + if (memcmp(&header[1], "CD001", 5) == 0) + { + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + } + } +} + +static void* cdreader_open_bin_track(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) +{ + void* file_handle; + rc_hash_cdrom_track_t* cdrom; + + if (track > 1) { + rc_hash_iterator_verbose(iterator, "Cannot locate secondary tracks without a cue sheet"); + return NULL; + } + + file_handle = iterator->callbacks.filereader.open(path); + if (!file_handle) + return NULL; + + cdrom = (rc_hash_cdrom_track_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + return NULL; + cdrom->file_reader = &iterator->callbacks.filereader; + cdrom->file_handle = file_handle; +#ifndef NDEBUG + cdrom->track_id = track; +#endif + + cdreader_determine_sector_size(cdrom); + + if (cdrom->sector_size == 0) + { + int64_t size; + + iterator->callbacks.filereader.seek(file_handle, 0, SEEK_END); + size = iterator->callbacks.filereader.tell(file_handle); + + if ((size % 2352) == 0) + { + /* raw tracks use all 2352 bytes and have a 24 byte header */ + cdrom->sector_size = 2352; + cdrom->sector_header_size = 24; + } + else if ((size % 2048) == 0) + { + /* cooked tracks eliminate all header/footer data */ + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + else if ((size % 2336) == 0) + { + /* MODE 2 format without 16-byte sync data */ + cdrom->sector_size = 2336; + cdrom->sector_header_size = 8; + } + else + { + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); + free(cdrom); + + rc_hash_iterator_verbose(iterator, "Could not determine sector size"); + + return NULL; + } + } + + return cdrom; +} + +static int cdreader_open_bin(rc_hash_cdrom_track_t* cdrom, const char* path, const char* mode) +{ + cdrom->file_handle = cdrom->file_reader->open(path); + if (!cdrom->file_handle) + return 0; + + /* determine sector size */ + cdreader_determine_sector_size(cdrom); + + /* could not determine, which means we'll probably have more issues later + * but use the CUE provided information anyway + */ + if (cdrom->sector_size == 0) + { + /* All of these modes have 2048 byte payloads. In MODE1/2352 and MODE2/2352 + * modes, the mode can actually be specified per sector to change the payload + * size, but that reduces the ability to recover from errors when the disc + * is damaged, so it's seldomly used, and when it is, it's mostly for audio + * or video data where a blip or two probably won't be noticed by the user. + * So, while we techincally support all of the following modes, we only do + * so with 2048 byte payloads. + * http://totalsonicmastering.com/cuesheetsyntax.htm + * MODE1/2048 ? CDROM Mode1 Data (cooked) [no header, no footer] + * MODE1/2352 ? CDROM Mode1 Data (raw) [16 byte header, 288 byte footer] + * MODE2/2336 ? CDROM-XA Mode2 Data [8 byte header, 280 byte footer] + * MODE2/2352 ? CDROM-XA Mode2 Data [24 byte header, 280 byte footer] + */ + if (memcmp(mode, "MODE2/2352", 10) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 24; + } + else if (memcmp(mode, "MODE1/2048", 10) == 0) + { + cdrom->sector_size = 2048; + cdrom->sector_header_size = 0; + } + else if (memcmp(mode, "MODE2/2336", 10) == 0) + { + cdrom->sector_size = 2336; + cdrom->sector_header_size = 8; + } + else if (memcmp(mode, "MODE1/2352", 10) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 16; + } + else if (memcmp(mode, "AUDIO", 5) == 0) + { + cdrom->sector_size = 2352; + cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2352; /* no header or footer data on audio tracks */ + } + } + + return (cdrom->sector_size != 0); +} + +static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name, const rc_hash_iterator_t* iterator) +{ + const char* filename = rc_path_get_filename(cue_path); + const size_t bin_name_len = strlen(bin_name); + const size_t cue_path_len = filename - cue_path; + const size_t needed = cue_path_len + bin_name_len + 1; + + char* bin_filename = (char*)malloc(needed); + if (!bin_filename) + { + rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)needed); + } + else + { + memcpy(bin_filename, cue_path, cue_path_len); + memcpy(bin_filename + cue_path_len, bin_name, bin_name_len + 1); + } + + return bin_filename; +} + +static int64_t cdreader_get_bin_size(const char* cue_path, const char* bin_name, const rc_hash_iterator_t* iterator) +{ + int64_t size = 0; + char* bin_filename = cdreader_get_bin_path(cue_path, bin_name, iterator); + if (bin_filename) + { + void* handle = iterator->callbacks.filereader.open(bin_filename); + if (handle) + { + iterator->callbacks.filereader.seek(handle, 0, SEEK_END); + size = iterator->callbacks.filereader.tell(handle); + + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(handle); + } + + free(bin_filename); + } + + return size; +} + +static void* cdreader_open_cue_track(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) +{ + void* cue_handle; + int64_t cue_offset = 0; + char buffer[1024]; + char* bin_filename = NULL; + char *ptr, *ptr2, *end; + int done = 0; + int session = 1; + size_t num_read = 0; + rc_hash_cdrom_track_t* cdrom = NULL; + + struct track_t + { + uint32_t id; + int sector_size; + int sector_count; + int first_sector; + int pregap_sectors; + int is_data; + int file_track_offset; + int file_first_sector; + char mode[16]; + char filename[256]; + } current_track, previous_track, largest_track; + + cue_handle = iterator->callbacks.filereader.open(path); + if (!cue_handle) + return NULL; + + memset(¤t_track, 0, sizeof(current_track)); + memset(&previous_track, 0, sizeof(previous_track)); + memset(&largest_track, 0, sizeof(largest_track)); + + do + { + num_read = iterator->callbacks.filereader.read(cue_handle, buffer, sizeof(buffer) - 1); + if (num_read == 0) + break; + + buffer[num_read] = 0; + if (num_read == sizeof(buffer) - 1) + end = buffer + sizeof(buffer) * 3 / 4; + else + end = buffer + num_read; + + for (ptr = buffer; ptr < end; ++ptr) + { + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "INDEX ", 6) == 0) + { + int m = 0, s = 0, f = 0; + int index; + int sector_offset; + + ptr += 6; + index = atoi(ptr); + + while (*ptr != ' ' && *ptr != '\n') + ++ptr; + while (*ptr == ' ') + ++ptr; + + /* convert mm:ss:ff to sector count */ + sscanf_s(ptr, "%d:%d:%d", &m, &s, &f); + sector_offset = ((m * 60) + s) * 75 + f; + + if (current_track.first_sector == -1) + { + current_track.first_sector = sector_offset; + if (strcmp(current_track.filename, previous_track.filename) == 0) + { + previous_track.sector_count = current_track.first_sector - previous_track.first_sector; + current_track.file_track_offset += previous_track.sector_count * previous_track.sector_size; + } + + /* if looking for the largest data track, determine previous track size */ + if (track == RC_HASH_CDTRACK_LARGEST && previous_track.sector_count > largest_track.sector_count && + previous_track.is_data) + { + memcpy(&largest_track, &previous_track, sizeof(largest_track)); + } + } + + if (index == 1) + { + current_track.pregap_sectors = (sector_offset - current_track.first_sector); + + { + char* scan = current_track.mode; + while (*scan && !isspace((unsigned char)*scan)) + ++scan; + *scan = '\0'; + + /* it's undesirable to truncate offset to 32-bits, but %lld isn't defined in c89. */ + rc_hash_iterator_verbose_formatted(iterator, "Found %s track %d (first sector %d, sector size %d, %d pregap sectors)", + current_track.mode, current_track.id, current_track.first_sector, current_track.sector_size, current_track.pregap_sectors); + } + + if (current_track.id == track) + { + done = 1; + break; + } + + if (track == RC_HASH_CDTRACK_FIRST_DATA && current_track.is_data) + { + track = current_track.id; + done = 1; + break; + } + + if (track == RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION && session == 2) + { + track = current_track.id; + done = 1; + break; + } + } + } + else if (strncasecmp(ptr, "TRACK ", 6) == 0) + { + if (current_track.sector_size) + memcpy(&previous_track, ¤t_track, sizeof(current_track)); + + ptr += 6; + current_track.id = atoi(ptr); + + current_track.pregap_sectors = -1; + current_track.first_sector = -1; + + while (*ptr != ' ') + ++ptr; + while (*ptr == ' ') + ++ptr; + memcpy(current_track.mode, ptr, sizeof(current_track.mode)); + current_track.is_data = (memcmp(current_track.mode, "MODE", 4) == 0); + + if (current_track.is_data) + { + current_track.sector_size = atoi(ptr + 6); + } + else + { + /* assume AUDIO */ + current_track.sector_size = 2352; + } + } + else if (strncasecmp(ptr, "FILE ", 5) == 0) + { + if (current_track.sector_size) + { + memcpy(&previous_track, ¤t_track, sizeof(previous_track)); + + if (previous_track.sector_count == 0) + { + const int64_t bin_size = cdreader_get_bin_size(path, previous_track.filename, iterator); + const uint32_t file_sector_count = (uint32_t)bin_size / previous_track.sector_size; + previous_track.sector_count = file_sector_count - previous_track.first_sector; + } + + /* if looking for the largest data track, check to see if this one is larger */ + if (track == RC_HASH_CDTRACK_LARGEST && previous_track.is_data && + previous_track.sector_count > largest_track.sector_count) + { + memcpy(&largest_track, &previous_track, sizeof(largest_track)); + } + } + + memset(¤t_track, 0, sizeof(current_track)); + + current_track.file_first_sector = previous_track.file_first_sector + + previous_track.first_sector + previous_track.sector_count; + + ptr += 5; + ptr2 = ptr; + if (*ptr == '"') + { + ++ptr; + do + { + ++ptr2; + } while (*ptr2 && *ptr2 != '\n' && *ptr2 != '"'); + } + else + { + do + { + ++ptr2; + } while (*ptr2 && *ptr2 != '\n' && *ptr2 != ' '); + } + + if (ptr2 - ptr < (int)sizeof(current_track.filename)) + memcpy(current_track.filename, ptr, ptr2 - ptr); + } + else if (strncasecmp(ptr, "REM ", 4) == 0) + { + ptr += 4; + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "SESSION ", 8) == 0) + { + ptr += 8; + while (*ptr == ' ') + ++ptr; + session = atoi(ptr); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + } + + if (done) + break; + + cue_offset += (ptr - buffer); + iterator->callbacks.filereader.seek(cue_handle, cue_offset, SEEK_SET); + + } while (1); + + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(cue_handle); + + if (track == RC_HASH_CDTRACK_LARGEST) + { + if (current_track.sector_size && current_track.is_data) + { + const int64_t bin_size = cdreader_get_bin_size(path, current_track.filename, iterator); + const uint32_t file_sector_count = (uint32_t)bin_size / current_track.sector_size; + current_track.sector_count = file_sector_count - current_track.first_sector; + + if (largest_track.sector_count > current_track.sector_count) + memcpy(¤t_track, &largest_track, sizeof(current_track)); + } + else + { + memcpy(¤t_track, &largest_track, sizeof(current_track)); + } + + track = current_track.id; + } + else if (track == RC_HASH_CDTRACK_LAST && !done) + { + track = current_track.id; + } + + if (current_track.id == track) + { + cdrom = (rc_hash_cdrom_track_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + { + rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); + return NULL; + } + + cdrom->file_reader = &iterator->callbacks.filereader; + cdrom->file_track_offset = current_track.file_track_offset; + cdrom->track_pregap_sectors = current_track.pregap_sectors; + cdrom->track_first_sector = current_track.file_first_sector + current_track.first_sector; +#ifndef NDEBUG + cdrom->track_id = current_track.id; +#endif + + /* verify existance of bin file */ + bin_filename = cdreader_get_bin_path(path, current_track.filename, iterator); + if (bin_filename) + { + if (cdreader_open_bin(cdrom, bin_filename, current_track.mode)) + { + if (cdrom->track_pregap_sectors) + rc_hash_iterator_verbose_formatted(iterator, "Opened track %d (sector size %d, %d pregap sectors)", + track, cdrom->sector_size, cdrom->track_pregap_sectors); + else + rc_hash_iterator_verbose_formatted(iterator, "Opened track %d (sector size %d)", track, cdrom->sector_size); + } + else + { + if (cdrom->file_handle) + { + cdrom->file_reader->close(cdrom->file_handle); + rc_hash_iterator_error_formatted(iterator, "Could not determine sector size for %s track", current_track.mode); + } + else + { + rc_hash_iterator_error_formatted(iterator, "Could not open %s", bin_filename); + } + + free(cdrom); + cdrom = NULL; + } + + free(bin_filename); + } + } + + return cdrom; +} + +static void* cdreader_open_gdi_track(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) +{ + void* file_handle; + char buffer[1024]; + char mode[16] = "MODE1/"; + char sector_size[16]; + char file[256]; + int64_t track_size; + int track_type; + char* bin_path = NULL; + uint32_t current_track = 0; + char* ptr, *ptr2, *end; + int lba = 0; + + uint32_t largest_track = 0; + int64_t largest_track_size = 0; + char largest_track_file[256]; + char largest_track_sector_size[16]; + int largest_track_lba = 0; + + int found = 0; + size_t num_read = 0; + int64_t file_offset = 0; + rc_hash_cdrom_track_t* cdrom = NULL; + + file_handle = iterator->callbacks.filereader.open(path); + if (!file_handle) + return NULL; + + file[0] = '\0'; + do + { + num_read = iterator->callbacks.filereader.read(file_handle, buffer, sizeof(buffer) - 1); + if (num_read == 0) + break; + + buffer[num_read] = 0; + if (num_read == sizeof(buffer) - 1) + end = buffer + sizeof(buffer) * 3 / 4; + else + end = buffer + num_read; + + ptr = buffer; + + /* the first line contains the number of tracks, so we can get the last track index from it */ + if (track == RC_HASH_CDTRACK_LAST) + track = atoi(ptr); + + /* first line contains the number of tracks and will be skipped */ + while (ptr < end) + { + /* skip until next newline */ + while (*ptr != '\n' && ptr < end) + ++ptr; + + /* skip newlines */ + while ((*ptr == '\n' || *ptr == '\r') && ptr < end) + ++ptr; + + /* line format: [trackid] [lba] [type] [sectorsize] [file] [?] */ + while (isspace((unsigned char)*ptr)) + ++ptr; + + current_track = (uint32_t)atoi(ptr); + if (track && current_track != track && track != RC_HASH_CDTRACK_FIRST_DATA) + continue; + + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + lba = atoi(ptr); + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + track_type = atoi(ptr); + while (isdigit((unsigned char)*ptr)) + ++ptr; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + ptr2 = sector_size; + while (isdigit((unsigned char)*ptr)) + *ptr2++ = *ptr++; + *ptr2 = '\0'; + ++ptr; + + while (isspace((unsigned char)*ptr)) + ++ptr; + + ptr2 = file; + if (*ptr == '\"') + { + ++ptr; + while (*ptr != '\"') + *ptr2++ = *ptr++; + ++ptr; + } + else + { + while (*ptr != ' ') + *ptr2++ = *ptr++; + } + *ptr2 = '\0'; + + if (track == current_track) + { + found = 1; + break; + } + else if (track == RC_HASH_CDTRACK_FIRST_DATA && track_type == 4) + { + track = current_track; + found = 1; + break; + } + else if (track == RC_HASH_CDTRACK_LARGEST && track_type == 4) + { + track_size = cdreader_get_bin_size(path, file, iterator); + if (track_size > largest_track_size) + { + largest_track_size = track_size; + largest_track = current_track; + largest_track_lba = lba; + strcpy_s(largest_track_file, sizeof(largest_track_file), file); + strcpy_s(largest_track_sector_size, sizeof(largest_track_sector_size), sector_size); + } + } + } + + if (found) + break; + + file_offset += (ptr - buffer); + iterator->callbacks.filereader.seek(file_handle, file_offset, SEEK_SET); + + } while (1); + + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); + + cdrom = (rc_hash_cdrom_track_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + { + rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); + return NULL; + } + + cdrom->file_reader = &iterator->callbacks.filereader; + + /* if we were tracking the largest track, make it the current track. + * otherwise, current_track will be the requested track, or last track. */ + if (largest_track != 0 && largest_track != current_track) + { + current_track = largest_track; + strcpy_s(file, sizeof(file), largest_track_file); + strcpy_s(sector_size, sizeof(sector_size), largest_track_sector_size); + lba = largest_track_lba; + } + + /* open the bin file for the track - construct mode parameter from sector_size */ + ptr = &mode[6]; + ptr2 = sector_size; + while (*ptr2 && *ptr2 != '\"') + *ptr++ = *ptr2++; + *ptr = '\0'; + + bin_path = cdreader_get_bin_path(path, file, iterator); + if (cdreader_open_bin(cdrom, bin_path, mode)) + { + cdrom->track_pregap_sectors = 0; + cdrom->track_first_sector = lba; +#ifndef NDEBUG + cdrom->track_id = current_track; +#endif + + rc_hash_iterator_verbose_formatted(iterator, "Opened track %d (sector size %d)", current_track, cdrom->sector_size); + } + else + { + rc_hash_iterator_error_formatted(iterator, "Could not open %s", bin_path); + + free(cdrom); + cdrom = NULL; + } + + free(bin_path); + + return cdrom; +} + +static void* cdreader_open_track_iterator(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) +{ + /* backwards compatibility - 0 used to mean largest */ + if (track == 0) + track = RC_HASH_CDTRACK_LARGEST; + + if (rc_path_compare_extension(path, "cue")) + return cdreader_open_cue_track(path, track, iterator); + if (rc_path_compare_extension(path, "gdi")) + return cdreader_open_gdi_track(path, track, iterator); + + return cdreader_open_bin_track(path, track, iterator); +} + +static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + int64_t sector_start; + size_t num_read, total_read = 0; + uint8_t* buffer_ptr = (uint8_t*)buffer; + + rc_hash_cdrom_track_t* cdrom = (rc_hash_cdrom_track_t*)track_handle; + if (!cdrom) + return 0; + + if (sector < (uint32_t)cdrom->track_first_sector) + return 0; + + sector_start = (int64_t)(sector - cdrom->track_first_sector) * cdrom->sector_size + + cdrom->sector_header_size + cdrom->file_track_offset; + + while (requested_bytes > (size_t)cdrom->raw_data_size) + { + cdrom->file_reader->seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = cdrom->file_reader->read(cdrom->file_handle, buffer_ptr, cdrom->raw_data_size); + total_read += num_read; + + if (num_read < (size_t)cdrom->raw_data_size) + return total_read; + + buffer_ptr += cdrom->raw_data_size; + sector_start += cdrom->sector_size; + requested_bytes -= cdrom->raw_data_size; + } + + cdrom->file_reader->seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = cdrom->file_reader->read(cdrom->file_handle, buffer_ptr, (int)requested_bytes); + total_read += num_read; + + return total_read; +} + +static void cdreader_close_track(void* track_handle) +{ + rc_hash_cdrom_track_t* cdrom = (rc_hash_cdrom_track_t*)track_handle; + if (cdrom) + { + if (cdrom->file_handle && cdrom->file_reader->close) + cdrom->file_reader->close(cdrom->file_handle); + + free(track_handle); + } +} + +static uint32_t cdreader_first_track_sector(void* track_handle) +{ + rc_hash_cdrom_track_t* cdrom = (rc_hash_cdrom_track_t*)track_handle; + if (cdrom) + return cdrom->track_first_sector + cdrom->track_pregap_sectors; + + return 0; +} + +void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader) +{ + cdreader->open_track = NULL; + cdreader->read_sector = cdreader_read_sector; + cdreader->close_track = cdreader_close_track; + cdreader->first_track_sector = cdreader_first_track_sector; + cdreader->open_track_iterator = cdreader_open_track_iterator; +} + +void rc_hash_init_default_cdreader(void) +{ + struct rc_hash_cdreader cdreader; + rc_hash_get_default_cdreader(&cdreader); + rc_hash_init_custom_cdreader(&cdreader); +} diff --git a/src/rcheevos/src/rhash/hash.c b/src/rcheevos/src/rhash/hash.c new file mode 100644 index 0000000000..fe6f3ca9d0 --- /dev/null +++ b/src/rcheevos/src/rhash/hash.c @@ -0,0 +1,1402 @@ +#include "rc_hash.h" + +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif + +#include +#include + +const char* rc_path_get_filename(const char* path); +static int rc_hash_from_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); + +/* ===================================================== */ + +static rc_hash_message_callback_deprecated g_error_message_callback = NULL; +static rc_hash_message_callback_deprecated g_verbose_message_callback = NULL; + +static void rc_hash_call_g_error_message_callback(const char* message, const rc_hash_iterator_t* iterator) +{ + (void)iterator; + g_error_message_callback(message); +} + +static void rc_hash_call_g_verbose_message_callback(const char* message, const rc_hash_iterator_t* iterator) +{ + (void)iterator; + g_verbose_message_callback(message); +} + +static void rc_hash_dispatch_message_va(const rc_hash_message_callback_func callback, + const rc_hash_iterator_t* iterator, const char* format, va_list args) +{ + char buffer[1024]; + +#ifdef __STDC_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + vsprintf(buffer, format, args); +#endif + + callback(buffer, iterator); +} + +void rc_hash_init_error_message_callback(rc_hash_message_callback_deprecated callback) +{ + g_error_message_callback = callback; +} + +static rc_hash_message_callback_func rc_hash_get_error_message_callback(const rc_hash_callbacks_t* callbacks) +{ + if (callbacks && callbacks->error_message) + return callbacks->error_message; + + if (g_error_message_callback) + return rc_hash_call_g_error_message_callback; + + if (callbacks && callbacks->verbose_message) + return callbacks->verbose_message; + + if (g_verbose_message_callback) + return rc_hash_call_g_verbose_message_callback; + + return NULL; +} + +int rc_hash_iterator_error(const rc_hash_iterator_t* iterator, const char* message) +{ + rc_hash_message_callback_func message_callback = rc_hash_get_error_message_callback(&iterator->callbacks); + + if (message_callback) + message_callback(message, iterator); + + return 0; +} + +int rc_hash_iterator_error_formatted(const rc_hash_iterator_t* iterator, const char* format, ...) +{ + rc_hash_message_callback_func message_callback = rc_hash_get_error_message_callback(&iterator->callbacks); + + if (message_callback) { + va_list args; + va_start(args, format); + rc_hash_dispatch_message_va(message_callback, iterator, format, args); + va_end(args); + } + + return 0; +} + +void rc_hash_init_verbose_message_callback(rc_hash_message_callback_deprecated callback) +{ + g_verbose_message_callback = callback; +} + +void rc_hash_iterator_verbose(const rc_hash_iterator_t* iterator, const char* message) +{ + if (iterator->callbacks.verbose_message) + iterator->callbacks.verbose_message(message, iterator); + else if (g_verbose_message_callback) + g_verbose_message_callback(message); +} + +void rc_hash_iterator_verbose_formatted(const rc_hash_iterator_t* iterator, const char* format, ...) +{ + if (iterator->callbacks.verbose_message) { + va_list args; + va_start(args, format); + rc_hash_dispatch_message_va(iterator->callbacks.verbose_message, iterator, format, args); + va_end(args); + } + else if (g_verbose_message_callback) { + va_list args; + va_start(args, format); + rc_hash_dispatch_message_va(rc_hash_call_g_verbose_message_callback, iterator, format, args); + va_end(args); + } +} + +/* ===================================================== */ + +static struct rc_hash_filereader g_filereader_funcs; +static struct rc_hash_filereader* g_filereader = NULL; + +#if defined(WINVER) && WINVER >= 0x0500 +static void* filereader_open(const char* path) +{ + /* Windows requires using wchar APIs for Unicode paths */ + /* Note that MultiByteToWideChar will only be defined for >= Windows 2000 */ + wchar_t* wpath; + int wpath_length; + FILE* fp; + + /* Calculate wpath length from path */ + wpath_length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, NULL, 0); + if (wpath_length == 0) /* 0 indicates error (this is likely from invalid UTF-8) */ + return NULL; + + wpath = (wchar_t*)malloc(wpath_length * sizeof(wchar_t)); + if (!wpath) + return NULL; + + if (MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, wpath_length) == 0) { + free(wpath); + return NULL; + } + + #if defined(__STDC_SECURE_LIB__) + /* have to use _SH_DENYNO because some cores lock the file while its loaded */ + fp = _wfsopen(wpath, L"rb", _SH_DENYNO); + #else + fp = _wfopen(wpath, L"rb"); + #endif + + free(wpath); + return fp; +} +#else /* !WINVER >= 0x0500 */ +static void* filereader_open(const char* path) +{ + #if defined(__STDC_SECURE_LIB__) + #if defined(WINVER) + /* have to use _SH_DENYNO because some cores lock the file while its loaded */ + return _fsopen(path, "rb", _SH_DENYNO); + #else /* !WINVER */ + FILE *fp; + fopen_s(&fp, path, "rb"); + return fp; + #endif + #else /* !__STDC_SECURE_LIB__ */ + return fopen(path, "rb"); + #endif +} +#endif /* WINVER >= 0x0500 */ + +static void filereader_seek(void* file_handle, int64_t offset, int origin) +{ +#if defined(_WIN32) + _fseeki64((FILE*)file_handle, offset, origin); +#elif defined(_LARGEFILE64_SOURCE) + fseeko64((FILE*)file_handle, offset, origin); +#else + fseek((FILE*)file_handle, offset, origin); +#endif +} + +static int64_t filereader_tell(void* file_handle) +{ +#if defined(_WIN32) + return _ftelli64((FILE*)file_handle); +#elif defined(_LARGEFILE64_SOURCE) + return ftello64((FILE*)file_handle); +#else + return ftell((FILE*)file_handle); +#endif +} + +static size_t filereader_read(void* file_handle, void* buffer, size_t requested_bytes) +{ + return fread(buffer, 1, requested_bytes, (FILE*)file_handle); +} + +static void filereader_close(void* file_handle) +{ + fclose((FILE*)file_handle); +} + +/* for unit tests - normally would call rc_hash_init_custom_filereader(NULL) */ +void rc_hash_reset_filereader(void) +{ + g_filereader = NULL; +} + +void rc_hash_init_custom_filereader(struct rc_hash_filereader* reader) +{ + /* initialize with defaults first */ + g_filereader_funcs.open = filereader_open; + g_filereader_funcs.seek = filereader_seek; + g_filereader_funcs.tell = filereader_tell; + g_filereader_funcs.read = filereader_read; + g_filereader_funcs.close = filereader_close; + + /* hook up any provided custom handlers */ + if (reader) { + if (reader->open) + g_filereader_funcs.open = reader->open; + + if (reader->seek) + g_filereader_funcs.seek = reader->seek; + + if (reader->tell) + g_filereader_funcs.tell = reader->tell; + + if (reader->read) + g_filereader_funcs.read = reader->read; + + if (reader->close) + g_filereader_funcs.close = reader->close; + } + + g_filereader = &g_filereader_funcs; +} + +void* rc_file_open(const rc_hash_iterator_t* iterator, const char* path) +{ + void* handle = NULL; + + if (!iterator->callbacks.filereader.open) { + rc_hash_iterator_error(iterator, "No callback registered for opening files"); + } else { + handle = iterator->callbacks.filereader.open(path); + if (handle) + rc_hash_iterator_verbose_formatted(iterator, "Opened %s", rc_path_get_filename(path)); + } + + return handle; +} + +void rc_file_seek(const rc_hash_iterator_t* iterator, void* file_handle, int64_t offset, int origin) +{ + if (iterator->callbacks.filereader.seek) + iterator->callbacks.filereader.seek(file_handle, offset, origin); +} + +int64_t rc_file_tell(const rc_hash_iterator_t* iterator, void* file_handle) +{ + return iterator->callbacks.filereader.tell ? iterator->callbacks.filereader.tell(file_handle) : 0; +} + +size_t rc_file_read(const rc_hash_iterator_t* iterator, void* file_handle, void* buffer, int requested_bytes) +{ + return iterator->callbacks.filereader.read ? iterator->callbacks.filereader.read(file_handle, buffer, requested_bytes) : 0; +} + +void rc_file_close(const rc_hash_iterator_t* iterator, void* file_handle) +{ + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); +} + +int64_t rc_file_size(const rc_hash_iterator_t* iterator, const char* path) +{ + int64_t size = 0; + + /* don't use rc_file_open to avoid log statements */ + if (!iterator->callbacks.filereader.open) { + rc_hash_iterator_error(iterator, "No callback registered for opening files"); + } else { + void* handle = iterator->callbacks.filereader.open(path); + if (handle) { + rc_file_seek(iterator, handle, 0, SEEK_END); + size = rc_file_tell(iterator, handle); + rc_file_close(iterator, handle); + } + } + + return size; +} + +/* ===================================================== */ + +const char* rc_path_get_filename(const char* path) +{ + const char* ptr = path + strlen(path); + do { + if (ptr[-1] == '/' || ptr[-1] == '\\') + break; + + --ptr; + } while (ptr > path); + + return ptr; +} + +const char* rc_path_get_extension(const char* path) +{ + const char* ptr = path + strlen(path); + do { + if (ptr[-1] == '.') + return ptr; + + --ptr; + } while (ptr > path); + + return path + strlen(path); +} + +int rc_path_compare_extension(const char* path, const char* ext) +{ + size_t path_len = strlen(path); + size_t ext_len = strlen(ext); + const char* ptr = path + path_len - ext_len; + if (ptr[-1] != '.') + return 0; + + if (memcmp(ptr, ext, ext_len) == 0) + return 1; + + do { + if (tolower(*ptr) != *ext) + return 0; + + ++ext; + ++ptr; + } while (*ptr); + + return 1; +} + +/* ===================================================== */ + +void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) { + uint32_t temp = *ptr; + temp = (temp & 0xFF00FF00) >> 8 | + (temp & 0x00FF00FF) << 8; + *ptr++ = temp; + } +} + +void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) { + uint32_t temp = *ptr; + temp = (temp & 0xFF000000) >> 24 | + (temp & 0x00FF0000) >> 8 | + (temp & 0x0000FF00) << 8 | + (temp & 0x000000FF) << 24; + *ptr++ = temp; + } +} + +int rc_hash_finalize(const rc_hash_iterator_t* iterator, md5_state_t* md5, char hash[33]) +{ + md5_byte_t digest[16]; + + md5_finish(md5, digest); + + /* NOTE: sizeof(hash) is 4 because it's still treated like a pointer, despite specifying a size */ + snprintf(hash, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); + + rc_hash_iterator_verbose_formatted(iterator, "Generated hash %s", hash); + + return 1; +} + +int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size, const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + + if (buffer_size > MAX_BUFFER_SIZE) + buffer_size = MAX_BUFFER_SIZE; + + md5_init(&md5); + + md5_append(&md5, buffer, (int)buffer_size); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte buffer", (unsigned)buffer_size); + + return rc_hash_finalize(iterator, &md5, hash); +} + +struct rc_buffered_file +{ + const uint8_t* read_ptr; + const uint8_t* data; + size_t data_size; +}; + +static struct rc_buffered_file rc_buffered_file; + +static void* rc_file_open_buffered_file(const char* path) +{ + struct rc_buffered_file* handle = (struct rc_buffered_file*)malloc(sizeof(struct rc_buffered_file)); + (void)path; + + if (handle) + memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + + return handle; +} + +static void rc_file_seek_buffered_file(void* file_handle, int64_t offset, int origin) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + switch (origin) { + case SEEK_SET: buffered_file->read_ptr = buffered_file->data + offset; break; + case SEEK_CUR: buffered_file->read_ptr += offset; break; + case SEEK_END: buffered_file->read_ptr = buffered_file->data + buffered_file->data_size + offset; break; + } + + if (buffered_file->read_ptr < buffered_file->data) + buffered_file->read_ptr = buffered_file->data; + else if (buffered_file->read_ptr > buffered_file->data + buffered_file->data_size) + buffered_file->read_ptr = buffered_file->data + buffered_file->data_size; +} + +static int64_t rc_file_tell_buffered_file(void* file_handle) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + return (buffered_file->read_ptr - buffered_file->data); +} + +static size_t rc_file_read_buffered_file(void* file_handle, void* buffer, size_t requested_bytes) +{ + struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; + const int64_t remaining = buffered_file->data_size - (buffered_file->read_ptr - buffered_file->data); + if ((int)requested_bytes > remaining) + requested_bytes = (int)remaining; + + memcpy(buffer, buffered_file->read_ptr, requested_bytes); + buffered_file->read_ptr += requested_bytes; + return requested_bytes; +} + +static void rc_file_close_buffered_file(void* file_handle) +{ + free(file_handle); +} + +static int rc_hash_file_from_buffer(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) +{ + int result; + + rc_hash_iterator_t buffered_file_iterator; + memset(&buffered_file_iterator, 0, sizeof(buffered_file_iterator)); + memcpy(&buffered_file_iterator.callbacks, &iterator->callbacks, sizeof(iterator->callbacks)); + buffered_file_iterator.userdata = iterator->userdata; + + buffered_file_iterator.callbacks.filereader.open = rc_file_open_buffered_file; + buffered_file_iterator.callbacks.filereader.close = rc_file_close_buffered_file; + buffered_file_iterator.callbacks.filereader.read = rc_file_read_buffered_file; + buffered_file_iterator.callbacks.filereader.seek = rc_file_seek_buffered_file; + buffered_file_iterator.callbacks.filereader.tell = rc_file_tell_buffered_file; + buffered_file_iterator.path = "memory stream"; + + rc_buffered_file.data = rc_buffered_file.read_ptr = iterator->buffer; + rc_buffered_file.data_size = iterator->buffer_size; + + result = rc_hash_from_file(hash, console_id, &buffered_file_iterator); + + buffered_file_iterator.path = NULL; + rc_hash_destroy_iterator(&buffered_file_iterator); + return result; +} + +static int rc_hash_from_buffer(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) +{ + switch (console_id) { + default: + return rc_hash_iterator_error_formatted(iterator, "Unsupported console for buffer hash: %d", console_id); + + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_ARCADIA_2001: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_ADVANCE: + case RC_CONSOLE_GAMEBOY_COLOR: + case RC_CONSOLE_GAME_GEAR: + case RC_CONSOLE_INTELLIVISION: + case RC_CONSOLE_INTERTON_VC_4000: + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGA_DRIVE: + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_MSX: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_PC8800: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: + case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: + case RC_CONSOLE_WONDERSWAN: + case RC_CONSOLE_ZX_SPECTRUM: + return rc_hash_buffer(hash, iterator->buffer, iterator->buffer_size, iterator); + +#ifndef RC_HASH_NO_ROM + case RC_CONSOLE_ARDUBOY: + return rc_hash_arduboy(hash, iterator); + + case RC_CONSOLE_ATARI_7800: + return rc_hash_7800(hash, iterator); + + case RC_CONSOLE_ATARI_LYNX: + return rc_hash_lynx(hash, iterator); + + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + case RC_CONSOLE_NINTENDO: + return rc_hash_nes(hash, iterator); + + case RC_CONSOLE_PC_ENGINE: /* NOTE: does not support PCEngine CD */ + return rc_hash_pce(hash, iterator); + + case RC_CONSOLE_SUPER_CASSETTEVISION: + return rc_hash_scv(hash, iterator); + + case RC_CONSOLE_SUPER_NINTENDO: + return rc_hash_snes(hash, iterator); +#endif + + case RC_CONSOLE_NINTENDO_64: + case RC_CONSOLE_NINTENDO_3DS: + case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: + return rc_hash_file_from_buffer(hash, console_id, iterator); + } +} + +int rc_hash_whole_file(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + uint8_t* buffer; + int64_t size; + const size_t buffer_size = 65536; + void* file_handle; + size_t remaining; + int result = 0; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_END); + size = rc_file_tell(iterator, file_handle); + + if (size > MAX_BUFFER_SIZE) { + rc_hash_iterator_verbose_formatted(iterator, "Hashing first %u bytes (of %u bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(iterator->path)); + remaining = MAX_BUFFER_SIZE; + } + else { + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s (%u bytes)", rc_path_get_filename(iterator->path), (unsigned)size); + remaining = (size_t)size; + } + + md5_init(&md5); + + buffer = (uint8_t*)malloc(buffer_size); + if (buffer) { + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) { + rc_file_read(iterator, file_handle, buffer, (int)buffer_size); + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) { + rc_file_read(iterator, file_handle, buffer, (int)remaining); + md5_append(&md5, buffer, (int)remaining); + } + + free(buffer); + result = rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return result; +} + +int rc_hash_buffered_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) +{ + uint8_t* buffer; + int64_t size; + int result = 0; + void* file_handle; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_END); + size = rc_file_tell(iterator, file_handle); + + if (size > MAX_BUFFER_SIZE) { + rc_hash_iterator_verbose_formatted(iterator, "Buffering first %u bytes (of %d bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(iterator->path)); + size = MAX_BUFFER_SIZE; + } + else { + rc_hash_iterator_verbose_formatted(iterator, "Buffering %s (%d bytes)", rc_path_get_filename(iterator->path), (unsigned)size); + } + + buffer = (uint8_t*)malloc((size_t)size); + if (buffer) { + rc_hash_iterator_t buffer_iterator; + memset(&buffer_iterator, 0, sizeof(buffer_iterator)); + memcpy(&buffer_iterator.callbacks, &iterator->callbacks, sizeof(iterator->callbacks)); + buffer_iterator.userdata = iterator->userdata; + buffer_iterator.path = iterator->path; + buffer_iterator.buffer = buffer; + buffer_iterator.buffer_size = (size_t)size; + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, (int)size); + + result = rc_hash_from_buffer(hash, console_id, &buffer_iterator); + + free(buffer); + } + + rc_file_close(iterator, file_handle); + return result; +} + +static int rc_hash_path_is_absolute(const char* path) +{ + if (!path[0]) + return 0; + + /* "/path/to/file" or "\path\to\file" */ + if (path[0] == '/' || path[0] == '\\') + return 1; + + /* "C:\path\to\file" */ + if (path[1] == ':' && path[2] == '\\') + return 1; + + /* "scheme:/path/to/file" */ + while (*path) { + if (path[0] == ':' && path[1] == '/') + return 1; + + ++path; + } + + return 0; +} + +static const char* rc_hash_get_first_item_from_playlist(const rc_hash_iterator_t* iterator) { + char buffer[1024]; + char* disc_path; + char* ptr, *start, *next; + size_t num_read, path_len, file_len; + void* file_handle; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) { + rc_hash_iterator_error(iterator, "Could not open playlist"); + return NULL; + } + + num_read = rc_file_read(iterator, file_handle, buffer, sizeof(buffer) - 1); + buffer[num_read] = '\0'; + + rc_file_close(iterator, file_handle); + + ptr = start = buffer; + do { + /* ignore empty and commented lines */ + while (*ptr == '#' || *ptr == '\r' || *ptr == '\n') { + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr) + ++ptr; + } + + /* find and extract the current line */ + start = ptr; + while (*ptr && *ptr != '\n') + ++ptr; + next = ptr; + + /* remove trailing whitespace - especially '\r' */ + while (ptr > start && isspace((unsigned char)ptr[-1])) + --ptr; + + /* if we found a non-empty line, break out of the loop to process it */ + file_len = ptr - start; + if (file_len) + break; + + /* did we reach the end of the file? */ + if (!*next) + return NULL; + + /* if the line only contained whitespace, keep searching */ + ptr = next + 1; + } while (1); + + rc_hash_iterator_verbose_formatted(iterator, "Extracted %.*s from playlist", (int)file_len, start); + + start[file_len++] = '\0'; + if (rc_hash_path_is_absolute(start)) + path_len = 0; + else + path_len = rc_path_get_filename(iterator->path) - iterator->path; + + disc_path = (char*)malloc(path_len + file_len + 1); + if (!disc_path) + return NULL; + + if (path_len) + memcpy(disc_path, iterator->path, path_len); + + memcpy(&disc_path[path_len], start, file_len); + return disc_path; +} + +static int rc_hash_generate_from_playlist(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { + rc_hash_iterator_t first_file_iterator; + const char* disc_path; + int result; + + rc_hash_iterator_verbose_formatted(iterator, "Processing playlist: %s", rc_path_get_filename(iterator->path)); + + disc_path = rc_hash_get_first_item_from_playlist(iterator); + if (!disc_path) + return rc_hash_iterator_error(iterator, "Failed to get first item from playlist"); + + memset(&first_file_iterator, 0, sizeof(first_file_iterator)); + memcpy(&first_file_iterator.callbacks, &iterator->callbacks, sizeof(iterator->callbacks)); + first_file_iterator.userdata = iterator->userdata; + first_file_iterator.path = disc_path; /* rc_hash_destory_iterator will free */ + + result = rc_hash_from_file(hash, console_id, &first_file_iterator); + + rc_hash_destroy_iterator(&first_file_iterator); + return result; +} + +static int rc_hash_from_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) +{ + const char* path = iterator->path; + + switch (console_id) { + default: + return rc_hash_iterator_error_formatted(iterator, "Unsupported console for file hash: %d", console_id); + + case RC_CONSOLE_ARCADIA_2001: + case RC_CONSOLE_ATARI_2600: + case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_COLECOVISION: + case RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER: + case RC_CONSOLE_FAIRCHILD_CHANNEL_F: + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_ADVANCE: + case RC_CONSOLE_GAMEBOY_COLOR: + case RC_CONSOLE_GAME_GEAR: + case RC_CONSOLE_INTELLIVISION: + case RC_CONSOLE_INTERTON_VC_4000: + case RC_CONSOLE_MAGNAVOX_ODYSSEY2: + case RC_CONSOLE_MASTER_SYSTEM: + case RC_CONSOLE_MEGADUCK: + case RC_CONSOLE_NEOGEO_POCKET: + case RC_CONSOLE_ORIC: + case RC_CONSOLE_POKEMON_MINI: + case RC_CONSOLE_SEGA_32X: + case RC_CONSOLE_SG1000: + case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: + case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: + case RC_CONSOLE_VECTREX: + case RC_CONSOLE_VIRTUAL_BOY: + case RC_CONSOLE_WASM4: + case RC_CONSOLE_WONDERSWAN: + case RC_CONSOLE_ZX_SPECTRUM: + /* generic whole-file hash - don't buffer */ + return rc_hash_whole_file(hash, iterator); + + case RC_CONSOLE_MEGA_DRIVE: + /* generic whole-file hash with m3u support - don't buffer */ + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_whole_file(hash, iterator); + + case RC_CONSOLE_ATARI_7800: + case RC_CONSOLE_ATARI_LYNX: + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + case RC_CONSOLE_NINTENDO: + case RC_CONSOLE_PC_ENGINE: + case RC_CONSOLE_SUPER_CASSETTEVISION: + case RC_CONSOLE_SUPER_NINTENDO: + /* additional logic whole-file hash - buffer then call rc_hash_generate_from_buffer */ + return rc_hash_buffered_file(hash, console_id, iterator); + + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_MSX: + case RC_CONSOLE_PC8800: + /* generic whole-file hash with m3u support - don't buffer */ + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_whole_file(hash, iterator); + +#ifndef RC_HASH_NO_DISC + case RC_CONSOLE_3DO: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_3do(hash, iterator); +#endif + +#ifndef RC_HASH_NO_ROM + case RC_CONSOLE_ARCADE: + return rc_hash_arcade(hash, iterator); + + case RC_CONSOLE_ARDUBOY: + return rc_hash_arduboy(hash, iterator); +#endif + +#ifndef RC_HASH_NO_DISC + case RC_CONSOLE_ATARI_JAGUAR_CD: + return rc_hash_jaguar_cd(hash, iterator); + + case RC_CONSOLE_DREAMCAST: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_dreamcast(hash, iterator); + + case RC_CONSOLE_GAMECUBE: + return rc_hash_gamecube(hash, iterator); +#endif + +#ifndef RC_HASH_NO_ZIP + case RC_CONSOLE_MS_DOS: + return rc_hash_ms_dos(hash, iterator); +#endif + +#ifndef RC_HASH_NO_DISC + case RC_CONSOLE_NEO_GEO_CD: + return rc_hash_neogeo_cd(hash, iterator); +#endif + +#ifndef RC_HASH_NO_ROM + case RC_CONSOLE_NINTENDO_64: + return rc_hash_n64(hash, iterator); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + case RC_CONSOLE_NINTENDO_3DS: + return rc_hash_nintendo_3ds(hash, iterator); +#endif + +#ifndef RC_HASH_NO_ROM + case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: + return rc_hash_nintendo_ds(hash, iterator); +#endif + +#ifndef RC_HASH_NO_DISC + case RC_CONSOLE_PC_ENGINE_CD: + if (rc_path_compare_extension(path, "cue") || rc_path_compare_extension(path, "chd")) + return rc_hash_pce_cd(hash, iterator); + + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_buffered_file(hash, console_id, iterator); + + case RC_CONSOLE_PCFX: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_pcfx_cd(hash, iterator); + + case RC_CONSOLE_PLAYSTATION: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_psx(hash, iterator); + + case RC_CONSOLE_PLAYSTATION_2: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_ps2(hash, iterator); + + case RC_CONSOLE_PSP: + return rc_hash_psp(hash, iterator); + + case RC_CONSOLE_SEGA_CD: + case RC_CONSOLE_SATURN: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_sega_cd(hash, iterator); + + case RC_CONSOLE_WII: + return rc_hash_wii(hash, iterator); +#endif + } +} + +static void rc_hash_initialize_iterator_from_path(rc_hash_iterator_t* iterator, const char* path); + +static void rc_hash_iterator_append_console(struct rc_hash_iterator* iterator, uint8_t console_id) { + int i = 0; + while (iterator->consoles[i] != 0) { + if (iterator->consoles[i] == console_id) + return; + + ++i; + } + + iterator->consoles[i] = console_id; +} + +void rc_hash_merge_callbacks(rc_hash_iterator_t* iterator, const rc_hash_callbacks_t* callbacks) +{ + if (callbacks->verbose_message) + iterator->callbacks.verbose_message = callbacks->verbose_message; + if (callbacks->error_message) + iterator->callbacks.verbose_message = callbacks->error_message; + + if (callbacks->filereader.open) + memcpy(&iterator->callbacks.filereader, &callbacks->filereader, sizeof(callbacks->filereader)); + +#ifndef RC_HASH_NO_DISC + if (callbacks->cdreader.open_track) + memcpy(&iterator->callbacks.cdreader, &callbacks->cdreader, sizeof(callbacks->cdreader)); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + if (callbacks->encryption.get_3ds_cia_normal_key) + iterator->callbacks.encryption.get_3ds_cia_normal_key = callbacks->encryption.get_3ds_cia_normal_key; + if (callbacks->encryption.get_3ds_ncch_normal_keys) + iterator->callbacks.encryption.get_3ds_ncch_normal_keys = callbacks->encryption.get_3ds_ncch_normal_keys; +#endif +} + + +static void rc_hash_reset_iterator(rc_hash_iterator_t* iterator) { + memset(iterator, 0, sizeof(*iterator)); + iterator->index = -1; + + if (g_verbose_message_callback) + iterator->callbacks.verbose_message = rc_hash_call_g_verbose_message_callback; + if (g_error_message_callback) + iterator->callbacks.error_message = rc_hash_call_g_error_message_callback; + + if (g_filereader) { + memcpy(&iterator->callbacks.filereader, g_filereader, sizeof(*g_filereader)); + } else if (!iterator->callbacks.filereader.open) { + iterator->callbacks.filereader.open = filereader_open; + iterator->callbacks.filereader.close = filereader_close; + iterator->callbacks.filereader.seek = filereader_seek; + iterator->callbacks.filereader.tell = filereader_tell; + iterator->callbacks.filereader.read = filereader_read; + } + +#ifndef RC_HASH_NO_DISC + rc_hash_reset_iterator_disc(iterator); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + rc_hash_reset_iterator_encrypted(iterator); +#endif +} + +static void rc_hash_initialize_iterator_single(rc_hash_iterator_t* iterator, int data) { + iterator->consoles[0] = (uint8_t)data; +} + +static void rc_hash_initialize_iterator_bin(rc_hash_iterator_t* iterator, int data) { + (void)data; + + if (iterator->buffer_size == 0) { + /* raw bin file may be a CD track. if it's more than 32MB, try a CD hash. */ + const int64_t size = rc_file_size(iterator, iterator->path); + if (size > 32 * 1024 * 1024) { + iterator->consoles[0] = RC_CONSOLE_3DO; /* 4DO supports directly opening the bin file */ + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION; /* PCSX ReARMed supports directly opening the bin file*/ + iterator->consoles[2] = RC_CONSOLE_PLAYSTATION_2; /* PCSX2 supports directly opening the bin file*/ + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* Genesis Plus GX supports directly opening the bin file*/ + + /* fallback to megadrive which just does a full hash. */ + iterator->consoles[4] = RC_CONSOLE_MEGA_DRIVE; + return; + } + } + + /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, + * Fairchild Channel F, Arcadia 2001, Interton VC 4000, and Super Cassette Vision. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; +} + +static void rc_hash_initialize_iterator_chd(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[2] = RC_CONSOLE_DREAMCAST; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + iterator->consoles[4] = RC_CONSOLE_PSP; + iterator->consoles[5] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[6] = RC_CONSOLE_3DO; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_PCFX; +} + +static void rc_hash_initialize_iterator_cue(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[2] = RC_CONSOLE_DREAMCAST; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[5] = RC_CONSOLE_3DO; + iterator->consoles[6] = RC_CONSOLE_PCFX; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_ATARI_JAGUAR_CD; +} + +static void rc_hash_initialize_iterator_d88(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PC8800; + iterator->consoles[1] = RC_CONSOLE_SHARPX1; +} + +static void rc_hash_initialize_iterator_dsk(rc_hash_iterator_t* iterator, int data) { + size_t size = iterator->buffer_size; + if (size == 0) + size = (size_t)rc_file_size(iterator, iterator->path); + + (void)data; + + if (size == 512 * 9 * 80) { /* 360KB */ + /* FAT-12 3.5" DD (512 byte sectors, 9 sectors per track, 80 tracks per side */ + /* FAT-12 5.25" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (size == 512 * 9 * 80 * 2) { /* 720KB */ + /* FAT-12 3.5" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + } + else if (size == 512 * 9 * 40) { /* 180KB */ + /* FAT-12 5.25" DD (512 byte sectors, 9 sectors per track, 40 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_MSX; + + /* AMSDOS 3" - 40 tracks */ + iterator->consoles[1] = RC_CONSOLE_AMSTRAD_PC; + } + else if (size == 256 * 16 * 35) { /* 140KB */ + /* Apple II new format - 256 byte sectors, 16 sectors per track, 35 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + else if (size == 256 * 13 * 35) { /* 113.75KB */ + /* Apple II old format - 256 byte sectors, 13 sectors per track, 35 tracks per side */ + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + } + + /* once a best guess has been identified, make sure the others are added as fallbacks */ + + /* check MSX first, as Apple II isn't supported by RetroArch, and RAppleWin won't use the iterator */ + rc_hash_iterator_append_console(iterator, RC_CONSOLE_MSX); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_AMSTRAD_PC); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_ZX_SPECTRUM); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); +} + +static void rc_hash_initialize_iterator_iso(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[1] = RC_CONSOLE_PSP; + iterator->consoles[2] = RC_CONSOLE_3DO; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + iterator->consoles[4] = RC_CONSOLE_GAMECUBE; + iterator->consoles[5] = RC_CONSOLE_WII; +} + +static void rc_hash_initialize_iterator_m3u(rc_hash_iterator_t* iterator, int data) { + const char* first_file_path; + + (void)data; + + /* temporarily set the iterator path to the m3u file so we can extract the + * path of the first disc. rc_hash_get_first_item_from_playlist will return + * an allocated string or NULL, so rc_hash_destroy_iterator won't get tripped + * up by the non-allocted value we're about to assign. + */ + first_file_path = rc_hash_get_first_item_from_playlist(iterator); + if (!first_file_path) /* did not find a disc */ + return; + + /* release the m3u path and replace with the first file path */ + free((void*)iterator->path); + iterator->path = first_file_path; /* assert: already malloc'd; don't need to strdup */ + + iterator->buffer = NULL; /* ignore buffer; assume it's the m3u contents */ + + rc_hash_initialize_iterator_from_path(iterator, iterator->path); +} + +static void rc_hash_initialize_iterator_nib(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + iterator->consoles[1] = RC_CONSOLE_COMMODORE_64; +} + +static void rc_hash_initialize_iterator_rom(rc_hash_iterator_t* iterator, int data) { + (void)data; + + /* rom is associated with MSX, Thomson TO-8, and Fairchild Channel F. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MSX; +} + +static void rc_hash_initialize_iterator_tap(rc_hash_iterator_t* iterator, int data) { + (void)data; + + /* also Oric and ZX Spectrum, but all are full file hashes */ + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; +} + +static const rc_hash_iterator_ext_handler_entry_t rc_hash_iterator_ext_handlers[] = { + { "2d", rc_hash_initialize_iterator_single, RC_CONSOLE_SHARPX1 }, + { "3ds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "3dsx", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "7z", rc_hash_initialize_iterator_single, RC_CONSOLE_ARCADE }, + { "83g", rc_hash_initialize_iterator_single, RC_CONSOLE_TI83 }, /* http://tibasicdev.wikidot.com/file-extensions */ + { "83p", rc_hash_initialize_iterator_single, RC_CONSOLE_TI83 }, + { "a26", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_2600 }, + { "a78", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_7800 }, + { "app", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "arduboy", rc_hash_initialize_iterator_single, RC_CONSOLE_ARDUBOY }, + { "axf", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "bin", rc_hash_initialize_iterator_bin, 0 }, + { "bs", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "cart", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_CASSETTEVISION }, + { "cas", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "cci", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "chd", rc_hash_initialize_iterator_chd, 0 }, + { "chf", rc_hash_initialize_iterator_single, RC_CONSOLE_FAIRCHILD_CHANNEL_F }, + { "cia", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "col", rc_hash_initialize_iterator_single, RC_CONSOLE_COLECOVISION }, + { "csw", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "cue", rc_hash_initialize_iterator_cue, 0 }, + { "cxi", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "d64", rc_hash_initialize_iterator_single, RC_CONSOLE_COMMODORE_64 }, + { "d88", rc_hash_initialize_iterator_d88, 0 }, + { "dosz", rc_hash_initialize_iterator_single, RC_CONSOLE_MS_DOS }, + { "dsk", rc_hash_initialize_iterator_dsk, 0 }, + { "elf", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "fd", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, + { "fds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO }, + { "fig", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "gb", rc_hash_initialize_iterator_single, RC_CONSOLE_GAMEBOY }, + { "gba", rc_hash_initialize_iterator_single, RC_CONSOLE_GAMEBOY_ADVANCE }, + { "gbc", rc_hash_initialize_iterator_single, RC_CONSOLE_GAMEBOY_COLOR }, + { "gdi", rc_hash_initialize_iterator_single, RC_CONSOLE_DREAMCAST }, + { "gg", rc_hash_initialize_iterator_single, RC_CONSOLE_GAME_GEAR }, + { "hex", rc_hash_initialize_iterator_single, RC_CONSOLE_ARDUBOY }, + { "iso", rc_hash_initialize_iterator_iso, 0 }, + { "jag", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_JAGUAR }, + { "k7", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* tape */ + { "lnx", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_LYNX }, + { "m3u", rc_hash_initialize_iterator_m3u, 0 }, + { "m5", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* cartridge */ + { "m7", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* cartridge */ + { "md", rc_hash_initialize_iterator_single, RC_CONSOLE_MEGA_DRIVE }, + { "min", rc_hash_initialize_iterator_single, RC_CONSOLE_POKEMON_MINI }, + { "mx1", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "mx2", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "n64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "ndd", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "nds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_DS }, /* handles both DS and DSi */ + { "nes", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO }, + { "ngc", rc_hash_initialize_iterator_single, RC_CONSOLE_NEOGEO_POCKET }, + { "nib", rc_hash_initialize_iterator_nib, 0 }, + { "pbp", rc_hash_initialize_iterator_single, RC_CONSOLE_PSP }, + { "pce", rc_hash_initialize_iterator_single, RC_CONSOLE_PC_ENGINE }, + { "pgm", rc_hash_initialize_iterator_single, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER }, + { "pzx", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "ri", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "rom", rc_hash_initialize_iterator_rom, 0 }, + { "sap", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* disk */ + { "scl", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "sfc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "sg", rc_hash_initialize_iterator_single, RC_CONSOLE_SG1000 }, + { "sgx", rc_hash_initialize_iterator_single, RC_CONSOLE_PC_ENGINE }, + { "smc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "sv", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPERVISION }, + { "swc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "tap", rc_hash_initialize_iterator_tap, 0 }, + { "tic", rc_hash_initialize_iterator_single, RC_CONSOLE_TIC80 }, + { "trd", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "tvc", rc_hash_initialize_iterator_single, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER }, + { "tzx", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "uze", rc_hash_initialize_iterator_single, RC_CONSOLE_UZEBOX }, + { "v64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "vb", rc_hash_initialize_iterator_single, RC_CONSOLE_VIRTUAL_BOY }, + { "wad", rc_hash_initialize_iterator_single, RC_CONSOLE_WII }, + { "wasm", rc_hash_initialize_iterator_single, RC_CONSOLE_WASM4 }, + { "woz", rc_hash_initialize_iterator_single, RC_CONSOLE_APPLE_II }, + { "wsc", rc_hash_initialize_iterator_single, RC_CONSOLE_WONDERSWAN }, + { "z64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "zip", rc_hash_initialize_iterator_single, RC_CONSOLE_ARCADE } +}; + +const rc_hash_iterator_ext_handler_entry_t* rc_hash_get_iterator_ext_handlers(size_t* num_handlers) { + *num_handlers = sizeof(rc_hash_iterator_ext_handlers) / sizeof(rc_hash_iterator_ext_handlers[0]); + return rc_hash_iterator_ext_handlers; +} + +static int rc_hash_iterator_find_handler(const void* left, const void* right) { + const rc_hash_iterator_ext_handler_entry_t* left_handler = + (const rc_hash_iterator_ext_handler_entry_t*)left; + const rc_hash_iterator_ext_handler_entry_t* right_handler = + (const rc_hash_iterator_ext_handler_entry_t*)right; + + return strcmp(left_handler->ext, right_handler->ext); +} + +static void rc_hash_initialize_iterator_from_path(rc_hash_iterator_t* iterator, const char* path) { + size_t num_handlers; + const rc_hash_iterator_ext_handler_entry_t* handlers = rc_hash_get_iterator_ext_handlers(&num_handlers); + const rc_hash_iterator_ext_handler_entry_t* handler; + rc_hash_iterator_ext_handler_entry_t search; + const char* ext = rc_path_get_extension(path); + size_t index; + + /* lowercase the extension as we copy it into the search object */ + memset(&search, 0, sizeof(search)); + for (index = 0; index < sizeof(search.ext) - 1; ++index) { + const int c = (int)ext[index]; + if (!c) + break; + + search.ext[index] = tolower(c); + } + + /* find the handler for the extension */ + handler = (const rc_hash_iterator_ext_handler_entry_t*) + bsearch(&search, handlers, num_handlers, sizeof(*handler), rc_hash_iterator_find_handler); + if (handler) { + handler->handler(iterator, handler->data); + + if (iterator->callbacks.verbose_message) { + int count = 0; + while (iterator->consoles[count]) + ++count; + + rc_hash_iterator_verbose_formatted(iterator, "Found %d potential consoles for %s file extension", count, ext); + } + } + else { + rc_hash_iterator_error_formatted(iterator, "No console mapping specified for %s file extension - trying full file hash", ext); + + /* if we didn't match the extension, default to something that does a whole file hash */ + if (!iterator->consoles[0]) + iterator->consoles[0] = RC_CONSOLE_GAMEBOY; + } +} + +void rc_hash_initialize_iterator(rc_hash_iterator_t* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) +{ + rc_hash_reset_iterator(iterator); + iterator->buffer = buffer; + iterator->buffer_size = buffer_size; + + if (path) + iterator->path = strdup(path); +} + +void rc_hash_destroy_iterator(rc_hash_iterator_t* iterator) { + if (iterator->path) { + free((void*)iterator->path); + iterator->path = NULL; + } + + iterator->buffer = NULL; +} + +int rc_hash_iterate(char hash[33], rc_hash_iterator_t* iterator) { + int next_console; + int result = 0; + + if (iterator->index == -1) { + rc_hash_initialize_iterator_from_path(iterator, iterator->path); + iterator->index = 0; + } + + do { + next_console = iterator->consoles[iterator->index]; + if (next_console == 0) { + hash[0] = '\0'; + break; + } + + ++iterator->index; + + rc_hash_iterator_verbose_formatted(iterator, "Trying console %d", next_console); + + result = rc_hash_generate(hash, next_console, iterator); + } while (!result); + + return result; +} + +int rc_hash_generate(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { + if (iterator->buffer) + return rc_hash_from_buffer(hash, console_id, iterator); + + return rc_hash_from_file(hash, console_id, iterator); +} + +int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size) { + rc_hash_iterator_t iterator; + int result; + + rc_hash_reset_iterator(&iterator); + iterator.buffer = buffer; + iterator.buffer_size = buffer_size; + + result = rc_hash_from_buffer(hash, console_id, &iterator); + + rc_hash_destroy_iterator(&iterator); + + return result; +} + +int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path){ + rc_hash_iterator_t iterator; + int result; + + rc_hash_reset_iterator(&iterator); + iterator.path = path; + + result = rc_hash_from_file(hash, console_id, &iterator); + + iterator.path = NULL; /* prevent free. we didn't strdup */ + + rc_hash_destroy_iterator(&iterator); + + return result; +} diff --git a/src/rcheevos/src/rhash/hash_disc.c b/src/rcheevos/src/rhash/hash_disc.c new file mode 100644 index 0000000000..83c93545e3 --- /dev/null +++ b/src/rcheevos/src/rhash/hash_disc.c @@ -0,0 +1,1340 @@ +#include "rc_hash.h" + +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include + +/* ===================================================== */ + +static struct rc_hash_cdreader g_cdreader_funcs; +static struct rc_hash_cdreader* g_cdreader = NULL; + +void rc_hash_reset_iterator_disc(rc_hash_iterator_t* iterator) +{ + if (g_cdreader) + memcpy(&iterator->callbacks.cdreader, g_cdreader, sizeof(*g_cdreader)); + else + rc_hash_get_default_cdreader(&iterator->callbacks.cdreader); +} + +void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader) +{ + if (reader) { + memcpy(&g_cdreader_funcs, reader, sizeof(g_cdreader_funcs)); + g_cdreader = &g_cdreader_funcs; + } + else { + g_cdreader = NULL; + } +} + +static void* rc_cd_open_track(const rc_hash_iterator_t* iterator, uint32_t track) +{ + if (iterator->callbacks.cdreader.open_track_iterator) + return iterator->callbacks.cdreader.open_track_iterator(iterator->path, track, iterator); + + if (iterator->callbacks.cdreader.open_track) + return iterator->callbacks.cdreader.open_track(iterator->path, track); + + if (g_cdreader && g_cdreader->open_track) + return g_cdreader->open_track(iterator->path, track); + + rc_hash_iterator_error(iterator, "no hook registered for cdreader_open_track"); + return NULL; +} + +static size_t rc_cd_read_sector(const rc_hash_iterator_t* iterator, void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + if (iterator->callbacks.cdreader.read_sector) + return iterator->callbacks.cdreader.read_sector(track_handle, sector, buffer, requested_bytes); + + if (g_cdreader && g_cdreader->read_sector) + return g_cdreader->read_sector(track_handle, sector, buffer, requested_bytes); + + rc_hash_iterator_error(iterator, "no hook registered for cdreader_read_sector"); + return 0; +} + +static uint32_t rc_cd_first_track_sector(const rc_hash_iterator_t* iterator, void* track_handle) +{ + if (iterator->callbacks.cdreader.first_track_sector) + return iterator->callbacks.cdreader.first_track_sector(track_handle); + + if (g_cdreader && g_cdreader->first_track_sector) + return g_cdreader->first_track_sector(track_handle); + + rc_hash_iterator_error(iterator, "no hook registered for cdreader_first_track_sector"); + return 0; +} + +static void rc_cd_close_track(const rc_hash_iterator_t* iterator, void* track_handle) +{ + if (iterator->callbacks.cdreader.close_track) + iterator->callbacks.cdreader.close_track(track_handle); + else if (g_cdreader && g_cdreader->close_track) + g_cdreader->close_track(track_handle); + else + rc_hash_iterator_error(iterator, "no hook registered for cdreader_close_track"); +} + +static uint32_t rc_cd_find_file_sector(const rc_hash_iterator_t* iterator, void* track_handle, const char* path, uint32_t* size) +{ + uint8_t buffer[2048], *tmp; + int sector; + uint32_t num_sectors = 0; + size_t filename_length; + const char* slash; + + if (!track_handle) + return 0; + + /* we start at the root. don't need to explicitly find it */ + if (*path == '\\') + ++path; + + filename_length = strlen(path); + slash = strrchr(path, '\\'); + if (slash) { + /* find the directory record for the first part of the path */ + memcpy(buffer, path, slash - path); + buffer[slash - path] = '\0'; + + sector = rc_cd_find_file_sector(iterator, track_handle, (const char *)buffer, NULL); + if (!sector) + return 0; + + ++slash; + filename_length -= (slash - path); + path = slash; + } + else { + uint32_t logical_block_size; + + /* find the cd information */ + if (!rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle) + 16, buffer, 256)) + return 0; + + /* the directory_record starts at 156, the sector containing the table of contents is 2 bytes into that. + * https://www.cdroller.com/htm/readdata.html + */ + sector = buffer[156 + 2] | (buffer[156 + 3] << 8) | (buffer[156 + 4] << 16); + + /* if the table of contents spans more than one sector, it's length of section will exceed it's logical block size */ + logical_block_size = (buffer[128] | (buffer[128 + 1] << 8)); /* logical block size */ + if (logical_block_size == 0) { + num_sectors = 1; + } else { + num_sectors = (buffer[156 + 10] | (buffer[156 + 11] << 8) | (buffer[156 + 12] << 16) | (buffer[156 + 13] << 24)); /* length of section */ + num_sectors /= logical_block_size; + } + } + + /* fetch and process the directory record */ + if (!rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer))) + return 0; + + tmp = buffer; + do { + if (tmp >= buffer + sizeof(buffer) || !*tmp) { + /* end of this path table block. if the path table spans multiple sectors, keep scanning */ + if (num_sectors > 1) { + --num_sectors; + if (rc_cd_read_sector(iterator, track_handle, ++sector, buffer, sizeof(buffer))) { + tmp = buffer; + continue; + } + } + break; + } + + /* filename is 33 bytes into the record and the format is "FILENAME;version" or "DIRECTORY" */ + if ((tmp[32] == filename_length || tmp[33 + filename_length] == ';') && + strncasecmp((const char*)(tmp + 33), path, filename_length) == 0) { + sector = tmp[2] | (tmp[3] << 8) | (tmp[4] << 16); + + rc_hash_iterator_verbose_formatted(iterator, "Found %s at sector %d", path, sector); + + if (size) + *size = tmp[10] | (tmp[11] << 8) | (tmp[12] << 16) | (tmp[13] << 24); + + return sector; + } + + /* the first byte of the record is the length of the record */ + tmp += *tmp; + } while (1); + + return 0; +} + +/* ===================================================== */ + +static int rc_hash_cd_file(md5_state_t* md5, const rc_hash_iterator_t* iterator, void* track_handle, uint32_t sector, const char* name, uint32_t size, const char* description) +{ + uint8_t buffer[2048]; + size_t num_read; + + if ((num_read = rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer))) < sizeof(buffer)) + return rc_hash_iterator_error_formatted(iterator, "Could not read %s", description); + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + if (name) + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s title (%u bytes) and contents (%u bytes) ", name, (unsigned)strlen(name), size); + else + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s contents (%u bytes @ sector %u)", description, size, sector); + + if (size < (unsigned)num_read) /* we read a whole sector - only hash the part containing file data */ + num_read = (size_t)size; + + do { + md5_append(md5, buffer, (int)num_read); + + if (size <= (unsigned)num_read) + break; + size -= (unsigned)num_read; + + ++sector; + if (size >= sizeof(buffer)) + num_read = rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + else + num_read = rc_cd_read_sector(iterator, track_handle, sector, buffer, size); + } while (num_read > 0); + + return 1; +} + +/* ===================================================== */ + +int rc_hash_3do(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2048]; + const uint8_t operafs_identifier[7] = { 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01 }; + void* track_handle; + md5_state_t md5; + int sector; + int block_size, block_location; + int offset, stop; + size_t size = 0; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* the Opera filesystem stores the volume information in the first 132 bytes of sector 0 + * https://github.com/barbeque/3dodump/blob/master/OperaFS-Format.md + */ + rc_cd_read_sector(iterator, track_handle, 0, buffer, 132); + + if (memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) { + rc_hash_iterator_verbose_formatted(iterator, "Found 3DO CD, title=%.32s", &buffer[0x28]); + + /* include the volume header in the hash */ + md5_init(&md5); + md5_append(&md5, buffer, 132); + + /* the block size is at offset 0x4C (assume 0x4C is always 0) */ + block_size = buffer[0x4D] * 65536 + buffer[0x4E] * 256 + buffer[0x4F]; + + /* the root directory block location is at offset 0x64 (and duplicated several + * times, but we just look at the primary record) (assume 0x64 is always 0)*/ + block_location = buffer[0x65] * 65536 + buffer[0x66] * 256 + buffer[0x67]; + + /* multiply the block index by the block size to get the real address */ + block_location *= block_size; + + /* convert that to a sector and read it */ + sector = block_location / 2048; + + do { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + + /* offset to start of entries is at offset 0x10 (assume 0x10 and 0x11 are always 0) */ + offset = buffer[0x12] * 256 + buffer[0x13]; + + /* offset to end of entries is at offset 0x0C (assume 0x0C is always 0) */ + stop = buffer[0x0D] * 65536 + buffer[0x0E] * 256 + buffer[0x0F]; + + while (offset < stop) { + if (buffer[offset + 0x03] == 0x02) { /* file */ + if (strcasecmp((const char*)&buffer[offset + 0x20], "LaunchMe") == 0) { + /* the block size is at offset 0x0C (assume 0x0C is always 0) */ + block_size = buffer[offset + 0x0D] * 65536 + buffer[offset + 0x0E] * 256 + buffer[offset + 0x0F]; + + /* the block location is at offset 0x44 (assume 0x44 is always 0) */ + block_location = buffer[offset + 0x45] * 65536 + buffer[offset + 0x46] * 256 + buffer[offset + 0x47]; + block_location *= block_size; + + /* the file size is at offset 0x10 (assume 0x10 is always 0) */ + size = (size_t)buffer[offset + 0x11] * 65536 + buffer[offset + 0x12] * 256 + buffer[offset + 0x13]; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing header (%u bytes) and %.32s (%u bytes) ", 132, &buffer[offset + 0x20], (unsigned)size); + + break; + } + } + + /* the number of extra copies of the file is at offset 0x40 (assume 0x40-0x42 are always 0) */ + offset += 0x48 + buffer[offset + 0x43] * 4; + } + + if (size != 0) + break; + + /* did not find the file, see if the directory listing is continued in another sector */ + offset = buffer[0x02] * 256 + buffer[0x03]; + + /* no more sectors to search*/ + if (offset == 0xFFFF) + break; + + /* get next sector */ + offset *= block_size; + sector = (block_location + offset) / 2048; + } while (1); + + if (size == 0) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Could not find LaunchMe"); + } + + sector = block_location / 2048; + + while (size > 2048) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= 2048; + } + + rc_cd_read_sector(iterator, track_handle, sector, buffer, size); + md5_append(&md5, buffer, (int)size); + } + else { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a 3DO CD"); + } + + rc_cd_close_track(iterator, track_handle); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_dreamcast(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[256] = ""; + void* track_handle; + char exe_file[32] = ""; + uint32_t size; + uint32_t sector; + int result = 0; + md5_state_t md5; + int i = 0; + + /* track 03 is the data track that contains the TOC and IP.BIN */ + track_handle = rc_cd_open_track(iterator, 3); + if (track_handle) { + /* first 256 bytes from first sector should have IP.BIN structure that stores game meta information + * https://mc.pp.se/dc/ip.bin.html */ + rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle), buffer, sizeof(buffer)); + } + + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) { + if (track_handle) + rc_cd_close_track(iterator, track_handle); + + /* not a gd-rom dreamcast file. check for mil-cd by looking for the marker in the first data track */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle), buffer, sizeof(buffer)); + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) { + /* did not find marker on track 3 or first data track */ + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a Dreamcast CD"); + } + } + + /* start the hash with the game meta information */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)buffer, 256); + + if (iterator->callbacks.verbose_message) { + uint8_t* ptr = &buffer[0xFF]; + while (ptr > &buffer[0x80] && ptr[-1] == ' ') + --ptr; + *ptr = '\0'; + + rc_hash_iterator_verbose_formatted(iterator, "Found Dreamcast CD: %.128s (%.16s)", (const char*)&buffer[0x80], (const char*)&buffer[0x40]); + } + + /* the boot filename is 96 bytes into the meta information (https://mc.pp.se/dc/ip0000.bin.html) */ + /* remove whitespace from bootfile */ + i = 0; + while (!isspace((unsigned char)buffer[96 + i]) && i < 16) + ++i; + + /* sometimes boot file isn't present on meta information. + * nothing can be done, as even the core doesn't run the game in this case. */ + if (i == 0) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Boot executable not specified on IP.BIN"); + } + + memcpy(exe_file, &buffer[96], i); + exe_file[i] = '\0'; + + sector = rc_cd_find_file_sector(iterator, track_handle, exe_file, &size); + if (sector == 0) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Could not locate boot executable"); + } + + if (rc_cd_read_sector(iterator, track_handle, sector, buffer, 1)) { + /* the boot executable is in the primary data track */ + } + else { + rc_cd_close_track(iterator, track_handle); + + /* the boot executable is normally in the last track */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_LAST); + } + + result = rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, "boot executable"); + rc_cd_close_track(iterator, track_handle); + + rc_hash_finalize(iterator, &md5, hash); + return result; +} + +static int rc_hash_nintendo_disc_partition(md5_state_t* md5, const rc_hash_iterator_t* iterator, + void* file_handle, const uint32_t part_offset, uint8_t wii_shift) +{ + const uint32_t BASE_HEADER_SIZE = 0x2440; + const uint32_t MAX_HEADER_SIZE = 1024 * 1024; + + uint32_t apploader_header_size, apploader_body_size, apploader_trailer_size, header_size; + + uint8_t quad_buffer[4]; + uint8_t addr_buffer[0xD8]; + uint8_t* buffer; + + uint64_t dol_offset; + uint64_t dol_offsets[18]; + uint64_t dol_sizes[18]; + + uint8_t ix; + uint64_t remaining_size; + const uint32_t MAX_CHUNK_SIZE = 1024 * 1024; + + /* GetApploaderSize */ + rc_file_seek(iterator, file_handle, part_offset + BASE_HEADER_SIZE + 0x14, SEEK_SET); + apploader_header_size = 0x20; + rc_file_read(iterator, file_handle, quad_buffer, 4); + apploader_body_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(iterator, file_handle, quad_buffer, 4); + apploader_trailer_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + header_size = BASE_HEADER_SIZE + apploader_header_size + apploader_body_size + apploader_trailer_size; + if (header_size > MAX_HEADER_SIZE) header_size = MAX_HEADER_SIZE; + + /* Hash headers */ + buffer = (uint8_t*)malloc(header_size); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + rc_file_seek(iterator, file_handle, part_offset, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, header_size); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte partition header", header_size); + md5_append(md5, buffer, header_size); + + /* GetBootDOLOffset + * Base header size is guaranteed larger than 0x423 therefore buffer contains dol_offset right now + */ + dol_offset = (((uint64_t)buffer[0x420] << 24) | + ((uint64_t)buffer[0x421] << 16) | + ((uint64_t)buffer[0x422] << 8) | + (uint64_t)buffer[0x423]) << wii_shift; + free(buffer); + + /* Find offsets and sizes for the 7 main.dol code segments and 11 main.dol data segments */ + rc_file_seek(iterator, file_handle, part_offset + dol_offset, SEEK_SET); + rc_file_read(iterator, file_handle, addr_buffer, 0xD8); + for (ix = 0; ix < 18; ix++) { + dol_offsets[ix] = (((uint64_t)addr_buffer[0x0 + ix * 4] << 24) | + ((uint64_t)addr_buffer[0x1 + ix * 4] << 16) | + ((uint64_t)addr_buffer[0x2 + ix * 4] << 8) | + (uint64_t)addr_buffer[0x3 + ix * 4]) << wii_shift; + dol_sizes[ix] = (((uint64_t)addr_buffer[0x90 + ix * 4] << 24) | + ((uint64_t)addr_buffer[0x91 + ix * 4] << 16) | + ((uint64_t)addr_buffer[0x92 + ix * 4] << 8) | + (uint64_t)addr_buffer[0x93 + ix * 4]) << wii_shift; + } + + /* Iterate through the 18 main.dol segments and hash each */ + buffer = (uint8_t*)malloc(MAX_CHUNK_SIZE); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + + for (ix = 0; ix < 18; ix++) { + if (dol_sizes[ix] == 0) + continue; + + rc_file_seek(iterator, file_handle, part_offset + dol_offsets[ix], SEEK_SET); + if (ix < 7) + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main.dol code segment %u", dol_sizes[ix], ix); + else + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main.dol data segment %u", dol_sizes[ix], ix - 7); + + remaining_size = dol_sizes[ix]; + while (remaining_size > MAX_CHUNK_SIZE) { + rc_file_read(iterator, file_handle, buffer, MAX_CHUNK_SIZE); + md5_append(md5, buffer, MAX_CHUNK_SIZE); + remaining_size -= MAX_CHUNK_SIZE; + } + rc_file_read(iterator, file_handle, buffer, (int32_t)remaining_size); + md5_append(md5, buffer, (int32_t)remaining_size); + } + + free(buffer); + return 1; +} + +int rc_hash_gamecube(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + void* file_handle; + + uint8_t quad_buffer[4]; + uint8_t success; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + md5_init(&md5); + /* Check Magic Word */ + rc_file_seek(iterator, file_handle, 0x1c, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 0xC2 && quad_buffer[1] == 0x33 && quad_buffer[2] == 0x9F && quad_buffer[3] == 0x3D) + success = rc_hash_nintendo_disc_partition(&md5, iterator, file_handle, 0, 0); + else + success = rc_hash_iterator_error(iterator, "Not a Gamecube disc"); + + /* Finalize */ + rc_file_close(iterator, file_handle); + + if (success) + return rc_hash_finalize(iterator, &md5, hash); + + return 0; +} + +/* helper variable only used for testing */ +const char* _rc_hash_jaguar_cd_homebrew_hash = NULL; + +int rc_hash_jaguar_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2352]; + void* track_handle; + md5_state_t md5; + int byteswapped = 0; + uint32_t size = 0; + uint32_t offset = 0; + uint32_t sector = 0; + uint32_t remaining; + uint32_t i; + + /* Jaguar CD header is in the first sector of the first data track OF THE SECOND SESSION. + * The first track must be an audio track, but may be a warning message or actual game audio */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* The header is an unspecified distance into the first sector, but usually two bytes in. + * It consists of 64 bytes of "TAIR" or "ATRI" repeating, depending on whether or not the data + * is byteswapped. Then another 32 byte that reads "ATARI APPROVED DATA HEADER ATRI " + * (possibly byteswapped). Then a big-endian 32-bit value for the address where the boot code + * should be loaded, and a second big-endian 32-bit value for the size of the boot code. */ + sector = rc_cd_first_track_sector(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + + for (i = 64; i < sizeof(buffer) - 32 - 4 * 3; i++) { + if (memcmp(&buffer[i], "TARA IPARPVODED TA AEHDAREA RT I", 32) == 0) { + byteswapped = 1; + offset = i + 32 + 4; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + break; + } + else if (memcmp(&buffer[i], "ATARI APPROVED DATA HEADER ATRI ", 32) == 0) { + byteswapped = 0; + offset = i + 32 + 4; + size = (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 3]); + break; + } + } + + if (size == 0) { /* did not see ATARI APPROVED DATA HEADER */ + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a Jaguar CD"); + } + + i = 0; /* only loop once */ + do { + md5_init(&md5); + + offset += 4; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing boot executable (%u bytes starting at %u bytes into sector %u)", size, offset, sector); + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + do { + if (byteswapped) + rc_hash_byteswap16(buffer, &buffer[sizeof(buffer)]); + + remaining = sizeof(buffer) - offset; + if (remaining >= size) { + md5_append(&md5, &buffer[offset], size); + size = 0; + break; + } + + md5_append(&md5, &buffer[offset], remaining); + size -= remaining; + offset = 0; + } while (rc_cd_read_sector(iterator, track_handle, ++sector, buffer, sizeof(buffer)) == sizeof(buffer)); + + rc_cd_close_track(iterator, track_handle); + + if (size > 0) + return rc_hash_iterator_error(iterator, "Not enough data"); + + rc_hash_finalize(iterator, &md5, hash); + + /* homebrew games all seem to have the same boot executable and store the actual game code in track 2. + * if we generated something other than the homebrew hash, return it. assume all homebrews are byteswapped. */ + if (strcmp(hash, "254487b59ab21bc005338e85cbf9fd2f") != 0 || !byteswapped) { + if (_rc_hash_jaguar_cd_homebrew_hash == NULL || strcmp(hash, _rc_hash_jaguar_cd_homebrew_hash) != 0) + return 1; + } + + /* if we've already been through the loop a second time, just return the hash */ + if (i == 1) + return 1; + ++i; + + rc_hash_iterator_verbose_formatted(iterator, "Potential homebrew at sector %u, checking for KART data in track 2", sector); + + track_handle = rc_cd_open_track(iterator, 2); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* track 2 of the homebrew code has the 64 bytes or ATRI followed by 32 bytes of "ATARI APPROVED DATA HEADER ATRI!", + * then 64 bytes of KART repeating. */ + sector = rc_cd_first_track_sector(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + if (memcmp(&buffer[0x5E], "RT!IRTKA", 8) != 0) + return rc_hash_iterator_error(iterator, "Homebrew executable not found in track 2"); + + /* found KART data*/ + rc_hash_iterator_verbose(iterator, "Found KART data in track 2"); + + offset = 0xA6; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + } while (1); +} + +int rc_hash_neogeo_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + char buffer[1024], *ptr; + void* track_handle; + uint32_t sector; + uint32_t size; + md5_state_t md5; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* https://wiki.neogeodev.org/index.php?title=IPL_file, https://wiki.neogeodev.org/index.php?title=PRG_file + * IPL file specifies data to be loaded before the game starts. PRG files are the executable code + */ + sector = rc_cd_find_file_sector(iterator, track_handle, "IPL.TXT", &size); + if (!sector) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a NeoGeo CD game disc"); + } + + if (rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)) == 0) { + rc_cd_close_track(iterator, track_handle); + return 0; + } + + md5_init(&md5); + + buffer[sizeof(buffer) - 1] = '\0'; + ptr = &buffer[0]; + do { + char* start = ptr; + while (*ptr && *ptr != '.') + ++ptr; + + if (strncasecmp(ptr, ".PRG", 4) == 0) { + ptr += 4; + *ptr++ = '\0'; + + sector = rc_cd_find_file_sector(iterator, track_handle, start, &size); + if (!sector || !rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, start)) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error_formatted(iterator, "Could not read %.16s", start); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr != '\n') + break; + ++ptr; + } while (*ptr != '\0' && *ptr != '\x1a'); + + rc_cd_close_track(iterator, track_handle); + return rc_hash_finalize(iterator, &md5, hash); +} + +static int rc_hash_pce_track(char hash[33], void* track_handle, const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2048]; + md5_state_t md5; + uint32_t sector, num_sectors; + uint32_t size; + + /* the PC-Engine uses the second sector to specify boot information and program name. + * the string "PC Engine CD-ROM SYSTEM" should exist at 32 bytes into the sector + * http://shu.sheldows.com/shu/download/pcedocs/pce_cdrom.html + */ + if (rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle) + 1, buffer, 128) < 128) + return rc_hash_iterator_error(iterator, "Not a PC Engine CD"); + + /* normal PC Engine CD will have a header block in sector 1 */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) { + /* the title of the disc is the last 22 bytes of the header */ + md5_init(&md5); + md5_append(&md5, &buffer[106], 22); + + buffer[128] = '\0'; + rc_hash_iterator_verbose_formatted(iterator, "Found PC Engine CD, title=%.22s", &buffer[106]); + + /* the first three bytes specify the sector of the program data, and the fourth byte + * is the number of sectors. + */ + sector = (buffer[0] << 16) + (buffer[1] << 8) + buffer[2]; + num_sectors = buffer[3]; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %d sectors starting at sector %d", num_sectors, sector); + + sector += rc_cd_first_track_sector(iterator, track_handle); + while (num_sectors > 0) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + /* GameExpress CDs use a standard Joliet filesystem - locate and hash the BOOT.BIN */ + else if ((sector = rc_cd_find_file_sector(iterator, track_handle, "BOOT.BIN", &size)) != 0 && size < MAX_BUFFER_SIZE) { + md5_init(&md5); + while (size > sizeof(buffer)) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= sizeof(buffer); + } + + if (size > 0) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, size); + md5_append(&md5, buffer, size); + } + } + else { + return rc_hash_iterator_error(iterator, "Not a PC Engine CD"); + } + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_pce_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + int result; + void* track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + result = rc_hash_pce_track(hash, track_handle, iterator); + + rc_cd_close_track(iterator, track_handle); + + return result; +} + +int rc_hash_pcfx_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2048]; + void* track_handle; + md5_state_t md5; + int sector, num_sectors; + + /* PC-FX executable can be in any track. Assume it's in the largest data track and check there first */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_LARGEST); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* PC-FX CD will have a header marker in sector 0 */ + sector = rc_cd_first_track_sector(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, 32); + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) != 0) { + rc_cd_close_track(iterator, track_handle); + + /* not found in the largest data track, check track 2 */ + track_handle = rc_cd_open_track(iterator, 2); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + sector = rc_cd_first_track_sector(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, 32); + } + + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) == 0) { + /* PC-FX boot header fills the first two sectors of the disc + * https://bitbucket.org/trap15/pcfxtools/src/master/pcfx-cdlink.c + * the important stuff is the first 128 bytes of the second sector (title being the first 32) */ + rc_cd_read_sector(iterator, track_handle, sector + 1, buffer, 128); + + md5_init(&md5); + md5_append(&md5, buffer, 128); + + rc_hash_iterator_verbose_formatted(iterator, "Found PC-FX CD, title=%.32s", &buffer[0]); + + /* the program sector is in bytes 33-36 (assume byte 36 is 0) */ + sector = (buffer[34] << 16) + (buffer[33] << 8) + buffer[32]; + + /* the number of sectors the program occupies is in bytes 37-40 (assume byte 40 is 0) */ + num_sectors = (buffer[38] << 16) + (buffer[37] << 8) + buffer[36]; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %d sectors starting at sector %d", num_sectors, sector); + + sector += rc_cd_first_track_sector(iterator, track_handle); + while (num_sectors > 0) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + else { + int result = 0; + rc_cd_read_sector(iterator, track_handle, sector + 1, buffer, 128); + + /* some PC-FX CDs still identify as PCE CDs */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) + result = rc_hash_pce_track(hash, track_handle, iterator); + + rc_cd_close_track(iterator, track_handle); + if (result) + return result; + + return rc_hash_iterator_error(iterator, "Not a PC-FX CD"); + } + + rc_cd_close_track(iterator, track_handle); + + return rc_hash_finalize(iterator, &md5, hash); +} + +static int rc_hash_find_playstation_executable(const rc_hash_iterator_t* iterator, void* track_handle, + const char* boot_key, const char* cdrom_prefix, + char exe_name[], uint32_t exe_name_size, uint32_t* exe_size) +{ + uint8_t buffer[2048]; + uint32_t size; + char* ptr; + char* start; + const size_t boot_key_len = strlen(boot_key); + const size_t cdrom_prefix_len = strlen(cdrom_prefix); + int sector; + + sector = rc_cd_find_file_sector(iterator, track_handle, "SYSTEM.CNF", NULL); + if (!sector) + return 0; + + size = (uint32_t)rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer) - 1); + buffer[size] = '\0'; + + sector = 0; + for (ptr = (char*)buffer; *ptr; ++ptr) { + if (strncmp(ptr, boot_key, boot_key_len) == 0) { + ptr += boot_key_len; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (*ptr == '=') { + ++ptr; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) + ptr += cdrom_prefix_len; + while (*ptr == '\\') + ++ptr; + + start = ptr; + while (!isspace((unsigned char)*ptr) && *ptr != ';') + ++ptr; + + size = (uint32_t)(ptr - start); + if (size >= exe_name_size) + size = exe_name_size - 1; + + memcpy(exe_name, start, size); + exe_name[size] = '\0'; + + rc_hash_iterator_verbose_formatted(iterator, "Looking for boot executable: %s", exe_name); + + sector = rc_cd_find_file_sector(iterator, track_handle, exe_name, exe_size); + break; + } + } + + /* advance to end of line */ + while (*ptr && *ptr != '\n') + ++ptr; + } + + return sector; +} + +int rc_hash_psx(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[32]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + uint32_t size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + sector = rc_hash_find_playstation_executable(iterator, track_handle, "BOOT", "cdrom:", exe_name, sizeof(exe_name), &size); + if (!sector) { + sector = rc_cd_find_file_sector(iterator, track_handle, "PSX.EXE", &size); + if (sector) + memcpy(exe_name, "PSX.EXE", 8); + } + + if (!sector) { + rc_hash_iterator_error(iterator, "Could not locate primary executable"); + } + else if (rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) { + rc_hash_iterator_error(iterator, "Could not read primary executable"); + } + else { + if (memcmp(buffer, "PS-X EXE", 7) != 0) { + rc_hash_iterator_verbose_formatted(iterator, "%s did not contain PS-X EXE marker", exe_name); + } + else { + /* the PS-X EXE header specifies the executable size as a 4-byte value 28 bytes into the header, which doesn't + * include the header itself. We want to include the header in the hash, so append another 2048 to that value. + */ + size = (((uint8_t)buffer[31] << 24) | ((uint8_t)buffer[30] << 16) | ((uint8_t)buffer[29] << 8) | (uint8_t)buffer[28]) + 2048; + } + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, iterator, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(iterator, &md5, hash); + } + + rc_cd_close_track(iterator, track_handle); + + return result; +} + +int rc_hash_ps2(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[4]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + uint32_t size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + sector = rc_hash_find_playstation_executable(iterator, track_handle, "BOOT2", "cdrom0:", exe_name, sizeof(exe_name), &size); + if (!sector) { + rc_hash_iterator_error(iterator, "Could not locate primary executable"); + } + else if (rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) { + rc_hash_iterator_error(iterator, "Could not read primary executable"); + } + else { + if (memcmp(buffer, "\x7f\x45\x4c\x46", 4) != 0) + rc_hash_iterator_verbose_formatted(iterator, "%s did not contain ELF marker", exe_name); + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, iterator, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(iterator, &md5, hash); + } + + rc_cd_close_track(iterator, track_handle); + + return result; +} + +int rc_hash_psp(char hash[33], const rc_hash_iterator_t* iterator) +{ + void* track_handle; + uint32_t sector; + uint32_t size; + md5_state_t md5; + + /* https://www.psdevwiki.com/psp/PBP + * A PBP file is an archive containing the PARAM.SFO, primary executable, and a bunch of metadata. + * While we could extract the PARAM.SFO and primary executable to mimic the normal PSP hashing logic, + * it's easier to just hash the entire file. This also helps alleviate issues where the primary + * executable is just a game engine and the only differentiating data would be the metadata. */ + if (rc_path_compare_extension(iterator->path, "pbp")) + return rc_hash_whole_file(hash, iterator); + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* http://www.romhacking.net/forum/index.php?topic=30899.0 + * PSP_GAME/PARAM.SFO contains key/value pairs identifying the game for the system (i.e. serial number, + * name, version). PSP_GAME/SYSDIR/EBOOT.BIN is the encrypted primary executable. + */ + sector = rc_cd_find_file_sector(iterator, track_handle, "PSP_GAME\\PARAM.SFO", &size); + if (!sector) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a PSP game disc"); + } + + md5_init(&md5); + if (!rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, "PSP_GAME\\PARAM.SFO")) { + rc_cd_close_track(iterator, track_handle); + return 0; + } + + sector = rc_cd_find_file_sector(iterator, track_handle, "PSP_GAME\\SYSDIR\\EBOOT.BIN", &size); + if (!sector) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Could not find primary executable"); + } + + if (!rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, "PSP_GAME\\SYSDIR\\EBOOT.BIN")) { + rc_cd_close_track(iterator, track_handle); + return 0; + } + + rc_cd_close_track(iterator, track_handle); + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_sega_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[512]; + void* track_handle; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* the first 512 bytes of sector 0 are a volume header and ROM header that uniquely identify the game. + * After that is an arbitrary amount of code that ensures the game is being run in the correct region. + * Then more arbitrary code follows that actually starts the boot process. Somewhere in there, the + * primary executable is loaded. In many cases, a single game will have multiple executables, so even + * if we could determine the primary one, it's just the tip of the iceberg. As such, we've decided that + * hashing the volume and ROM headers is sufficient for identifying the game, and we'll have to trust + * that our players aren't modifying anything else on the disc. + */ + rc_cd_read_sector(iterator, track_handle, 0, buffer, sizeof(buffer)); + rc_cd_close_track(iterator, track_handle); + + if (memcmp(buffer, "SEGADISCSYSTEM ", 16) != 0 && /* Sega CD */ + memcmp(buffer, "SEGA SEGASATURN ", 16) != 0) { /* Sega Saturn */ + return rc_hash_iterator_error(iterator, "Not a Sega CD"); + } + + return rc_hash_buffer(hash, buffer, sizeof(buffer), iterator); +} + +static int rc_hash_wii_disc(md5_state_t* md5, const rc_hash_iterator_t* iterator, void* file_handle) +{ + const uint32_t MAIN_HEADER_SIZE = 0x80; + const uint64_t REGION_CODE_ADDRESS = 0x4E000; + const uint32_t CLUSTER_SIZE = 0x7C00; + const uint32_t MAX_CLUSTER_COUNT = 1024; + + uint32_t partition_info_table[8]; + uint32_t total_partition_count = 0; + uint32_t* partition_table; + uint64_t tmd_offset; + uint32_t tmd_size; + uint64_t part_offset; + uint64_t part_size; + uint32_t cluster_count; + + uint8_t quad_buffer[4]; + uint8_t* buffer; + + uint32_t ix, jx, kx; + uint8_t encrypted; + + /* Check encryption byte - if 0x61 is 0, disc is encrypted */ + rc_file_seek(iterator, file_handle, 0x61, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 1); + encrypted = (quad_buffer[0] == 0); + + /* Hash main headers */ + buffer = (uint8_t*)malloc(CLUSTER_SIZE); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main header for [%c%c%c%c%c%c]", + MAIN_HEADER_SIZE, buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]); + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, MAIN_HEADER_SIZE); + md5_append(md5, buffer, MAIN_HEADER_SIZE); + + /* Hash region code */ + rc_file_seek(iterator, file_handle, REGION_CODE_ADDRESS, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + md5_append(md5, quad_buffer, 4); + + /* Scan partition table */ + rc_file_seek(iterator, file_handle, 0x40000, SEEK_SET); + for (ix = 0; ix < 8; ix++) { + rc_file_read(iterator, file_handle, quad_buffer, 4); + partition_info_table[ix] = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + if (ix % 2 == 0) + total_partition_count += partition_info_table[ix]; + } + + if (total_partition_count == 0) { + free(buffer); + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "No partitions found"); + } + + partition_table = (uint32_t*)malloc(total_partition_count * 4 * 2); + kx = 0; + for (jx = 0; jx < 8; jx += 2) { + rc_file_seek(iterator, file_handle, ((uint64_t)partition_info_table[jx + 1]) << 2, SEEK_SET); + for (ix = 0; ix < partition_info_table[jx]; ix++) { + rc_file_read(iterator, file_handle, quad_buffer, 4); + partition_table[kx++] = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(iterator, file_handle, quad_buffer, 4); + partition_table[kx++] = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + } + } + + /* Read each partition */ + for (jx = 0; jx < total_partition_count * 2; jx += 2) { + /* Don't hash Update partition*/ + if (partition_table[jx + 1] == 1) + continue; + + /* Hash title metadata */ + rc_file_seek(iterator, file_handle, ((uint64_t)partition_table[jx] << 2) + 0x2A4, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + tmd_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(iterator, file_handle, quad_buffer, 4); + tmd_offset = + ((uint64_t)((quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3])) << 2; + + if (tmd_size > CLUSTER_SIZE) + tmd_size = CLUSTER_SIZE; + + rc_file_seek(iterator, file_handle, ((uint64_t)partition_table[jx] << 2) + tmd_offset, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, tmd_size); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte title metadata (partition type %u)", + tmd_size, partition_table[jx + 1]); + md5_append(md5, buffer, tmd_size); + + /* Hash partition */ + rc_file_seek(iterator, file_handle, ((uint64_t)partition_table[jx] << 2) + 0x2B8, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + part_offset = + ((uint64_t)((quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3])) << 2; + rc_file_read(iterator, file_handle, quad_buffer, 4); + part_size = + ((uint64_t)((quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3])) << 2; + + if (encrypted) { + cluster_count = (part_size / 0x8000 > MAX_CLUSTER_COUNT) ? MAX_CLUSTER_COUNT : (uint32_t)(part_size / 0x8000); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u encrypted clusters (%u bytes)", + cluster_count, cluster_count * CLUSTER_SIZE); + for (ix = 0; ix < cluster_count; ix++) { + rc_file_seek(iterator, file_handle, part_offset + (ix * 0x8000) + 0x400, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, CLUSTER_SIZE); + md5_append(md5, buffer, CLUSTER_SIZE); + } + } + else { /* Decrypted */ + if (rc_hash_nintendo_disc_partition(md5, iterator, file_handle, (uint32_t)part_offset, 2) == 0) { + free(partition_table); + free(buffer); + return rc_hash_iterator_error(iterator, "Failed to hash Wii partition"); + } + } + } + free(partition_table); + free(buffer); + return 1; +} + +static int rc_hash_wiiware(md5_state_t* md5, const rc_hash_iterator_t* iterator, void* file_handle) +{ + uint32_t cert_chain_size, ticket_size, tmd_size; + uint32_t tmd_start_addr, content_count, content_addr, content_size, buffer_size; + uint32_t ix; + + uint8_t quad_buffer[4]; + uint8_t* buffer; + + rc_file_seek(iterator, file_handle, 0x08, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + cert_chain_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + /* Each content is individually aligned to a 0x40-byte boundary. */ + cert_chain_size = (cert_chain_size + 0x3F) & ~0x3F; + rc_file_seek(iterator, file_handle, 0x10, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + ticket_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + ticket_size = (ticket_size + 0x3F) & ~0x3F; + rc_file_read(iterator, file_handle, quad_buffer, 4); + tmd_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + tmd_size = (tmd_size + 0x3F) & ~0x3F; + if (tmd_size > MAX_BUFFER_SIZE) + tmd_size = MAX_BUFFER_SIZE; + + tmd_start_addr = 0x40 + cert_chain_size + ticket_size; + + /* Hash TMD */ + buffer = (uint8_t*)malloc(tmd_size); + rc_file_seek(iterator, file_handle, tmd_start_addr, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, tmd_size); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte TMD", tmd_size); + md5_append(md5, buffer, tmd_size); + free(buffer); + + /* Get count of content sections */ + rc_file_seek(iterator, file_handle, (uint64_t)tmd_start_addr + 0x1de, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 2); + content_count = (quad_buffer[0] << 8) | quad_buffer[1]; + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u content sections", content_count); + content_addr = tmd_start_addr + tmd_size; + for (ix = 0; ix < content_count; ix++) { + /* Get content section size */ + rc_file_seek(iterator, file_handle, (uint64_t)tmd_start_addr + 0x1e4 + 8 + ix * 0x24, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 0x00 && quad_buffer[1] == 0x00 && quad_buffer[2] == 0x00 && quad_buffer[3] == 0x00) { + rc_file_read(iterator, file_handle, quad_buffer, 4); + content_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + /* Padding between content should be ignored. But because the content data is encrypted, + the size to hash for each content should be rounded up to the size of an AES block (16 bytes). */ + content_size = (content_size + 0x0F) & ~0x0F; + } + else { + /* size > 4GB, just assume MAX_BUFFER_SIZE */ + content_size = MAX_BUFFER_SIZE; + } + buffer_size = (content_size > MAX_BUFFER_SIZE) ? MAX_BUFFER_SIZE : content_size; + + /* Hash content */ + buffer = (uint8_t*)malloc(buffer_size); + rc_file_seek(iterator, file_handle, content_addr, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, buffer_size); + md5_append(md5, buffer, buffer_size); + content_addr += content_size; + content_addr = (content_addr + 0x3F) & ~0x3F; + free(buffer); + } + + return 1; +} + +int rc_hash_wii(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + void* file_handle; + + uint8_t quad_buffer[4]; + uint8_t success; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + md5_init(&md5); + /* Check Magic Words */ + rc_file_seek(iterator, file_handle, 0x18, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 0x5D && quad_buffer[1] == 0x1C && quad_buffer[2] == 0x9E && quad_buffer[3] == 0xA3) { + success = rc_hash_wii_disc(&md5, iterator, file_handle); + } + else { + rc_file_seek(iterator, file_handle, 0x04, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 'I' && quad_buffer[1] == 's' && quad_buffer[2] == 0x00 && quad_buffer[3] == 0x00) + success = rc_hash_wiiware(&md5, iterator, file_handle); + else + success = rc_hash_iterator_error(iterator, "Not a supported Wii file"); + } + + /* Finalize */ + rc_file_close(iterator, file_handle); + + if (success) + return rc_hash_finalize(iterator, &md5, hash); + + return 0; +} diff --git a/src/rcheevos/src/rhash/hash_encrypted.c b/src/rcheevos/src/rhash/hash_encrypted.c new file mode 100644 index 0000000000..e90e310757 --- /dev/null +++ b/src/rcheevos/src/rhash/hash_encrypted.c @@ -0,0 +1,566 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include "aes.h" + +/* ===================================================== */ + +static rc_hash_3ds_get_cia_normal_key_func _3ds_get_cia_normal_key_func = NULL; +static rc_hash_3ds_get_ncch_normal_keys_func _3ds_get_ncch_normal_keys_func = NULL; + +void rc_hash_reset_iterator_encrypted(rc_hash_iterator_t* iterator) +{ + iterator->callbacks.encryption.get_3ds_cia_normal_key = _3ds_get_cia_normal_key_func; + iterator->callbacks.encryption.get_3ds_ncch_normal_keys = _3ds_get_ncch_normal_keys_func; +} + +void rc_hash_init_3ds_get_cia_normal_key_func(rc_hash_3ds_get_cia_normal_key_func func) +{ + _3ds_get_cia_normal_key_func = func; +} + +void rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func) +{ + _3ds_get_ncch_normal_keys_func = func; +} + +/* ===================================================== */ + +static int rc_hash_nintendo_3ds_ncch(md5_state_t* md5, void* file_handle, uint8_t header[0x200], + struct AES_ctx* cia_aes, const rc_hash_iterator_t* iterator) +{ + struct AES_ctx ncch_aes; + uint8_t* hash_buffer; + uint64_t exefs_offset, exefs_real_size; + uint32_t exefs_buffer_size; + uint8_t primary_key[AES_KEYLEN], secondary_key[AES_KEYLEN]; + uint8_t fixed_key_flag, no_crypto_flag, seed_crypto_flag; + uint8_t crypto_method, secondary_key_x_slot; + uint16_t ncch_version; + uint32_t i; + uint8_t primary_key_y[AES_KEYLEN], program_id[sizeof(uint64_t)]; + uint8_t iv[AES_BLOCKLEN], cia_iv[AES_BLOCKLEN]; + uint8_t exefs_section_name[8]; + uint64_t exefs_section_offset, exefs_section_size; + + exefs_offset = ((uint32_t)header[0x1A3] << 24) | (header[0x1A2] << 16) | (header[0x1A1] << 8) | header[0x1A0]; + exefs_real_size = ((uint32_t)header[0x1A7] << 24) | (header[0x1A6] << 16) | (header[0x1A5] << 8) | header[0x1A4]; + + /* Offset and size are in "media units" (1 media unit = 0x200 bytes) */ + exefs_offset *= 0x200; + exefs_real_size *= 0x200; + + if (exefs_real_size > MAX_BUFFER_SIZE) + exefs_buffer_size = MAX_BUFFER_SIZE; + else + exefs_buffer_size = (uint32_t)exefs_real_size; + + /* This region is technically optional, but it should always be present for executable content (i.e. games) */ + if (exefs_offset == 0 || exefs_real_size == 0) + return rc_hash_iterator_error(iterator, "ExeFS was not available"); + + /* NCCH flag 7 is a bitfield of various crypto related flags */ + fixed_key_flag = header[0x188 + 7] & 0x01; + no_crypto_flag = header[0x188 + 7] & 0x04; + seed_crypto_flag = header[0x188 + 7] & 0x20; + + ncch_version = (header[0x113] << 8) | header[0x112]; + + if (no_crypto_flag == 0) { + rc_hash_iterator_verbose(iterator, "Encrypted NCCH detected"); + + if (fixed_key_flag != 0) { + /* Fixed crypto key means all 0s for both keys */ + memset(primary_key, 0, sizeof(primary_key)); + memset(secondary_key, 0, sizeof(secondary_key)); + rc_hash_iterator_verbose(iterator, "Using fixed key crypto"); + } + else { + if (iterator->callbacks.encryption.get_3ds_ncch_normal_keys == NULL) + return rc_hash_iterator_error(iterator, "An encrypted NCCH was detected, but the NCCH normal keys callback was not set"); + + /* Primary key y is just the first 16 bytes of the header */ + memcpy(primary_key_y, header, sizeof(primary_key_y)); + + /* NCCH flag 3 indicates which secondary key x slot is used */ + crypto_method = header[0x188 + 3]; + + switch (crypto_method) { + case 0x00: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v1"); + secondary_key_x_slot = 0x2C; + break; + case 0x01: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v2"); + secondary_key_x_slot = 0x25; + break; + case 0x0A: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v3"); + secondary_key_x_slot = 0x18; + break; + case 0x0B: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v4"); + secondary_key_x_slot = 0x1B; + break; + default: + return rc_hash_iterator_error_formatted(iterator, "Invalid crypto method %02X", (unsigned)crypto_method); + } + + /* We only need the program id if we're doing seed crypto */ + if (seed_crypto_flag != 0) { + rc_hash_iterator_verbose(iterator, "Using seed crypto"); + memcpy(program_id, &header[0x118], sizeof(program_id)); + } + + if (iterator->callbacks.encryption.get_3ds_ncch_normal_keys(primary_key_y, secondary_key_x_slot, seed_crypto_flag != 0 ? program_id : NULL, primary_key, secondary_key) == 0) + return rc_hash_iterator_error(iterator, "Could not obtain NCCH normal keys"); + } + + switch (ncch_version) { + case 0: + case 2: + rc_hash_iterator_verbose(iterator, "Detected NCCH version 0/2"); + for (i = 0; i < 8; i++) { + /* First 8 bytes is the partition id in reverse byte order */ + iv[7 - i] = header[0x108 + i]; + } + + /* Magic number for ExeFS */ + iv[8] = 2; + + /* Rest of the bytes are 0 */ + memset(&iv[9], 0, sizeof(iv) - 9); + break; + + case 1: + rc_hash_iterator_verbose(iterator, "Detected NCCH version 1"); + for (i = 0; i < 8; i++) { + /* First 8 bytes is the partition id in normal byte order */ + iv[i] = header[0x108 + i]; + } + + /* Next 4 bytes are 0 */ + memset(&iv[8], 0, 4); + + /* Last 4 bytes is the ExeFS byte offset in big endian */ + iv[12] = (exefs_offset >> 24) & 0xFF; + iv[13] = (exefs_offset >> 16) & 0xFF; + iv[14] = (exefs_offset >> 8) & 0xFF; + iv[15] = exefs_offset & 0xFF; + break; + + default: + return rc_hash_iterator_error_formatted(iterator, "Invalid NCCH version %04X", (unsigned)ncch_version); + } + } + + /* ASSERT: file position must be +0x200 from start of NCCH (i.e. end of header) */ + exefs_offset -= 0x200; + + if (cia_aes) { + /* CBC decryption works by setting the IV to the encrypted previous block. + * Normally this means we would need to decrypt the data between the header and the ExeFS so the CIA AES state is correct. + * However, we can abuse how CBC decryption works and just set the IV to last block we would otherwise decrypt. + * We don't care about the data betweeen the header and ExeFS, so this works fine. */ + + rc_file_seek(iterator, file_handle, (int64_t)exefs_offset - AES_BLOCKLEN, SEEK_CUR); + if (rc_file_read(iterator, file_handle, cia_iv, AES_BLOCKLEN) != AES_BLOCKLEN) + return rc_hash_iterator_error(iterator, "Could not read NCCH data"); + + AES_ctx_set_iv(cia_aes, cia_iv); + } + else { + /* No encryption present, just skip over the in-between data */ + rc_file_seek(iterator, file_handle, (int64_t)exefs_offset, SEEK_CUR); + } + + hash_buffer = (uint8_t*)malloc(exefs_buffer_size); + if (!hash_buffer) + return rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)exefs_buffer_size); + + /* Clear out crypto flags to ensure we get the same hash for decrypted and encrypted ROMs */ + memset(&header[0x114], 0, 4); + header[0x188 + 3] = 0; + header[0x188 + 7] &= ~(0x20 | 0x04 | 0x01); + + rc_hash_iterator_verbose(iterator, "Hashing 512 byte NCCH header"); + md5_append(md5, header, 0x200); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u bytes for ExeFS (at NCCH offset %08X%08X)", + (unsigned)exefs_buffer_size, (unsigned)(exefs_offset >> 32), (unsigned)exefs_offset); + + if (rc_file_read(iterator, file_handle, hash_buffer, exefs_buffer_size) != exefs_buffer_size) { + free(hash_buffer); + return rc_hash_iterator_error(iterator, "Could not read ExeFS data"); + } + + if (cia_aes) { + rc_hash_iterator_verbose(iterator, "Performing CIA decryption for ExeFS"); + AES_CBC_decrypt_buffer(cia_aes, hash_buffer, exefs_buffer_size); + } + + if (no_crypto_flag == 0) { + rc_hash_iterator_verbose(iterator, "Performing NCCH decryption for ExeFS"); + + AES_init_ctx_iv(&ncch_aes, primary_key, iv); + AES_CTR_xcrypt_buffer(&ncch_aes, hash_buffer, 0x200); + + for (i = 0; i < 8; i++) { + memcpy(exefs_section_name, &hash_buffer[i * 16], sizeof(exefs_section_name)); + exefs_section_offset = ((uint32_t)hash_buffer[i * 16 + 11] << 24) | (hash_buffer[i * 16 + 10] << 16) | (hash_buffer[i * 16 + 9] << 8) | hash_buffer[i * 16 + 8]; + exefs_section_size = ((uint32_t)hash_buffer[i * 16 + 15] << 24) | (hash_buffer[i * 16 + 14] << 16) | (hash_buffer[i * 16 + 13] << 8) | hash_buffer[i * 16 + 12]; + + /* 0 size indicates an unused section */ + if (exefs_section_size == 0) + continue; + + /* Offsets must be aligned by a media unit */ + if (exefs_section_offset & 0x1FF) + return rc_hash_iterator_error(iterator, "ExeFS section offset is misaligned"); + + /* Offset is relative to the end of the header */ + exefs_section_offset += 0x200; + + /* Check against malformed sections */ + if (exefs_section_offset + ((exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF) > (uint64_t)exefs_real_size) + return rc_hash_iterator_error(iterator, "ExeFS section would overflow"); + + if (memcmp(exefs_section_name, "icon", 4) == 0 || + memcmp(exefs_section_name, "banner", 6) == 0) { + /* Align size up by a media unit */ + exefs_section_size = (exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF; + AES_init_ctx(&ncch_aes, primary_key); + } + else { + /* We don't align size up here, as the padding bytes will use the primary key rather than the secondary key */ + AES_init_ctx(&ncch_aes, secondary_key); + } + + /* In theory, the section offset + size could be greater than the buffer size */ + /* In practice, this likely never occurs, but just in case it does, ignore the section or constrict the size */ + if (exefs_section_offset + exefs_section_size > exefs_buffer_size) { + if (exefs_section_offset >= exefs_buffer_size) + continue; + + exefs_section_size = exefs_buffer_size - exefs_section_offset; + } + + exefs_section_name[7] = '\0'; + rc_hash_iterator_verbose_formatted(iterator, "Decrypting ExeFS file %s at ExeFS offset %08X with size %08X", + (const char*)exefs_section_name, (unsigned)exefs_section_offset, (unsigned)exefs_section_size); + + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], exefs_section_size & ~(uint64_t)0xF); + + if (exefs_section_size & 0x1FF) { + /* Handle padding bytes, these always use the primary key */ + exefs_section_offset += exefs_section_size; + exefs_section_size = 0x200 - (exefs_section_size & 0x1FF); + + rc_hash_iterator_verbose_formatted(iterator, "Decrypting ExeFS padding at ExeFS offset %08X with size %08X", + (unsigned)exefs_section_offset, (unsigned)exefs_section_size); + + /* Align our decryption start to an AES block boundary */ + if (exefs_section_size & 0xF) { + /* We're a little evil here re-using the IV like this, but this seems to be the best way to deal with this... */ + memcpy(iv, ncch_aes.Iv, sizeof(iv)); + exefs_section_offset &= ~(uint64_t)0xF; + + /* First decrypt these last bytes using the secondary key */ + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); + + /* Now re-encrypt these bytes using the primary key */ + AES_init_ctx_iv(&ncch_aes, primary_key, iv); + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); + + /* All of the padding can now be decrypted using the primary key */ + AES_ctx_set_iv(&ncch_aes, iv); + exefs_section_size += 0x10 - (exefs_section_size & 0xF); + } + + AES_init_ctx(&ncch_aes, primary_key); + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], (size_t)exefs_section_size); + } + } + } + + md5_append(md5, hash_buffer, exefs_buffer_size); + + free(hash_buffer); + return 1; +} + +static uint32_t rc_hash_nintendo_3ds_cia_signature_size(uint8_t header[0x200], const rc_hash_iterator_t* iterator) +{ + uint32_t signature_type; + + signature_type = ((uint32_t)header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; + switch (signature_type) { + case 0x010000: + case 0x010003: + return 0x200 + 0x3C; + + case 0x010001: + case 0x010004: + return 0x100 + 0x3C; + + case 0x010002: + case 0x010005: + return 0x3C + 0x40; + + default: + return rc_hash_iterator_error_formatted(iterator, "Invalid signature type %08X", (unsigned)signature_type); + } +} + +static int rc_hash_nintendo_3ds_cia(md5_state_t* md5, void* file_handle, uint8_t header[0x200], + const rc_hash_iterator_t* iterator) +{ + const uint32_t CIA_HEADER_SIZE = 0x2020; /* Yes, this is larger than the header[0x200], but we only use the beginning of the header */ + const uint64_t CIA_ALIGNMENT_MASK = 64 - 1; /* sizes are aligned by 64 bytes */ + struct AES_ctx aes; + uint8_t iv[AES_BLOCKLEN], normal_key[AES_KEYLEN], title_key[AES_KEYLEN], title_id[sizeof(uint64_t)]; + uint32_t cert_size, tik_size, tmd_size; + int64_t cert_offset, tik_offset, tmd_offset, content_offset; + uint32_t signature_size, i; + uint16_t content_count; + uint8_t common_key_index; + + cert_size = ((uint32_t)header[0x0B] << 24) | (header[0x0A] << 16) | (header[0x09] << 8) | header[0x08]; + tik_size = ((uint32_t)header[0x0F] << 24) | (header[0x0E] << 16) | (header[0x0D] << 8) | header[0x0C]; + tmd_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; + + cert_offset = (CIA_HEADER_SIZE + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + tik_offset = (cert_offset + cert_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + tmd_offset = (tik_offset + tik_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + content_offset = (tmd_offset + tmd_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + + /* Check if this CIA is encrypted, if it isn't, we can hash it right away */ + + rc_file_seek(iterator, file_handle, tmd_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 4) != 4) + return rc_hash_iterator_error(iterator, "Could not read TMD signature type"); + + signature_size = rc_hash_nintendo_3ds_cia_signature_size(header, iterator); + if (signature_size == 0) + return 0; /* rc_hash_nintendo_3ds_cia_signature_size will call rc_hash_error, so we don't need to do so here */ + + rc_file_seek(iterator, file_handle, signature_size + 0x9E, SEEK_CUR); + if (rc_file_read(iterator, file_handle, header, 2) != 2) + return rc_hash_iterator_error(iterator, "Could not read TMD content count"); + + content_count = (header[0] << 8) | header[1]; + + rc_file_seek(iterator, file_handle, 0x9C4 - 0x9E - 2, SEEK_CUR); + for (i = 0; i < content_count; i++) { + if (rc_file_read(iterator, file_handle, header, 0x30) != 0x30) + return rc_hash_iterator_error(iterator, "Could not read TMD content chunk"); + + /* Content index 0 is the main content (i.e. the 3DS executable) */ + if (((header[4] << 8) | header[5]) == 0) + break; + + content_offset += ((uint32_t)header[0xC] << 24) | (header[0xD] << 16) | (header[0xE] << 8) | header[0xF]; + } + + if (i == content_count) + return rc_hash_iterator_error(iterator, "Could not find main content chunk in TMD"); + + if ((header[7] & 1) == 0) { + /* Not encrypted, we can hash the NCCH immediately */ + rc_file_seek(iterator, file_handle, content_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 0x200) != 0x200) + return rc_hash_iterator_error(iterator, "Could not read NCCH header"); + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + return rc_hash_iterator_error_formatted(iterator, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); + + return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, NULL, iterator); + } + + if (iterator->callbacks.encryption.get_3ds_cia_normal_key == NULL) + return rc_hash_iterator_error(iterator, "An encrypted CIA was detected, but the CIA normal key callback was not set"); + + /* Acquire the encrypted title key, title id, and common key index from the ticket */ + /* These will be needed to decrypt the title key, and that will be needed to decrypt the CIA */ + + rc_file_seek(iterator, file_handle, tik_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 4) != 4) + return rc_hash_iterator_error(iterator, "Could not read ticket signature type"); + + signature_size = rc_hash_nintendo_3ds_cia_signature_size(header, iterator); + if (signature_size == 0) + return 0; + + rc_file_seek(iterator, file_handle, signature_size, SEEK_CUR); + if (rc_file_read(iterator, file_handle, header, 0xB2) != 0xB2) + return rc_hash_iterator_error(iterator, "Could not read ticket data"); + + memcpy(title_key, &header[0x7F], sizeof(title_key)); + memcpy(title_id, &header[0x9C], sizeof(title_id)); + common_key_index = header[0xB1]; + + if (common_key_index > 5) + return rc_hash_iterator_error_formatted(iterator, "Invalid common key index %02X", (unsigned)common_key_index); + + if (iterator->callbacks.encryption.get_3ds_cia_normal_key(common_key_index, normal_key) == 0) + return rc_hash_iterator_error_formatted(iterator, "Could not obtain common key %02X", (unsigned)common_key_index); + + memset(iv, 0, sizeof(iv)); + memcpy(iv, title_id, sizeof(title_id)); + AES_init_ctx_iv(&aes, normal_key, iv); + + /* Finally, decrypt the title key */ + AES_CBC_decrypt_buffer(&aes, title_key, sizeof(title_key)); + + /* Now we can hash the NCCH */ + + rc_file_seek(iterator, file_handle, content_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 0x200) != 0x200) + return rc_hash_iterator_error(iterator, "Could not read NCCH header"); + + memset(iv, 0, sizeof(iv)); /* Content index is iv (which is always 0 for main content) */ + AES_init_ctx_iv(&aes, title_key, iv); + AES_CBC_decrypt_buffer(&aes, header, 0x200); + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + return rc_hash_iterator_error_formatted(iterator, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); + + return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, &aes, iterator); +} + +static int rc_hash_nintendo_3ds_3dsx(md5_state_t* md5, void* file_handle, uint8_t header[0x200], const rc_hash_iterator_t* iterator) +{ + uint8_t* hash_buffer; + uint32_t header_size, reloc_header_size, code_size; + int64_t code_offset; + + header_size = (header[5] << 8) | header[4]; + reloc_header_size = (header[7] << 8) | header[6]; + code_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; + + /* 3 relocation headers are in-between the 3DSX header and code segment */ + code_offset = header_size + reloc_header_size * 3; + + if (code_size > MAX_BUFFER_SIZE) + code_size = MAX_BUFFER_SIZE; + + hash_buffer = (uint8_t*)malloc(code_size); + if (!hash_buffer) + return rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)code_size); + + rc_file_seek(iterator, file_handle, code_offset, SEEK_SET); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u bytes for 3DSX (at %08X)", (unsigned)code_size, (unsigned)code_offset); + + if (rc_file_read(iterator, file_handle, hash_buffer, code_size) != code_size) { + free(hash_buffer); + return rc_hash_iterator_error(iterator, "Could not read 3DSX code segment"); + } + + md5_append(md5, hash_buffer, code_size); + + free(hash_buffer); + return 1; +} + +int rc_hash_nintendo_3ds(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + void* file_handle; + uint8_t header[0x200]; /* NCCH and NCSD headers are both 0x200 bytes */ + int64_t header_offset; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + + /* If we don't have a full header, this is probably not a 3DS ROM */ + if (rc_file_read(iterator, file_handle, header, sizeof(header)) != sizeof(header)) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not read 3DS ROM header"); + } + + md5_init(&md5); + + if (memcmp(&header[0x100], "NCSD", 4) == 0) { + /* A NCSD container contains 1-8 NCCH partitions */ + /* The first partition (index 0) is reserved for executable content */ + header_offset = ((uint32_t)header[0x123] << 24) | (header[0x122] << 16) | (header[0x121] << 8) | header[0x120]; + /* Offset is in "media units" (1 media unit = 0x200 bytes) */ + header_offset *= 0x200; + + /* We include the NCSD header in the hash, as that will ensure different versions of a game result in a different hash + * This is due to some revisions / languages only ever changing other NCCH paritions (e.g. the game manual) + */ + rc_hash_iterator_verbose(iterator, "Hashing 512 byte NCSD header"); + md5_append(&md5, header, sizeof(header)); + + rc_hash_iterator_verbose_formatted(iterator, + "Detected NCSD header, seeking to NCCH partition at %08X%08X", + (unsigned)(header_offset >> 32), (unsigned)header_offset); + + rc_file_seek(iterator, file_handle, header_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, sizeof(header)) != sizeof(header)) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not read 3DS NCCH header"); + } + + if (memcmp(&header[0x100], "NCCH", 4) != 0) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error_formatted(iterator, "3DS NCCH header was not at %08X%08X", (unsigned)(header_offset >> 32), (unsigned)header_offset); + } + } + + if (memcmp(&header[0x100], "NCCH", 4) == 0) { + if (rc_hash_nintendo_3ds_ncch(&md5, file_handle, header, NULL, iterator)) { + rc_file_close(iterator, file_handle); + return rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Failed to hash 3DS NCCH container"); + } + + /* Couldn't identify either an NCSD or NCCH */ + + /* Try to identify this as a CIA */ + if (header[0] == 0x20 && header[1] == 0x20 && header[2] == 0x00 && header[3] == 0x00) { + rc_hash_iterator_verbose(iterator, "Detected CIA, attempting to find executable NCCH"); + + if (rc_hash_nintendo_3ds_cia(&md5, file_handle, header, iterator)) { + rc_file_close(iterator, file_handle); + return rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Failed to hash 3DS CIA container"); + } + + /* This might be a homebrew game, try to detect that */ + if (memcmp(&header[0], "3DSX", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Detected 3DSX"); + + if (rc_hash_nintendo_3ds_3dsx(&md5, file_handle, header, iterator)) { + rc_file_close(iterator, file_handle); + return rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Failed to hash 3DS 3DSX container"); + } + + /* Raw ELF marker (AXF/ELF files) */ + if (memcmp(&header[0], "\x7f\x45\x4c\x46", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Detected AXF/ELF file, hashing entire file"); + + /* Don't bother doing anything fancy here, just hash entire file */ + rc_file_close(iterator, file_handle); + return rc_hash_whole_file(hash, iterator); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Not a 3DS ROM"); +} diff --git a/src/rcheevos/src/rhash/hash_rom.c b/src/rcheevos/src/rhash/hash_rom.c new file mode 100644 index 0000000000..996c3080c3 --- /dev/null +++ b/src/rcheevos/src/rhash/hash_rom.c @@ -0,0 +1,426 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include + +/* ===================================================== */ + +static int rc_hash_unheadered_iterator_buffer(char hash[33], const rc_hash_iterator_t* iterator, size_t header_size) +{ + return rc_hash_buffer(hash, iterator->buffer + header_size, iterator->buffer_size - header_size, iterator); +} + +static int rc_hash_iterator_buffer(char hash[33], const rc_hash_iterator_t* iterator) +{ + return rc_hash_buffer(hash, iterator->buffer, iterator->buffer_size, iterator); +} + +/* ===================================================== */ + +int rc_hash_7800(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&iterator->buffer[1], "ATARI7800", 9) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring 7800 header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 128); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_arcade(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* arcade hash is just the hash of the filename (no extension) - the cores are pretty stringent about having the right ROM data */ + const char* filename = rc_path_get_filename(iterator->path); + const char* ext = rc_path_get_extension(filename); + char buffer[128]; /* realistically, this should never need more than ~32 characters */ + size_t filename_length = ext - filename - 1; + + /* fbneo supports loading subsystems by using specific folder names. + * if one is found, include it in the hash. + * https://github.com/libretro/FBNeo/blob/master/src/burner/libretro/README.md#emulating-consoles-and-computers + */ + if (filename > iterator->path + 1) { + int include_folder = 0; + const char* folder = filename - 1; + size_t parent_folder_length = 0; + + do { + if (folder[-1] == '/' || folder[-1] == '\\') + break; + + --folder; + } while (folder > iterator->path); + + parent_folder_length = filename - folder - 1; + if (parent_folder_length < 16) { + char* ptr = buffer; + while (folder < filename - 1) + *ptr++ = tolower(*folder++); + *ptr = '\0'; + + folder = buffer; + } + + switch (parent_folder_length) { + case 3: + if (memcmp(folder, "nes", 3) == 0 || /* NES */ + memcmp(folder, "fds", 3) == 0 || /* FDS */ + memcmp(folder, "sms", 3) == 0 || /* Master System */ + memcmp(folder, "msx", 3) == 0 || /* MSX */ + memcmp(folder, "ngp", 3) == 0 || /* NeoGeo Pocket */ + memcmp(folder, "pce", 3) == 0 || /* PCEngine */ + memcmp(folder, "chf", 3) == 0 || /* ChannelF */ + memcmp(folder, "sgx", 3) == 0) /* SuperGrafX */ + include_folder = 1; + break; + case 4: + if (memcmp(folder, "tg16", 4) == 0 || /* TurboGrafx-16 */ + memcmp(folder, "msx1", 4) == 0) /* MSX */ + include_folder = 1; + break; + case 5: + if (memcmp(folder, "neocd", 5) == 0) /* NeoGeo CD */ + include_folder = 1; + break; + case 6: + if (memcmp(folder, "coleco", 6) == 0 || /* Colecovision */ + memcmp(folder, "sg1000", 6) == 0) /* SG-1000 */ + include_folder = 1; + break; + case 7: + if (memcmp(folder, "genesis", 7) == 0) /* Megadrive (Genesis) */ + include_folder = 1; + break; + case 8: + if (memcmp(folder, "gamegear", 8) == 0 || /* Game Gear */ + memcmp(folder, "megadriv", 8) == 0 || /* Megadrive */ + memcmp(folder, "pcengine", 8) == 0 || /* PCEngine */ + memcmp(folder, "channelf", 8) == 0 || /* ChannelF */ + memcmp(folder, "spectrum", 8) == 0) /* ZX Spectrum */ + include_folder = 1; + break; + case 9: + if (memcmp(folder, "megadrive", 9) == 0) /* Megadrive */ + include_folder = 1; + break; + case 10: + if (memcmp(folder, "supergrafx", 10) == 0 || /* SuperGrafX */ + memcmp(folder, "zxspectrum", 10) == 0) /* ZX Spectrum */ + include_folder = 1; + break; + case 12: + if (memcmp(folder, "mastersystem", 12) == 0 || /* Master System */ + memcmp(folder, "colecovision", 12) == 0) /* Colecovision */ + include_folder = 1; + break; + default: + break; + } + + if (include_folder) { + if (parent_folder_length + filename_length + 1 < sizeof(buffer)) { + buffer[parent_folder_length] = '_'; + memcpy(&buffer[parent_folder_length + 1], filename, filename_length); + return rc_hash_buffer(hash, (uint8_t*)&buffer[0], parent_folder_length + filename_length + 1, iterator); + } + } + } + + return rc_hash_buffer(hash, (uint8_t*)filename, filename_length, iterator); +} + +static int rc_hash_text(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + const uint8_t* scan = iterator->buffer; + const uint8_t* stop = iterator->buffer + iterator->buffer_size; + + md5_init(&md5); + + do { + const uint8_t* line = scan; + + /* find end of line */ + while (scan < stop && *scan != '\r' && *scan != '\n') + ++scan; + + md5_append(&md5, line, (int)(scan - line)); + + /* include a normalized line ending */ + /* NOTE: this causes a line ending to be hashed at the end of the file, even if one was not present */ + md5_append(&md5, (const uint8_t*)"\n", 1); + + /* skip newline */ + if (scan < stop && *scan == '\r') + ++scan; + if (scan < stop && *scan == '\n') + ++scan; + + } while (scan < stop); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_arduboy(char hash[33], const rc_hash_iterator_t* iterator) +{ + if (iterator->path && rc_path_compare_extension(iterator->path, "arduboy")) { +#ifndef RC_HASH_NO_ZIP + return rc_hash_arduboyfx(hash, iterator); +#else + rc_hash_iterator_verbose(iterator, ".arduboy file processing not enabled"); + return 0; +#endif + } + + if (!iterator->buffer) + return rc_hash_buffered_file(hash, RC_CONSOLE_ARDUBOY, iterator); + + /* https://en.wikipedia.org/wiki/Intel_HEX */ + return rc_hash_text(hash, iterator); +} + +int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&iterator->buffer[0], "LYNX", 5) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring LYNX header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 64); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_nes(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&iterator->buffer[0], "NES\x1a", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring NES header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 16); + } + + if (memcmp(&iterator->buffer[0], "FDS\x1a", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring FDS header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 16); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_n64(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t* buffer; + uint8_t* stop; + const size_t buffer_size = 65536; + md5_state_t md5; + size_t remaining; + void* file_handle; + int is_v64 = 0; + int is_n64 = 0; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + buffer = (uint8_t*)malloc(buffer_size); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + stop = buffer + buffer_size; + + /* read first byte so we can detect endianness */ + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, 1); + + if (buffer[0] == 0x80) { /* z64 format (big endian [native]) */ + } + else if (buffer[0] == 0x37) { /* v64 format (byteswapped) */ + rc_hash_iterator_verbose(iterator, "converting v64 to z64"); + is_v64 = 1; + } + else if (buffer[0] == 0x40) { /* n64 format (little endian) */ + rc_hash_iterator_verbose(iterator, "converting n64 to z64"); + is_n64 = 1; + } + else if (buffer[0] == 0xE8 || buffer[0] == 0x22) { /* ndd format (don't byteswap) */ + } + else { + free(buffer); + rc_file_close(iterator, file_handle); + + rc_hash_iterator_verbose(iterator, "Not a Nintendo 64 ROM"); + return 0; + } + + /* calculate total file size */ + rc_file_seek(iterator, file_handle, 0, SEEK_END); + remaining = (size_t)rc_file_tell(iterator, file_handle); + if (remaining > MAX_BUFFER_SIZE) + remaining = MAX_BUFFER_SIZE; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u bytes", (unsigned)remaining); + + /* begin hashing */ + md5_init(&md5); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) { + rc_file_read(iterator, file_handle, buffer, (int)buffer_size); + + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) { + rc_file_read(iterator, file_handle, buffer, (int)remaining); + + stop = buffer + remaining; + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)remaining); + } + + /* cleanup */ + rc_file_close(iterator, file_handle); + free(buffer); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_nintendo_ds(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t header[512]; + uint8_t* hash_buffer; + uint32_t hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; + size_t num_read; + int64_t offset = 0; + md5_state_t md5; + void* file_handle; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, sizeof(header)) != 512) + return rc_hash_iterator_error(iterator, "Failed to read header"); + + if (header[0] == 0x2E && header[1] == 0x00 && header[2] == 0x00 && header[3] == 0xEA && + header[0xB0] == 0x44 && header[0xB1] == 0x46 && header[0xB2] == 0x96 && header[0xB3] == 0) { + /* SuperCard header detected, ignore it */ + rc_hash_iterator_verbose(iterator, "Ignoring SuperCard header"); + + offset = 512; + rc_file_seek(iterator, file_handle, offset, SEEK_SET); + rc_file_read(iterator, file_handle, header, sizeof(header)); + } + + arm9_addr = header[0x20] | (header[0x21] << 8) | (header[0x22] << 16) | (header[0x23] << 24); + arm9_size = header[0x2C] | (header[0x2D] << 8) | (header[0x2E] << 16) | (header[0x2F] << 24); + arm7_addr = header[0x30] | (header[0x31] << 8) | (header[0x32] << 16) | (header[0x33] << 24); + arm7_size = header[0x3C] | (header[0x3D] << 8) | (header[0x3E] << 16) | (header[0x3F] << 24); + icon_addr = header[0x68] | (header[0x69] << 8) | (header[0x6A] << 16) | (header[0x6B] << 24); + + if (arm9_size + arm7_size > 16 * 1024 * 1024) { + /* sanity check - code blocks are typically less than 1MB each - assume not a DS ROM */ + return rc_hash_iterator_error_formatted(iterator, "arm9 code size (%u) + arm7 code size (%u) exceeds 16MB", arm9_size, arm7_size); + } + + hash_size = 0xA00; + if (arm9_size > hash_size) + hash_size = arm9_size; + if (arm7_size > hash_size) + hash_size = arm7_size; + + hash_buffer = (uint8_t*)malloc(hash_size); + if (!hash_buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", hash_size); + } + + md5_init(&md5); + + rc_hash_iterator_verbose(iterator, "Hashing 352 byte header"); + md5_append(&md5, header, 0x160); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte arm9 code (at %08X)", arm9_size, arm9_addr); + + rc_file_seek(iterator, file_handle, arm9_addr + offset, SEEK_SET); + rc_file_read(iterator, file_handle, hash_buffer, arm9_size); + md5_append(&md5, hash_buffer, arm9_size); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte arm7 code (at %08X)", arm7_size, arm7_addr); + + rc_file_seek(iterator, file_handle, arm7_addr + offset, SEEK_SET); + rc_file_read(iterator, file_handle, hash_buffer, arm7_size); + md5_append(&md5, hash_buffer, arm7_size); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing 2560 byte icon and labels data (at %08X)", icon_addr); + + rc_file_seek(iterator, file_handle, icon_addr + offset, SEEK_SET); + num_read = rc_file_read(iterator, file_handle, hash_buffer, 0xA00); + if (num_read < 0xA00) { + /* some homebrew games don't provide a full icon block, and no data after the icon block. + * if we didn't get a full icon block, fill the remaining portion with 0s + */ + rc_hash_iterator_verbose_formatted(iterator, + "Warning: only got %u bytes for icon and labels data, 0-padding to 2560 bytes", (unsigned)num_read); + + memset(&hash_buffer[num_read], 0, 0xA00 - num_read); + } + md5_append(&md5, hash_buffer, 0xA00); + + free(hash_buffer); + rc_file_close(iterator, file_handle); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_pce(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* The PCE header doesn't bear any distinguishable marks, so we have to detect + * it by looking at the file size. The core looks for anything that's 512 bytes + * more than a multiple of 8KB, so we'll do that too. + * https://github.com/libretro/beetle-pce-libretro/blob/af28fb0385d00e0292c4703b3aa7e72762b564d2/mednafen/pce/huc.cpp#L196-L202 + */ + if (iterator->buffer_size & 512) { + rc_hash_iterator_verbose(iterator, "Ignoring PCE header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 512); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_scv(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + /* https://gitlab.com/MaaaX-EmuSCV/libretro-emuscv/-/blob/master/readme.txt#L211 */ + if (memcmp(iterator->buffer, "EmuSCV", 6) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring SCV header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 32); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_snes(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + uint32_t calc_size = ((uint32_t)iterator->buffer_size / 0x2000) * 0x2000; + if (iterator->buffer_size - calc_size == 512) { + rc_hash_iterator_verbose(iterator, "Ignoring SNES header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 512); + } + + return rc_hash_iterator_buffer(hash, iterator); +} diff --git a/src/rcheevos/src/rhash/hash_zip.c b/src/rcheevos/src/rhash/hash_zip.c new file mode 100644 index 0000000000..060dc938dd --- /dev/null +++ b/src/rcheevos/src/rhash/hash_zip.c @@ -0,0 +1,460 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +struct rc_hash_zip_idx +{ + size_t length; + uint8_t* data; +}; + +static int rc_hash_zip_idx_sort(const void* a, const void* b) +{ + struct rc_hash_zip_idx* A = (struct rc_hash_zip_idx*)a, * B = (struct rc_hash_zip_idx*)b; + size_t len = (A->length < B->length ? A->length : B->length); + return memcmp(A->data, B->data, len); +} + +typedef int (RC_CCONV* rc_hash_zip_filter_t)(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata); + +static int rc_hash_zip_file(md5_state_t* md5, void* file_handle, + const rc_hash_iterator_t* iterator, + rc_hash_zip_filter_t filter_func, void* filter_userdata) +{ + uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size, nparents; + uint32_t cdir_entry_len; + size_t sizeof_idx, indices_offset, alloc_size; + int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs; + struct rc_hash_zip_idx* hashindices, *hashindex; + + rc_file_seek(iterator, file_handle, 0, SEEK_END); + archive_size = rc_file_tell(iterator, file_handle); + + /* Basic sanity checks - reject files which are too small */ + eocdirhdr_size = 22; /* the 'end of central directory header' is 22 bytes */ + if (archive_size < eocdirhdr_size) + return rc_hash_iterator_error(iterator, "ZIP is too small"); + + /* Macros used for reading ZIP and writing to a buffer for hashing (undefined again at the end of the function) */ + #define RC_ZIP_READ_LE16(p) ((uint16_t)(((const uint8_t*)(p))[0]) | ((uint16_t)(((const uint8_t*)(p))[1]) << 8U)) + #define RC_ZIP_READ_LE32(p) ((uint32_t)(((const uint8_t*)(p))[0]) | ((uint32_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint32_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint32_t)(((const uint8_t*)(p))[3]) << 24U)) + #define RC_ZIP_READ_LE64(p) ((uint64_t)(((const uint8_t*)(p))[0]) | ((uint64_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint64_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint64_t)(((const uint8_t*)(p))[3]) << 24U) | ((uint64_t)(((const uint8_t*)(p))[4]) << 32U) | ((uint64_t)(((const uint8_t*)(p))[5]) << 40U) | ((uint64_t)(((const uint8_t*)(p))[6]) << 48U) | ((uint64_t)(((const uint8_t*)(p))[7]) << 56U)) + #define RC_ZIP_WRITE_LE32(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint32_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint32_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint32_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)((uint32_t)(v) >> 24); } + #define RC_ZIP_WRITE_LE64(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint64_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint64_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint64_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)(((uint64_t)(v) >> 24) & 0xFF); ((uint8_t*)(p))[4] = (uint8_t)(((uint64_t)(v) >> 32) & 0xFF); ((uint8_t*)(p))[5] = (uint8_t)(((uint64_t)(v) >> 40) & 0xFF); ((uint8_t*)(p))[6] = (uint8_t)(((uint64_t)(v) >> 48) & 0xFF); ((uint8_t*)(p))[7] = (uint8_t)((uint64_t)(v) >> 56); } + + /* Find the end of central directory record by scanning the file from the end towards the beginning */ + for (ecdh_ofs = archive_size - sizeof(buf); ; ecdh_ofs -= (sizeof(buf) - 3)) { + int i, n = sizeof(buf); + if (ecdh_ofs < 0) + ecdh_ofs = 0; + if (n > archive_size) + n = (int)archive_size; + + rc_file_seek(iterator, file_handle, ecdh_ofs, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, n) != (size_t)n) + return rc_hash_iterator_error(iterator, "ZIP read error"); + + for (i = n - 4; i >= 0; --i) { + if (buf[i] == 'P' && RC_ZIP_READ_LE32(buf + i) == 0x06054b50) /* end of central directory header signature */ + break; + } + + if (i >= 0) { + ecdh_ofs += i; + break; + } + + if (!ecdh_ofs || (archive_size - ecdh_ofs) >= (0xFFFF + eocdirhdr_size)) + return rc_hash_iterator_error(iterator, "Failed to find ZIP central directory"); + } + + /* Read and verify the end of central directory record. */ + rc_file_seek(iterator, file_handle, ecdh_ofs, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, eocdirhdr_size) != eocdirhdr_size) + return rc_hash_iterator_error(iterator, "Failed to read ZIP central directory"); + + /* Read central dir information from end of central directory header */ + total_files = RC_ZIP_READ_LE16(buf + 0x0A); + cdir_size = RC_ZIP_READ_LE32(buf + 0x0C); + cdir_ofs = RC_ZIP_READ_LE32(buf + 0x10); + + /* Check if this is a Zip64 file. In the block of code below: + * - 20 is the size of the ZIP64 end of central directory locator + * - 56 is the size of the ZIP64 end of central directory header + */ + if ((cdir_ofs == 0xFFFFFFFF || cdir_size == 0xFFFFFFFF || total_files == 0xFFFF) && ecdh_ofs >= (20 + 56)) { + /* Read the ZIP64 end of central directory locator if it actually exists */ + rc_file_seek(iterator, file_handle, ecdh_ofs - 20, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) { /* locator signature */ + /* Found the locator, now read the actual ZIP64 end of central directory header */ + int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08); + if (ecdh64_ofs <= (archive_size - 56)) { + rc_file_seek(iterator, file_handle, ecdh64_ofs, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, 56) == 56 && RC_ZIP_READ_LE32(buf) == 0x06064b50) { /* header signature */ + total_files = RC_ZIP_READ_LE64(buf + 0x20); + cdir_size = RC_ZIP_READ_LE64(buf + 0x28); + cdir_ofs = RC_ZIP_READ_LE64(buf + 0x30); + } + } + } + } + + /* Basic verificaton of central directory (limit to a 256MB content directory) */ + cdirhdr_size = 46; /* the 'central directory header' is 46 bytes */ + if ((cdir_size >= 0x10000000) || (cdir_size < total_files * cdirhdr_size) || ((cdir_ofs + cdir_size) > archive_size)) + return rc_hash_iterator_error(iterator, "Central directory of ZIP file is invalid"); + + /* Allocate once for both directory and our temporary sort index (memory aligned to sizeof(rc_hash_zip_idx)) */ + sizeof_idx = sizeof(struct rc_hash_zip_idx); + indices_offset = (size_t)((cdir_size + sizeof_idx - 1) / sizeof_idx * sizeof_idx); + alloc_size = (size_t)(indices_offset + total_files * sizeof_idx); + alloc_buf = (uint8_t*)malloc(alloc_size); + + /* Read entire central directory to a buffer */ + if (!alloc_buf) + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + + rc_file_seek(iterator, file_handle, cdir_ofs, SEEK_SET); + if ((int64_t)rc_file_read(iterator, file_handle, alloc_buf, (int)cdir_size) != cdir_size) { + free(alloc_buf); + return rc_hash_iterator_error(iterator, "Failed to read central directory of ZIP file"); + } + + cdir_start = alloc_buf; + cdir_max = cdir_start + cdir_size - cdirhdr_size; + cdir = cdir_start; + + /* Write our temporary hash data to the same buffer we read the central directory from. + * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record. + */ + hashdata = alloc_buf; + hashindices = (struct rc_hash_zip_idx*)(alloc_buf + indices_offset); + hashindex = hashindices; + + /* Now process the central directory file records */ + for (i_file = nparents = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) { + const uint8_t* name, * name_end; + uint32_t signature = RC_ZIP_READ_LE32(cdir + 0x00); + uint32_t method = RC_ZIP_READ_LE16(cdir + 0x0A); + uint32_t crc32 = RC_ZIP_READ_LE32(cdir + 0x10); + uint64_t comp_size = RC_ZIP_READ_LE32(cdir + 0x14); + uint64_t decomp_size = RC_ZIP_READ_LE32(cdir + 0x18); + uint32_t filename_len = RC_ZIP_READ_LE16(cdir + 0x1C); + int32_t extra_len = RC_ZIP_READ_LE16(cdir + 0x1E); + int32_t comment_len = RC_ZIP_READ_LE16(cdir + 0x20); + int32_t external_attr = RC_ZIP_READ_LE16(cdir + 0x26); + uint64_t local_hdr_ofs = RC_ZIP_READ_LE32(cdir + 0x2A); + cdir_entry_len = cdirhdr_size + filename_len + extra_len + comment_len; + + if (signature != 0x02014b50) /* expected central directory entry signature */ + break; + + /* Ignore records describing a directory (we only hash file records) */ + name = (cdir + cdirhdr_size); + if (name[filename_len - 1] == '/' || name[filename_len - 1] == '\\' || (external_attr & 0x10)) + continue; + + /* Handle Zip64 fields */ + if (decomp_size == 0xFFFFFFFF || comp_size == 0xFFFFFFFF || local_hdr_ofs == 0xFFFFFFFF) { + int invalid = 0; + const uint8_t* x = cdir + cdirhdr_size + filename_len, * xEnd, * field, * fieldEnd; + for (xEnd = x + extra_len; (x + (sizeof(uint16_t) * 2)) < xEnd; x = fieldEnd) { + field = x + (sizeof(uint16_t) * 2); + fieldEnd = field + RC_ZIP_READ_LE16(x + 2); + if (RC_ZIP_READ_LE16(x) != 0x0001 || fieldEnd > xEnd) + continue; /* Not the Zip64 extended information extra field */ + + if (decomp_size == 0xFFFFFFFF) { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { + invalid = 1; + break; + } + + decomp_size = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + + if (comp_size == 0xFFFFFFFF) { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { + invalid = 1; + break; + } + + comp_size = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + + if (local_hdr_ofs == 0xFFFFFFFF) { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { + invalid = 1; + break; + } + + local_hdr_ofs = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + + break; + } + + if (invalid) { + free(alloc_buf); + return rc_hash_iterator_error(iterator, "Encountered invalid Zip64 file"); + } + } + + /* Basic sanity check on file record */ + /* 30 is the length of the local directory header preceeding the compressed data */ + if ((!method && decomp_size != comp_size) || (decomp_size && !comp_size) || + ((local_hdr_ofs + 30 + comp_size) > (uint64_t)archive_size)) { + free(alloc_buf); + return rc_hash_iterator_error(iterator, "Encountered invalid entry in ZIP central directory"); + } + + if (filter_func) { + int filtered = filter_func((const char*)name, filename_len, decomp_size, filter_userdata); + if (filtered < 0) { + free(alloc_buf); + return 0; + } + + if (filtered) /* this file shouldn't be hashed */ + continue; + } + + /* Write the pointer and length of the data we record about this file */ + hashindex->data = hashdata; + hashindex->length = filename_len + 1 + 4 + 8; + hashindex++; + + rc_hash_iterator_verbose_formatted(iterator, "File in ZIP: %.*s (%u bytes, CRC32 = %08X)", filename_len, (const char*)name, (unsigned)decomp_size, crc32); + + /* Convert and store the file name in the hash data buffer */ + for (name_end = name + filename_len; name != name_end; name++) { + *(hashdata++) = + (*name == '\\' ? '/' : /* convert back-slashes to regular slashes */ + (*name >= 'A' && *name <= 'Z') ? (*name | 0x20) : /* convert upper case letters to lower case */ + *name); /* else use the byte as-is */ + } + + /* Add zero terminator, CRC32 and decompressed size to the hash data buffer */ + *(hashdata++) = '\0'; + RC_ZIP_WRITE_LE32(hashdata, crc32); + hashdata += 4; + RC_ZIP_WRITE_LE64(hashdata, decomp_size); + hashdata += 8; + } + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u files in ZIP archive", (unsigned)(hashindex - hashindices)); + + /* Sort the file list indices */ + qsort(hashindices, (hashindex - hashindices), sizeof(struct rc_hash_zip_idx), rc_hash_zip_idx_sort); + + /* Hash the data in the order of the now sorted indices */ + for (; hashindices != hashindex; hashindices++) + md5_append(md5, hashindices->data, (int)hashindices->length); + + free(alloc_buf); + + return 1; + + #undef RC_ZIP_READ_LE16 + #undef RC_ZIP_READ_LE32 + #undef RC_ZIP_READ_LE64 + #undef RC_ZIP_WRITE_LE32 + #undef RC_ZIP_WRITE_LE64 +} + +/* ===================================================== */ + +static int rc_hash_arduboyfx_filter(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata) +{ + (void)decomp_size; + (void)userdata; + + /* An .arduboy file is a zip file containing an info.json pointing at one or more bin + * and hex files. It can also contain a bunch of screenshots, but we don't care about + * those. As they're also referenced in the info.json, we have to ignore that too. + * Instead of ignoring the info.json and all image files, only process any bin/hex files */ + if (filename_len > 4) { + const char* ext = &filename[filename_len - 4]; + if (strncasecmp(ext, ".hex", 4) == 0 || strncasecmp(ext, ".bin", 4) == 0) + return 0; /* keep hex and bin */ + } + + return 1; /* filter everything else */ +} + +int rc_hash_arduboyfx(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + int res; + + void* file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + md5_init(&md5); + res = rc_hash_zip_file(&md5, file_handle, iterator, rc_hash_arduboyfx_filter, NULL); + rc_file_close(iterator, file_handle); + + if (!res) + return 0; + + return rc_hash_finalize(iterator, &md5, hash); +} + +/* ===================================================== */ + +struct rc_hash_ms_dos_dosz_state { + const char* path; + const struct rc_hash_ms_dos_dosz_state* child; + + md5_state_t* md5; + const rc_hash_iterator_t* iterator; + void* file_handle; + uint32_t nparents; +}; + +static int rc_hash_dosz(struct rc_hash_ms_dos_dosz_state* dosz); + +static int rc_hash_ms_dos_parent(const struct rc_hash_ms_dos_dosz_state* child, + const char* parentname, uint32_t parentname_len) +{ + const char* lastfslash = strrchr(child->path, '/'); + const char* lastbslash = strrchr(child->path, '\\'); + const char* lastslash = (lastbslash > lastfslash ? lastbslash : lastfslash); + size_t dir_len = (lastslash ? (lastslash + 1 - child->path) : 0); + char* parent_path = (char*)malloc(dir_len + parentname_len + 1); + struct rc_hash_ms_dos_dosz_state parent; + const struct rc_hash_ms_dos_dosz_state* check; + void* parent_handle; + int parent_res; + + /* Build the path of the parent by combining the directory of the current file with the name */ + if (!parent_path) + return rc_hash_iterator_error(child->iterator, "Could not allocate temporary buffer"); + + memcpy(parent_path, child->path, dir_len); + memcpy(parent_path + dir_len, parentname, parentname_len); + parent_path[dir_len + parentname_len] = '\0'; + + /* Make sure there is no recursion where a parent DOSZ is an already seen child DOSZ */ + for (check = child->child; check; check = check->child) { + if (!strcmp(check->path, parent_path)) { + free(parent_path); + return rc_hash_iterator_error(child->iterator, "Invalid DOSZ file with recursive parents"); + } + } + + /* Try to open the parent DOSZ file */ + parent_handle = rc_file_open(child->iterator, parent_path); + if (!parent_handle) { + rc_hash_iterator_error_formatted(child->iterator, "DOSZ parent file '%s' does not exist", parent_path); + free(parent_path); + return 0; + } + + /* Fully hash the parent DOSZ ahead of the child */ + memcpy(&parent, child, sizeof(parent)); + parent.path = parent_path; + parent.child = child; + parent.file_handle = parent_handle; + parent_res = rc_hash_dosz(&parent); + rc_file_close(child->iterator, parent_handle); + free(parent_path); + return parent_res; +} + +static int rc_hash_ms_dos_dosc(const struct rc_hash_ms_dos_dosz_state* dosz) +{ + size_t path_len = strlen(dosz->path); + if (dosz->path[path_len - 1] == 'z' || dosz->path[path_len - 1] == 'Z') { + void* file_handle; + char* dosc_path = strdup(dosz->path); + if (!dosc_path) + return rc_hash_iterator_error(dosz->iterator, "Could not allocate temporary buffer"); + + /* Swap the z to c and use the same capitalization, hash the file if it exists */ + dosc_path[path_len - 1] = (dosz->path[path_len - 1] == 'z' ? 'c' : 'C'); + file_handle = rc_file_open(dosz->iterator, dosc_path); + free(dosc_path); + + if (file_handle) { + /* Hash the entire contents of the DOSC file */ + int res = rc_hash_zip_file(dosz->md5, file_handle, dosz->iterator, NULL, NULL); + rc_file_close(dosz->iterator, file_handle); + if (!res) + return 0; + } + } + + return 1; +} + +static int rc_hash_dosz_filter(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata) +{ + struct rc_hash_ms_dos_dosz_state* dosz = (struct rc_hash_ms_dos_dosz_state*)userdata; + + /* A DOSZ file can contain a special empty .dosz.parent file in its root which means a parent dosz file is used */ + if (decomp_size == 0 && filename_len > 7 && + strncasecmp(&filename[filename_len - 7], ".parent", 7) == 0 && + !memchr(filename, '/', filename_len) && + !memchr(filename, '\\', filename_len)) + { + /* A DOSZ file can only have one parent file */ + if (dosz->nparents++) + return -1; + + /* process the parent. if it fails, stop */ + if (!rc_hash_ms_dos_parent(dosz, filename, (filename_len - 7))) + return -1; + + /* We don't hash this meta file so a user is free to rename it and the parent file */ + return 1; + } + + return 0; +} + +static int rc_hash_dosz(struct rc_hash_ms_dos_dosz_state* dosz) +{ + if (!rc_hash_zip_file(dosz->md5, dosz->file_handle, dosz->iterator, rc_hash_dosz_filter, dosz)) + return 0; + + /* A DOSZ file can only have one parent file */ + if (dosz->nparents > 1) + return rc_hash_iterator_error(dosz->iterator, "Invalid DOSZ file with multiple parents"); + + /* Check if an associated .dosc file exists */ + if (!rc_hash_ms_dos_dosc(dosz)) + return 0; + + return 1; +} + +int rc_hash_ms_dos(char hash[33], const rc_hash_iterator_t* iterator) +{ + struct rc_hash_ms_dos_dosz_state dosz; + md5_state_t md5; + int res; + + void* file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + memset(&dosz, 0, sizeof(dosz)); + dosz.path = iterator->path; + dosz.file_handle = file_handle; + dosz.iterator = iterator; + dosz.md5 = &md5; + + md5_init(&md5); + res = rc_hash_dosz(&dosz); + rc_file_close(iterator, file_handle); + + if (!res) + return 0; + + return rc_hash_finalize(iterator, &md5, hash); +} diff --git a/src/rcheevos/src/rhash/md5.c b/src/rcheevos/src/rhash/md5.c new file mode 100644 index 0000000000..f3a5205669 --- /dev/null +++ b/src/rcheevos/src/rhash/md5.c @@ -0,0 +1,382 @@ +/* + Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.c is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order + either statically or dynamically; added missing #include + in library. + 2002-03-11 lpd Corrected argument list for main(), and added int return + type, in test program and T value program. + 2002-02-21 lpd Added missing #include in test program. + 2000-07-03 lpd Patched to eliminate warnings about "constant is + unsigned in ANSI C, signed in traditional"; made test program + self-checking. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). + 1999-05-03 lpd Original version. + */ + +#include "md5.h" +#include +#include + +#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ +#ifdef ARCH_IS_BIG_ENDIAN +# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) +#else +# define BYTE_ORDER 0 +#endif + +#define T_MASK ((md5_word_t)~0) +#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) +#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) +#define T3 0x242070db +#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) +#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) +#define T6 0x4787c62a +#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) +#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) +#define T9 0x698098d8 +#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) +#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) +#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) +#define T13 0x6b901122 +#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) +#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) +#define T16 0x49b40821 +#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) +#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) +#define T19 0x265e5a51 +#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) +#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) +#define T22 0x02441453 +#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) +#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) +#define T25 0x21e1cde6 +#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) +#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) +#define T28 0x455a14ed +#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) +#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) +#define T31 0x676f02d9 +#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) +#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) +#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) +#define T35 0x6d9d6122 +#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) +#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) +#define T38 0x4bdecfa9 +#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) +#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) +#define T41 0x289b7ec6 +#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) +#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) +#define T44 0x04881d05 +#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) +#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) +#define T47 0x1fa27cf8 +#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) +#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) +#define T50 0x432aff97 +#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) +#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) +#define T53 0x655b59c3 +#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) +#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) +#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) +#define T57 0x6fa87e4f +#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) +#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) +#define T60 0x4e0811a1 +#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) +#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) +#define T63 0x2ad7d2bb +#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) + + +static void +md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) +{ + md5_word_t + a = pms->abcd[0], b = pms->abcd[1], + c = pms->abcd[2], d = pms->abcd[3]; + md5_word_t t; +#if BYTE_ORDER > 0 + /* Define storage only for big-endian CPUs. */ + md5_word_t X[16]; +#else + /* Define storage for little-endian or both types of CPUs. */ + md5_word_t xbuf[16]; + const md5_word_t *X; +#endif + + { +#if BYTE_ORDER == 0 + /* + * Determine dynamically whether this is a big-endian or + * little-endian machine, since we can use a more efficient + * algorithm on the latter. + */ + static const int w = 1; + + if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ +#endif +#if BYTE_ORDER <= 0 /* little-endian */ + { + /* + * On little-endian machines, we can process properly aligned + * data without copying it. + */ + if (!((ptrdiff_t)data & 3)) { + /* data are properly aligned */ + X = (const md5_word_t *)data; + } else { + /* not aligned */ + memcpy(xbuf, data, 64); + X = xbuf; + } + } +#endif +#if BYTE_ORDER == 0 + else /* dynamic big-endian */ +#endif +#if BYTE_ORDER >= 0 /* big-endian */ + { + /* + * On big-endian machines, we must arrange the bytes in the + * right order. + */ + const md5_byte_t *xp = data; + int i; + +# if BYTE_ORDER == 0 + X = xbuf; /* (dynamic only) */ +# else +# define xbuf X /* (static only) */ +# endif + for (i = 0; i < 16; ++i, xp += 4) + xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24); + } +#endif + } + +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + + /* Round 1. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ +#define F(x, y, z) (((x) & (y)) | (~(x) & (z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + F(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 7, T1); + SET(d, a, b, c, 1, 12, T2); + SET(c, d, a, b, 2, 17, T3); + SET(b, c, d, a, 3, 22, T4); + SET(a, b, c, d, 4, 7, T5); + SET(d, a, b, c, 5, 12, T6); + SET(c, d, a, b, 6, 17, T7); + SET(b, c, d, a, 7, 22, T8); + SET(a, b, c, d, 8, 7, T9); + SET(d, a, b, c, 9, 12, T10); + SET(c, d, a, b, 10, 17, T11); + SET(b, c, d, a, 11, 22, T12); + SET(a, b, c, d, 12, 7, T13); + SET(d, a, b, c, 13, 12, T14); + SET(c, d, a, b, 14, 17, T15); + SET(b, c, d, a, 15, 22, T16); +#undef SET + + /* Round 2. */ + /* Let [abcd k s i] denote the operation + a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ +#define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + G(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 1, 5, T17); + SET(d, a, b, c, 6, 9, T18); + SET(c, d, a, b, 11, 14, T19); + SET(b, c, d, a, 0, 20, T20); + SET(a, b, c, d, 5, 5, T21); + SET(d, a, b, c, 10, 9, T22); + SET(c, d, a, b, 15, 14, T23); + SET(b, c, d, a, 4, 20, T24); + SET(a, b, c, d, 9, 5, T25); + SET(d, a, b, c, 14, 9, T26); + SET(c, d, a, b, 3, 14, T27); + SET(b, c, d, a, 8, 20, T28); + SET(a, b, c, d, 13, 5, T29); + SET(d, a, b, c, 2, 9, T30); + SET(c, d, a, b, 7, 14, T31); + SET(b, c, d, a, 12, 20, T32); +#undef SET + + /* Round 3. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + H(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 5, 4, T33); + SET(d, a, b, c, 8, 11, T34); + SET(c, d, a, b, 11, 16, T35); + SET(b, c, d, a, 14, 23, T36); + SET(a, b, c, d, 1, 4, T37); + SET(d, a, b, c, 4, 11, T38); + SET(c, d, a, b, 7, 16, T39); + SET(b, c, d, a, 10, 23, T40); + SET(a, b, c, d, 13, 4, T41); + SET(d, a, b, c, 0, 11, T42); + SET(c, d, a, b, 3, 16, T43); + SET(b, c, d, a, 6, 23, T44); + SET(a, b, c, d, 9, 4, T45); + SET(d, a, b, c, 12, 11, T46); + SET(c, d, a, b, 15, 16, T47); + SET(b, c, d, a, 2, 23, T48); +#undef SET + + /* Round 4. */ + /* Let [abcd k s t] denote the operation + a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ +#define I(x, y, z) ((y) ^ ((x) | ~(z))) +#define SET(a, b, c, d, k, s, Ti)\ + t = a + I(b,c,d) + X[k] + Ti;\ + a = ROTATE_LEFT(t, s) + b + /* Do the following 16 operations. */ + SET(a, b, c, d, 0, 6, T49); + SET(d, a, b, c, 7, 10, T50); + SET(c, d, a, b, 14, 15, T51); + SET(b, c, d, a, 5, 21, T52); + SET(a, b, c, d, 12, 6, T53); + SET(d, a, b, c, 3, 10, T54); + SET(c, d, a, b, 10, 15, T55); + SET(b, c, d, a, 1, 21, T56); + SET(a, b, c, d, 8, 6, T57); + SET(d, a, b, c, 15, 10, T58); + SET(c, d, a, b, 6, 15, T59); + SET(b, c, d, a, 13, 21, T60); + SET(a, b, c, d, 4, 6, T61); + SET(d, a, b, c, 11, 10, T62); + SET(c, d, a, b, 2, 15, T63); + SET(b, c, d, a, 9, 21, T64); +#undef SET + + /* Then perform the following additions. (That is increment each + of the four registers by the value it had before this block + was started.) */ + pms->abcd[0] += a; + pms->abcd[1] += b; + pms->abcd[2] += c; + pms->abcd[3] += d; +} + +void +md5_init(md5_state_t *pms) +{ + pms->count[0] = pms->count[1] = 0; + pms->abcd[0] = 0x67452301; + pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; + pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; + pms->abcd[3] = 0x10325476; +} + +void +md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) +{ + const md5_byte_t *p = data; + int left = nbytes; + int offset = (pms->count[0] >> 3) & 63; + md5_word_t nbits = (md5_word_t)(nbytes << 3); + + if (nbytes <= 0) + return; + + /* Update the message length. */ + pms->count[1] += nbytes >> 29; + pms->count[0] += nbits; + if (pms->count[0] < nbits) + pms->count[1]++; + + /* Process an initial partial block. */ + if (offset) { + int copy = (offset + nbytes > 64 ? 64 - offset : nbytes); + + memcpy(pms->buf + offset, p, copy); + if (offset + copy < 64) + return; + p += copy; + left -= copy; + md5_process(pms, pms->buf); + } + + /* Process full blocks. */ + for (; left >= 64; p += 64, left -= 64) + md5_process(pms, p); + + /* Process a final partial block. */ + if (left) + memcpy(pms->buf, p, left); +} + +void +md5_finish(md5_state_t *pms, md5_byte_t digest[16]) +{ + static const md5_byte_t pad[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + md5_byte_t data[8]; + int i; + + /* Save the length before padding. */ + for (i = 0; i < 8; ++i) + data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); + /* Pad to 56 bytes mod 64. */ + md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); + /* Append the length. */ + md5_append(pms, data, 8); + for (i = 0; i < 16; ++i) + digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); +} diff --git a/src/rcheevos/src/rhash/md5.h b/src/rcheevos/src/rhash/md5.h new file mode 100644 index 0000000000..698c995d8f --- /dev/null +++ b/src/rcheevos/src/rhash/md5.h @@ -0,0 +1,91 @@ +/* + Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + */ +/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */ +/* + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.h is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Removed support for non-ANSI compilers; removed + references to Ghostscript; clarified derivation from RFC 1321; + now handles byte order either statically or dynamically. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); + added conditionalization for C++ compilation from Martin + Purschke . + 1999-05-03 lpd Original version. + */ + +#ifndef md5_INCLUDED +# define md5_INCLUDED + +/* + * This package supports both compile-time and run-time determination of CPU + * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be + * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is + * defined as non-zero, the code will be compiled to run only on big-endian + * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to + * run on either big- or little-endian CPUs, but will run slightly less + * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. + */ + +typedef unsigned char md5_byte_t; /* 8-bit byte */ +typedef unsigned int md5_word_t; /* 32-bit word */ + +/* Define the state of the MD5 Algorithm. */ +typedef struct md5_state_s { + md5_word_t count[2]; /* message length in bits, lsw first */ + md5_word_t abcd[4]; /* digest buffer */ + md5_byte_t buf[64]; /* accumulate block */ +} md5_state_t; + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* Initialize the algorithm. */ +void md5_init(md5_state_t *pms); + +/* Append a string to the message. */ +void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes); + +/* Finish the message and return the digest. */ +void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); + +#ifdef __cplusplus +} /* end extern "C" */ +#endif + +#endif /* md5_INCLUDED */ diff --git a/src/rcheevos/src/rhash/rc_hash_internal.h b/src/rcheevos/src/rhash/rc_hash_internal.h new file mode 100644 index 0000000000..f50a9348d2 --- /dev/null +++ b/src/rcheevos/src/rhash/rc_hash_internal.h @@ -0,0 +1,116 @@ +#ifndef RC_HASH_INTERNAL_H +#define RC_HASH_INTERNAL_H + +#include "rc_hash.h" +#include "md5.h" + +RC_BEGIN_C_DECLS + +/* hash.c */ + +void* rc_file_open(const rc_hash_iterator_t* iterator, const char* path); +void rc_file_seek(const rc_hash_iterator_t* iterator, void* file_handle, int64_t offset, int origin); +int64_t rc_file_tell(const rc_hash_iterator_t* iterator, void* file_handle); +size_t rc_file_read(const rc_hash_iterator_t* iterator, void* file_handle, void* buffer, int requested_bytes); +void rc_file_close(const rc_hash_iterator_t* iterator, void* file_handle); +int64_t rc_file_size(const rc_hash_iterator_t* iterator, const char* path); + + +void rc_hash_iterator_verbose(const rc_hash_iterator_t* iterator, const char* message); +void rc_hash_iterator_verbose_formatted(const rc_hash_iterator_t* iterator, const char* format, ...); +int rc_hash_iterator_error(const rc_hash_iterator_t* iterator, const char* message); +int rc_hash_iterator_error_formatted(const rc_hash_iterator_t* iterator, const char* format, ...); + + +/* arbitrary limit to prevent allocating and hashing large files */ +#define MAX_BUFFER_SIZE 64 * 1024 * 1024 + +void rc_hash_merge_callbacks(rc_hash_iterator_t* iterator, const rc_hash_callbacks_t* callbacks); +int rc_hash_finalize(const rc_hash_iterator_t* iterator, md5_state_t* md5, char hash[33]); + +int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size, const rc_hash_iterator_t* iterator); +void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop); +void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop); + + +const char* rc_path_get_filename(const char* path); +const char* rc_path_get_extension(const char* path); +int rc_path_compare_extension(const char* path, const char* ext); + + +typedef void (RC_CCONV* rc_hash_iterator_ext_handler_t)(rc_hash_iterator_t* iterator, int data); +typedef struct rc_hash_iterator_ext_handler_entry_t { + char ext[8]; + rc_hash_iterator_ext_handler_t handler; + int data; +} rc_hash_iterator_ext_handler_entry_t; + +const rc_hash_iterator_ext_handler_entry_t* rc_hash_get_iterator_ext_handlers(size_t* num_handlers); + + +typedef struct rc_hash_cdrom_track_t { + void* file_handle; /* the file handle for reading the track data */ + const rc_hash_filereader_t* file_reader; /* functions to perform raw file I/O */ + int64_t file_track_offset;/* the offset of the track data within the file */ + int sector_size; /* the size of each sector in the track data */ + int sector_header_size; /* the offset to the raw data within a sector block */ + int raw_data_size; /* the amount of raw data within a sector block */ + int track_first_sector; /* the first absolute sector associated to the track (includes pregap) */ + int track_pregap_sectors; /* the number of pregap sectors */ +#ifndef NDEBUG + uint32_t track_id; /* the index of the track */ +#endif +} rc_hash_cdrom_track_t; + + +int rc_hash_whole_file(char hash[33], const rc_hash_iterator_t* iterator); +int rc_hash_buffered_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); + +#ifndef RC_HASH_NO_ROM + /* hash_rom.c */ + int rc_hash_7800(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_arcade(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_arduboy(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_nes(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_n64(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_nintendo_ds(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_pce(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_scv(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_snes(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +#ifndef RC_HASH_NO_DISC + /* hash_disc.c */ + void rc_hash_reset_iterator_disc(rc_hash_iterator_t* iterator); + + int rc_hash_3do(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_dreamcast(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_gamecube(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_jaguar_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_neogeo_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_pce_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_pcfx_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_psx(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_ps2(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_psp(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_sega_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_wii(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + /* hash_encrypted.c */ + void rc_hash_reset_iterator_encrypted(rc_hash_iterator_t* iterator); + + int rc_hash_nintendo_3ds(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +#ifndef RC_HASH_NO_ZIP + /* hash_zip.c */ + int rc_hash_ms_dos(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_arduboyfx(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +RC_END_C_DECLS + +#endif /* RC_HASH_INTERNAL_H */ From 921c74e744128d91189a93326b58859d2e1f7582 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:15:49 +0100 Subject: [PATCH 30/41] rcheevos v3 --- src/rcheevos/include/rc_api_editor.h | 296 + src/rcheevos/include/rc_api_info.h | 278 + src/rcheevos/include/rc_api_request.h | 77 + src/rcheevos/include/rc_api_runtime.h | 417 + src/rcheevos/include/rc_api_user.h | 262 + src/rcheevos/include/rc_client.h | 871 +++ .../include/rc_client_raintegration.h | 101 + src/rcheevos/include/rc_consoles.h | 138 + src/rcheevos/include/rc_error.h | 59 + src/rcheevos/include/rc_export.h | 100 + src/rcheevos/include/rc_hash.h | 200 + src/rcheevos/include/rc_runtime.h | 148 + src/rcheevos/include/rc_runtime_types.h | 452 ++ src/rcheevos/include/rc_util.h | 51 + src/rcheevos/include/rcheevos.h | 8 + src/rcheevos/src/rapi/rc_api_common.c | 1379 ++++ src/rcheevos/src/rapi/rc_api_common.h | 88 + src/rcheevos/src/rapi/rc_api_editor.c | 625 ++ src/rcheevos/src/rapi/rc_api_info.c | 582 ++ src/rcheevos/src/rapi/rc_api_runtime.c | 901 +++ src/rcheevos/src/rapi/rc_api_user.c | 483 ++ src/rcheevos/src/rc_client.c | 6775 +++++++++++++++++ src/rcheevos/src/rc_client_external.c | 277 + src/rcheevos/src/rc_client_external.h | 173 + .../src/rc_client_external_versions.h | 171 + src/rcheevos/src/rc_client_internal.h | 409 + src/rcheevos/src/rc_client_raintegration.c | 566 ++ .../src/rc_client_raintegration_internal.h | 61 + src/rcheevos/src/rc_client_types.natvis | 396 + src/rcheevos/src/rc_compat.c | 251 + src/rcheevos/src/rc_compat.h | 121 + src/rcheevos/src/rc_libretro.c | 915 +++ src/rcheevos/src/rc_libretro.h | 98 + src/rcheevos/src/rc_util.c | 199 + src/rcheevos/src/rc_version.c | 11 + src/rcheevos/src/rc_version.h | 32 + 36 files changed, 17971 insertions(+) create mode 100644 src/rcheevos/include/rc_api_editor.h create mode 100644 src/rcheevos/include/rc_api_info.h create mode 100644 src/rcheevos/include/rc_api_request.h create mode 100644 src/rcheevos/include/rc_api_runtime.h create mode 100644 src/rcheevos/include/rc_api_user.h create mode 100644 src/rcheevos/include/rc_client.h create mode 100644 src/rcheevos/include/rc_client_raintegration.h create mode 100644 src/rcheevos/include/rc_consoles.h create mode 100644 src/rcheevos/include/rc_error.h create mode 100644 src/rcheevos/include/rc_export.h create mode 100644 src/rcheevos/include/rc_hash.h create mode 100644 src/rcheevos/include/rc_runtime.h create mode 100644 src/rcheevos/include/rc_runtime_types.h create mode 100644 src/rcheevos/include/rc_util.h create mode 100644 src/rcheevos/include/rcheevos.h create mode 100644 src/rcheevos/src/rapi/rc_api_common.c create mode 100644 src/rcheevos/src/rapi/rc_api_common.h create mode 100644 src/rcheevos/src/rapi/rc_api_editor.c create mode 100644 src/rcheevos/src/rapi/rc_api_info.c create mode 100644 src/rcheevos/src/rapi/rc_api_runtime.c create mode 100644 src/rcheevos/src/rapi/rc_api_user.c create mode 100644 src/rcheevos/src/rc_client.c create mode 100644 src/rcheevos/src/rc_client_external.c create mode 100644 src/rcheevos/src/rc_client_external.h create mode 100644 src/rcheevos/src/rc_client_external_versions.h create mode 100644 src/rcheevos/src/rc_client_internal.h create mode 100644 src/rcheevos/src/rc_client_raintegration.c create mode 100644 src/rcheevos/src/rc_client_raintegration_internal.h create mode 100644 src/rcheevos/src/rc_client_types.natvis create mode 100644 src/rcheevos/src/rc_compat.c create mode 100644 src/rcheevos/src/rc_compat.h create mode 100644 src/rcheevos/src/rc_libretro.c create mode 100644 src/rcheevos/src/rc_libretro.h create mode 100644 src/rcheevos/src/rc_util.c create mode 100644 src/rcheevos/src/rc_version.c create mode 100644 src/rcheevos/src/rc_version.h diff --git a/src/rcheevos/include/rc_api_editor.h b/src/rcheevos/include/rc_api_editor.h new file mode 100644 index 0000000000..61e2fd4edd --- /dev/null +++ b/src/rcheevos/include/rc_api_editor.h @@ -0,0 +1,296 @@ +#ifndef RC_API_EDITOR_H +#define RC_API_EDITOR_H + +#include "rc_api_request.h" + +#include + +RC_BEGIN_C_DECLS + +/* --- Fetch Code Notes --- */ + +/** + * API parameters for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_request_t { + /* The unique identifier of the game */ + uint32_t game_id; +} +rc_api_fetch_code_notes_request_t; + +/* A code note definiton */ +typedef struct rc_api_code_note_t { + /* The address the note is associated to */ + uint32_t address; + /* The name of the use who last updated the note */ + const char* author; + /* The contents of the note */ + const char* note; +} rc_api_code_note_t; + +/** + * Response data for a fetch code notes request. + */ +typedef struct rc_api_fetch_code_notes_response_t { + /* An array of code notes for the game */ + rc_api_code_note_t* notes; + /* The number of items in the notes array */ + uint32_t num_notes; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_code_notes_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_code_notes_request_hosted(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_code_notes_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response); + +/* --- Update Code Note --- */ + +/** + * API parameters for an update code note request. + */ +typedef struct rc_api_update_code_note_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The address the note is associated to */ + uint32_t address; + /* The contents of the note (NULL or empty to delete a note) */ + const char* note; +} +rc_api_update_code_note_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_update_code_note_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_code_note_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_update_code_note_request_hosted(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_update_code_note_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response); + +/* --- Update Achievement --- */ + +/** + * API parameters for an update achievement request. + */ +typedef struct rc_api_update_achievement_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement (0 to create a new achievement) */ + uint32_t achievement_id; + /* The unique identifier of the game */ + uint32_t game_id; + /* The name of the achievement */ + const char* title; + /* The description of the achievement */ + const char* description; + /* The badge name for the achievement */ + const char* badge; + /* The serialized trigger for the achievement */ + const char* trigger; + /* The number of points the achievement is worth */ + uint32_t points; + /* The category of the achievement */ + uint32_t category; + /* The type of the achievement */ + uint32_t type; +} +rc_api_update_achievement_request_t; + +/** + * Response data for an update achievement request. + */ +typedef struct rc_api_update_achievement_response_t { + /* The unique identifier of the achievement */ + uint32_t achievement_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_achievement_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_update_achievement_request_hosted(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_update_achievement_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response); + +/* --- Update Leaderboard --- */ + +/** + * API parameters for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard (0 to create a new leaderboard) */ + uint32_t leaderboard_id; + /* The unique identifier of the game */ + uint32_t game_id; + /* The name of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The start trigger for the leaderboard */ + const char* start_trigger; + /* The submit trigger for the leaderboard */ + const char* submit_trigger; + /* The cancel trigger for the leaderboard */ + const char* cancel_trigger; + /* The value definition for the leaderboard */ + const char* value_definition; + /* The format of leaderboard values */ + const char* format; + /* Whether or not lower scores are better for the leaderboard */ + uint32_t lower_is_better; +} +rc_api_update_leaderboard_request_t; + +/** + * Response data for an update leaderboard request. + */ +typedef struct rc_api_update_leaderboard_response_t { + /* The unique identifier of the leaderboard */ + uint32_t leaderboard_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_leaderboard_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_update_leaderboard_request_hosted(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_update_leaderboard_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response); + +/* --- Update Rich Presence --- */ + +/** + * API parameters for an update rich presence request. + */ +typedef struct rc_api_update_rich_presence_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The script for the rich_presence */ + const char* script; +} +rc_api_update_rich_presence_request_t; + +/** + * Response data for an update rich presence request. + */ +typedef struct rc_api_update_rich_presence_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_update_rich_presence_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_update_rich_presence_request(rc_api_request_t* request, const rc_api_update_rich_presence_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_update_rich_presence_request_hosted(rc_api_request_t* request, const rc_api_update_rich_presence_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_update_rich_presence_server_response(rc_api_update_rich_presence_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_update_rich_presence_response(rc_api_update_rich_presence_response_t* response); + +/* --- Fetch Badge Range --- */ + +/** + * API parameters for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_request_t { + /* Unused */ + uint32_t unused; +} +rc_api_fetch_badge_range_request_t; + +/** + * Response data for a fetch badge range request. + */ +typedef struct rc_api_fetch_badge_range_response_t { + /* The numeric identifier of the first valid badge ID */ + uint32_t first_badge_id; + /* The numeric identifier of the first unassigned badge ID */ + uint32_t next_badge_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_badge_range_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_badge_range_request_hosted(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_badge_range_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response); + +/* --- Add Game Hash --- */ + +/** + * API parameters for an add game hash request. + */ +typedef struct rc_api_add_game_hash_request_t { + /* The username of the developer */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game (0 to create a new game entry) */ + uint32_t game_id; + /* The unique identifier of the console for the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The hash being added */ + const char* hash; + /* A description of the hash being added (usually the normalized ROM name) */ + const char* hash_description; +} +rc_api_add_game_hash_request_t; + +/** + * Response data for an update code note request. + */ +typedef struct rc_api_add_game_hash_response_t { + /* The unique identifier of the game */ + uint32_t game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_add_game_hash_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_add_game_hash_request_hosted(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_add_game_hash_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_EDITOR_H */ diff --git a/src/rcheevos/include/rc_api_info.h b/src/rcheevos/include/rc_api_info.h new file mode 100644 index 0000000000..ee5121df05 --- /dev/null +++ b/src/rcheevos/include/rc_api_info.h @@ -0,0 +1,278 @@ +#ifndef RC_API_INFO_H +#define RC_API_INFO_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/* --- Fetch Achievement Info --- */ + +/** + * API parameters for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + uint32_t achievement_id; + /* The 1-based index of the first entry to retrieve */ + uint32_t first_entry; + /* The number of entries to retrieve */ + uint32_t count; + /* Non-zero to only return unlocks earned by the user's friends */ + uint32_t friends_only; +} +rc_api_fetch_achievement_info_request_t; + +/* An achievement awarded entry */ +typedef struct rc_api_achievement_awarded_entry_t { + /* The user associated to the entry */ + const char* username; + /* A URL to the user's avatar image */ + const char* avatar_url; + /* When the achievement was awarded */ + time_t awarded; +} +rc_api_achievement_awarded_entry_t; + +/** + * Response data for a fetch achievement info request. + */ +typedef struct rc_api_fetch_achievement_info_response_t { + /* The unique identifier of the achievement */ + uint32_t id; + /* The unique identifier of the game to which the leaderboard is associated */ + uint32_t game_id; + /* The number of times the achievement has been awarded */ + uint32_t num_awarded; + /* The number of players that have earned at least one achievement for the game */ + uint32_t num_players; + + /* An array of recently rewarded entries */ + rc_api_achievement_awarded_entry_t* recently_awarded; + /* The number of items in the recently_awarded array */ + uint32_t num_recently_awarded; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_achievement_info_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_achievement_info_request_hosted(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_achievement_info_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); + +/* --- Fetch Leaderboard Info --- */ + +/** + * API parameters for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_request_t { + /* The unique identifier of the leaderboard */ + uint32_t leaderboard_id; + /* The number of entries to retrieve */ + uint32_t count; + /* The 1-based index of the first entry to retrieve */ + uint32_t first_entry; + /* The username of the player around whom the entries should be returned */ + const char* username; +} +rc_api_fetch_leaderboard_info_request_t; + +/* A leaderboard info entry */ +typedef struct rc_api_lboard_info_entry_t { + /* The user associated to the entry */ + const char* username; + /* A URL to the user's avatar image */ + const char* avatar_url; + /* The rank of the entry */ + uint32_t rank; + /* The index of the entry */ + uint32_t index; + /* The value of the entry */ + int32_t score; + /* When the entry was submitted */ + time_t submitted; +} +rc_api_lboard_info_entry_t; + +/** + * Response data for a fetch leaderboard info request. + */ +typedef struct rc_api_fetch_leaderboard_info_response_t { + /* The unique identifier of the leaderboard */ + uint32_t id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* If non-zero, indicates that lower scores appear first */ + uint32_t lower_is_better; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ + const char* definition; + /* The unique identifier of the game to which the leaderboard is associated */ + uint32_t game_id; + /* The author of the leaderboard */ + const char* author; + /* When the leaderboard was first uploaded to the server */ + time_t created; + /* When the leaderboard was last modified on the server */ + time_t updated; + + /* An array of requested entries */ + rc_api_lboard_info_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* The total number of entries on the server */ + uint32_t total_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_leaderboard_info_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_leaderboard_info_request_hosted(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_leaderboard_info_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); + +/* --- Fetch Games List --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_request_t { + /* The unique identifier of the console to query */ + uint32_t console_id; +} +rc_api_fetch_games_list_request_t; + +/* A game list entry */ +typedef struct rc_api_game_list_entry_t { + /* The unique identifier of the game */ + uint32_t id; + /* The name of the game */ + const char* name; +} +rc_api_game_list_entry_t; + +/** + * Response data for a fetch games list request. + */ +typedef struct rc_api_fetch_games_list_response_t { + /* An array of requested entries */ + rc_api_game_list_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_games_list_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_games_list_request_hosted(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_games_list_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); + +/* --- Fetch Game Titles --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_game_titles_request_t { + /* An array of game ids to fetch titles for */ + const uint32_t* game_ids; + /* The number of items in the game_ids array */ + uint32_t num_game_ids; +} +rc_api_fetch_game_titles_request_t; + +/* A game title entry */ +typedef struct rc_api_game_title_entry_t { + /* The unique identifier of the game */ + uint32_t id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; +} +rc_api_game_title_entry_t; + +/** + * Response data for a fetch games title request. + */ +typedef struct rc_api_fetch_game_titles_response_t { + /* An array of requested entries */ + rc_api_game_title_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_titles_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_titles_request(rc_api_request_t* request, const rc_api_fetch_game_titles_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_titles_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_titles_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_titles_server_response(rc_api_fetch_game_titles_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_titles_response(rc_api_fetch_game_titles_response_t* response); + +/* --- Fetch Game Hashes --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_hash_library_request_t { + /** + * The unique identifier of the console to query. + * Passing RC_CONSOLE_UNKNOWN will return hashes for all consoles. + */ + uint32_t console_id; +} rc_api_fetch_hash_library_request_t; + +/* A hash library entry */ +typedef struct rc_api_hash_library_entry_t { + /* The hash for the game */ + const char* hash; + /* The unique identifier of the game */ + uint32_t game_id; +} rc_api_hash_library_entry_t; + +/** + * Response data for a fetch hash library request. + */ +typedef struct rc_api_fetch_hash_library_response_t { + /* An array of entries, one per game */ + rc_api_hash_library_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_hash_library_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_hash_library_request(rc_api_request_t* request, const rc_api_fetch_hash_library_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_hash_library_request_hosted(rc_api_request_t* request, const rc_api_fetch_hash_library_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_hash_library_server_response(rc_api_fetch_hash_library_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_hash_library_response(rc_api_fetch_hash_library_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_API_INFO_H */ diff --git a/src/rcheevos/include/rc_api_request.h b/src/rcheevos/include/rc_api_request.h new file mode 100644 index 0000000000..ddccc7bc28 --- /dev/null +++ b/src/rcheevos/include/rc_api_request.h @@ -0,0 +1,77 @@ +#ifndef RC_API_REQUEST_H +#define RC_API_REQUEST_H + +#include "rc_error.h" +#include "rc_util.h" + +#include + +RC_BEGIN_C_DECLS + +/** + * Information about the server being connected to. + */ +typedef struct rc_api_host_t { + /* The host name for the API calls (retroachievements.org) */ + const char* host; + /* The host name for media URLs (media.retroachievements.org) */ + const char* media_host; +} +rc_api_host_t; + +/** + * A constructed request to send to the retroachievements server. + */ +typedef struct rc_api_request_t { + /* The URL to send the request to (contains protocol, host, path, and query args) */ + const char* url; + /* Additional query args that should be sent via a POST command. If null, GET may be used */ + const char* post_data; + /* The HTTP Content-Type of the POST data. */ + const char* content_type; + + /* Storage for the url and post_data */ + rc_buffer_t buffer; +} +rc_api_request_t; + +/** + * Common attributes for all server responses. + */ +typedef struct rc_api_response_t { + /* Server-provided success indicator (non-zero on success, zero on failure) */ + int succeeded; + /* Server-provided message associated to the failure */ + const char* error_message; + /* Server-provided error code associated to the failure */ + const char* error_code; + + /* Storage for the response data */ + rc_buffer_t buffer; +} +rc_api_response_t; + +RC_EXPORT void RC_CCONV rc_api_destroy_request(rc_api_request_t* request); + +/* [deprecated] use rc_api_init_*_hosted instead */ +RC_EXPORT void RC_CCONV rc_api_set_host(const char* hostname); +/* [deprecated] use rc_api_init_*_hosted instead */ +RC_EXPORT void RC_CCONV rc_api_set_image_host(const char* hostname); + +typedef struct rc_api_server_response_t { + /* Pointer to the data returned from the server */ + const char* body; + /* Length of data returned from the server (Content-Length) */ + size_t body_length; + /* HTTP status code returned from the server */ + int http_status_code; +} rc_api_server_response_t; + +enum { + RC_API_SERVER_RESPONSE_CLIENT_ERROR = -1, + RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR = -2 +}; + +RC_END_C_DECLS + +#endif /* RC_API_REQUEST_H */ diff --git a/src/rcheevos/include/rc_api_runtime.h b/src/rcheevos/include/rc_api_runtime.h new file mode 100644 index 0000000000..8a0de01b7f --- /dev/null +++ b/src/rcheevos/include/rc_api_runtime.h @@ -0,0 +1,417 @@ +#ifndef RC_API_RUNTIME_H +#define RC_API_RUNTIME_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/* --- Fetch Image --- */ + +/** + * API parameters for a fetch image request. + * NOTE: fetch image server response is the raw image data. There is no rc_api_process_fetch_image_response function. + */ +typedef struct rc_api_fetch_image_request_t { + /* The name of the image to fetch */ + const char* image_name; + /* The type of image to fetch */ + uint32_t image_type; +} +rc_api_fetch_image_request_t; + +#define RC_IMAGE_TYPE_GAME 1 +#define RC_IMAGE_TYPE_ACHIEVEMENT 2 +#define RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED 3 +#define RC_IMAGE_TYPE_USER 4 + +RC_EXPORT int RC_CCONV rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host); + +/* --- Resolve Hash --- */ + +/** + * API parameters for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_request_t { + /* Unused - hash lookup does not require credentials */ + const char* username; + /* Unused - hash lookup does not require credentials */ + const char* api_token; + /* The generated hash of the game to be identified */ + const char* game_hash; +} +rc_api_resolve_hash_request_t; + +/** + * Response data for a resolve hash request. + */ +typedef struct rc_api_resolve_hash_response_t { + /* The unique identifier of the game, 0 if no match was found */ + uint32_t game_id; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_resolve_hash_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_resolve_hash_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); + +/* --- Fetch Game Data --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The generated hash of the game to be identified (ignored if game_id is not 0) */ + const char* game_hash; +} +rc_api_fetch_game_data_request_t; + +/* A leaderboard definition */ +typedef struct rc_api_leaderboard_definition_t { + /* The unique identifier of the leaderboard */ + uint32_t id; + /* The format to pass to rc_format_value to format the leaderboard value */ + int format; + /* The title of the leaderboard */ + const char* title; + /* The description of the leaderboard */ + const char* description; + /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ + const char* definition; + /* Non-zero if lower values are better for this leaderboard */ + uint8_t lower_is_better; + /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ + uint8_t hidden; +} +rc_api_leaderboard_definition_t; + +/* An achievement definition */ +typedef struct rc_api_achievement_definition_t { + /* The unique identifier of the achievement */ + uint32_t id; + /* The number of points the achievement is worth */ + uint32_t points; + /* The achievement category (core, unofficial) */ + uint32_t category; + /* The title of the achievement */ + const char* title; + /* The description of the achievement */ + const char* description; + /* The definition of the achievement to be passed to rc_runtime_activate_achievement */ + const char* definition; + /* The author of the achievment */ + const char* author; + /* The image name for the achievement badge */ + const char* badge_name; + /* When the achievement was first uploaded to the server */ + time_t created; + /* When the achievement was last modified on the server */ + time_t updated; + /* The achievement type (win/progression/missable) */ + uint32_t type; + /* The approximate rarity of the achievement (X% of users have earned the achievement) */ + float rarity; + /* The approximate rarity of the achievement in hardcore (X% of users have earned the achievement in hardcore) */ + float rarity_hardcore; + /* The URL for the achievement badge */ + const char* badge_url; + /* The URL for the locked achievement badge */ + const char* badge_locked_url; +} +rc_api_achievement_definition_t; + +#define RC_ACHIEVEMENT_CATEGORY_CORE 3 +#define RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL 5 + +#define RC_ACHIEVEMENT_TYPE_STANDARD 0 +#define RC_ACHIEVEMENT_TYPE_MISSABLE 1 +#define RC_ACHIEVEMENT_TYPE_PROGRESSION 2 +#define RC_ACHIEVEMENT_TYPE_WIN 3 + +/** + * Response data for a fetch game data request. + */ +typedef struct rc_api_fetch_game_data_response_t { + /* The unique identifier of the game */ + uint32_t id; + /* The console associated to the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The URL for the game badge */ + const char* image_url; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + + /* An array of achievements for the game */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + uint32_t num_achievements; + + /* An array of leaderboards for the game */ + rc_api_leaderboard_definition_t* leaderboards; + /* The number of items in the leaderboards array */ + uint32_t num_leaderboards; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_data_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_game_data_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); + +/* --- Fetch Game Sets --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_sets_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The generated hash of the game to be identified (ignored if game_id is not 0) */ + const char* game_hash; +} +rc_api_fetch_game_sets_request_t; + +#define RC_ACHIEVEMENT_SET_TYPE_CORE 0 +#define RC_ACHIEVEMENT_SET_TYPE_BONUS 1 +#define RC_ACHIEVEMENT_SET_TYPE_SPECIALTY 2 +#define RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE 3 + +/* A game subset definition */ +typedef struct rc_api_achievement_set_definition_t { + /* The unique identifier of the achievement set */ + uint32_t id; + /* The legacy game_id of the achievement set (used for editor API calls) */ + uint32_t game_id; + /* The title of the achievement set */ + const char* title; + /* The image name for the achievement set badge */ + const char* image_name; + /* The URL for the achievement set badge */ + const char* image_url; + + /* An array of achievements for the achievement set */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + uint32_t num_achievements; + + /* An array of leaderboards for the achievement set */ + rc_api_leaderboard_definition_t* leaderboards; + /* The number of items in the leaderboards array */ + uint32_t num_leaderboards; + + /* The type of the achievement set */ + uint8_t type; +} +rc_api_achievement_set_definition_t; + +/** + * Response data for a fetch game sets request. + */ +typedef struct rc_api_fetch_game_sets_response_t { + /* The unique identifier of the game */ + uint32_t id; + /* The console associated to the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The URL for the game badge */ + const char* image_url; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + /* The unique identifier of the game to use for session requests (startsession/ping/etc) */ + uint32_t session_game_id; + + /* An array of sets for the game */ + rc_api_achievement_set_definition_t* sets; + /* The number of items in the sets array */ + uint32_t num_sets; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_sets_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response); + +/* --- Ping --- */ + +/** + * API parameters for a ping request. + */ +typedef struct rc_api_ping_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* (optional) The current rich presence evaluation for the user */ + const char* rich_presence; + /* (recommended) The hash associated to the game being played */ + const char* game_hash; + /* Non-zero if hardcore is currently enabled (ignored if game_hash is null) */ + uint32_t hardcore; +} +rc_api_ping_request_t; + +/** + * Response data for a ping request. + */ +typedef struct rc_api_ping_response_t { + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_ping_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_ping_request_hosted(rc_api_request_t* request, const rc_api_ping_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_ping_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_ping_response(rc_api_ping_response_t* response); + +/* --- Award Achievement --- */ + +/** + * API parameters for an award achievement request. + */ +typedef struct rc_api_award_achievement_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the achievement */ + uint32_t achievement_id; + /* Non-zero if the achievement was earned in hardcore */ + uint32_t hardcore; + /* The hash associated to the game being played */ + const char* game_hash; + /* The number of seconds since the achievement was unlocked */ + uint32_t seconds_since_unlock; +} +rc_api_award_achievement_request_t; + +/** + * Response data for an award achievement request. + */ +typedef struct rc_api_award_achievement_response_t { + /* The unique identifier of the achievement that was awarded */ + uint32_t awarded_achievement_id; + /* The updated player score */ + uint32_t new_player_score; + /* The updated player softcore score */ + uint32_t new_player_score_softcore; + /* The number of achievements the user has not yet unlocked for this game + * (in hardcore/non-hardcore per hardcore flag in request) */ + uint32_t achievements_remaining; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_award_achievement_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_award_achievement_request_hosted(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_award_achievement_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); + +/* --- Submit Leaderboard Entry --- */ + +/** + * API parameters for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the leaderboard */ + uint32_t leaderboard_id; + /* The value being submitted */ + int32_t score; + /* The hash associated to the game being played */ + const char* game_hash; + /* The number of seconds since the leaderboard attempt was completed */ + uint32_t seconds_since_completion; +} +rc_api_submit_lboard_entry_request_t; + +/* A leaderboard entry */ +typedef struct rc_api_lboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + uint32_t rank; + /* The value of the entry */ + int32_t score; +} +rc_api_lboard_entry_t; + +/** + * Response data for a submit lboard entry request. + */ +typedef struct rc_api_submit_lboard_entry_response_t { + /* The value that was submitted */ + int32_t submitted_score; + /* The player's best submitted value */ + int32_t best_score; + /* The player's new rank within the leaderboard */ + uint32_t new_rank; + /* The total number of entries in the leaderboard */ + uint32_t num_entries; + + /* An array of the top entries for the leaderboard */ + rc_api_lboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + uint32_t num_top_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_submit_lboard_entry_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_submit_lboard_entry_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_API_RUNTIME_H */ diff --git a/src/rcheevos/include/rc_api_user.h b/src/rcheevos/include/rc_api_user.h new file mode 100644 index 0000000000..c977da9564 --- /dev/null +++ b/src/rcheevos/include/rc_api_user.h @@ -0,0 +1,262 @@ +#ifndef RC_API_USER_H +#define RC_API_USER_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/* --- Login --- */ + +/** + * API parameters for a login request. + * If both password and api_token are provided, api_token will be ignored. + */ +typedef struct rc_api_login_request_t { + /* The username of the player being logged in */ + const char* username; + /* The API token from a previous login */ + const char* api_token; + /* The player's password */ + const char* password; +} +rc_api_login_request_t; + +/** + * Response data for a login request. + */ +typedef struct rc_api_login_response_t { + /* The case-corrected username of the player */ + const char* username; + /* The API token to use for all future requests */ + const char* api_token; + /* The current score of the player */ + uint32_t score; + /* The current softcore score of the player */ + uint32_t score_softcore; + /* The number of unread messages waiting for the player on the web site */ + uint32_t num_unread_messages; + /* The preferred name to display for the player */ + const char* display_name; + /* A URL to the user's avatar image */ + const char* avatar_url; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_login_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_login_request_hosted(rc_api_request_t* request, const rc_api_login_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_login_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_login_response(rc_api_login_response_t* response); + +/* --- Start Session --- */ + +/** + * API parameters for a start session request. + */ +typedef struct rc_api_start_session_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* (recommended) The hash associated to the game being played */ + const char* game_hash; + /* Non-zero if hardcore is currently enabled (ignored if game_hash is null) */ + uint32_t hardcore; +} +rc_api_start_session_request_t; + +/** + * Response data for an achievement unlock. + */ +typedef struct rc_api_unlock_entry_t { + /* The unique identifier of the unlocked achievement */ + uint32_t achievement_id; + /* When the achievement was unlocked */ + time_t when; +} +rc_api_unlock_entry_t; + +/** + * Response data for a start session request. + */ +typedef struct rc_api_start_session_response_t { + /* An array of hardcore user unlocks */ + rc_api_unlock_entry_t* hardcore_unlocks; + /* An array of user unlocks */ + rc_api_unlock_entry_t* unlocks; + + /* The number of items in the hardcore_unlocks array */ + uint32_t num_hardcore_unlocks; + /* The number of items in the unlocks array */ + uint32_t num_unlocks; + + /* The server timestamp when the response was generated */ + time_t server_now; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_start_session_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_start_session_request_hosted(rc_api_request_t* request, const rc_api_start_session_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_start_session_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); + +/* --- Fetch User Unlocks --- */ + +/** + * API parameters for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* Non-zero to fetch hardcore unlocks, 0 to fetch non-hardcore unlocks */ + uint32_t hardcore; +} +rc_api_fetch_user_unlocks_request_t; + +/** + * Response data for a fetch user unlocks request. + */ +typedef struct rc_api_fetch_user_unlocks_response_t { + /* An array of achievement IDs previously unlocked by the user */ + uint32_t* achievement_ids; + /* The number of items in the achievement_ids array */ + uint32_t num_achievement_ids; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_user_unlocks_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_user_unlocks_request_hosted(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_user_unlocks_server_response instead */ +RC_EXPORT int RC_CCONV rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +RC_EXPORT int RC_CCONV rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); + +/* --- Fetch Followed Users --- */ + +/** + * API parameters for a fetch followed users request. + */ +typedef struct rc_api_fetch_followed_users_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; +} +rc_api_fetch_followed_users_request_t; + +typedef struct rc_api_followed_user_activity_t { + /* The record associated to the activity */ + const char* context; + /* The image of the record associated to the activity */ + const char* context_image_url; + /* The description of the activity */ + const char* description; + /* The time of the activity */ + time_t when; + /* The unique identifier of the record associated to the activity */ + uint32_t context_id; +} +rc_api_followed_user_activity_t; + +/** + * Response data for a followed user. + */ +typedef struct rc_api_followed_user_t { + /* The preferred name to display for the player */ + const char* display_name; + /* A URL to the user's avatar image */ + const char* avatar_url; + /* The player's last registered activity */ + rc_api_followed_user_activity_t recent_activity; + /* The current score of the player */ + uint32_t score; +} +rc_api_followed_user_t; + +/** + * Response data for a fetch followed users request. + */ +typedef struct rc_api_fetch_followed_users_response_t { + /* An array of followed user information */ + rc_api_followed_user_t* users; + /* The number of items in the users array */ + uint32_t num_users; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_followed_users_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_followed_users_request(rc_api_request_t* request, const rc_api_fetch_followed_users_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_followed_users_request_hosted(rc_api_request_t* request, const rc_api_fetch_followed_users_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_users_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_followed_users_response(rc_api_fetch_followed_users_response_t* response); + +/* --- Fetch All Progress --- */ + +/** + * API parameters for a fetch all user progress request. + */ +typedef struct rc_api_fetch_all_user_progress_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the console to query */ + uint32_t console_id; +} rc_api_fetch_all_user_progress_request_t; + +/* An all-user-progress entry */ +typedef struct rc_api_all_user_progress_entry_t { + /* The unique identifier of the game */ + uint32_t game_id; + /* The total number of achievements for this game */ + uint32_t num_achievements; + /* The total number of unlocked achievements for this game in softcore mode */ + uint32_t num_unlocked_achievements; + /* The total number of unlocked achievements for this game in hardcore mode */ + uint32_t num_unlocked_achievements_hardcore; +} rc_api_all_user_progress_entry_t; + +/** + * Response data for a fetch all user progress request. + */ +typedef struct rc_api_fetch_all_user_progress_response_t { + /* An array of entries, one per game */ + rc_api_all_user_progress_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} rc_api_fetch_all_user_progress_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_all_user_progress_request(rc_api_request_t* request, const rc_api_fetch_all_user_progress_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_all_user_progress_request_hosted(rc_api_request_t* request, const rc_api_fetch_all_user_progress_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_all_user_progress_server_response(rc_api_fetch_all_user_progress_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_all_user_progress_response(rc_api_fetch_all_user_progress_response_t* response); + +RC_END_C_DECLS + +#endif /* RC_API_H */ diff --git a/src/rcheevos/include/rc_client.h b/src/rcheevos/include/rc_client.h new file mode 100644 index 0000000000..ef3507217d --- /dev/null +++ b/src/rcheevos/include/rc_client.h @@ -0,0 +1,871 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +RC_BEGIN_C_DECLS + +/* implementation abstracted in rc_client_internal.h */ +typedef struct rc_client_t rc_client_t; +typedef struct rc_client_async_handle_t rc_client_async_handle_t; + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address into buffer. + * Returns the number of bytes read. A return value of 0 indicates the address was invalid. + */ +typedef uint32_t (RC_CCONV *rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); + +/** + * Internal method passed to rc_client_server_call_t to process the server response. + */ +typedef void (RC_CCONV *rc_client_server_callback_t)(const rc_api_server_response_t* server_response, void* callback_data); + +/** + * Callback used to issue a request to the server. + */ +typedef void (RC_CCONV *rc_client_server_call_t)(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +/** + * Generic callback for asynchronous eventing. + */ +typedef void (RC_CCONV *rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +/** + * Callback for logging or displaying a message. + */ +typedef void (RC_CCONV *rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +RC_EXPORT rc_client_t* RC_CCONV rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function); + +/** + * Releases resources associated to a rc_client_t object. + * Pointer will no longer be valid after making this call. + */ +RC_EXPORT void RC_CCONV rc_client_destroy(rc_client_t* client); + +/** + * Sets whether hardcore is enabled (on by default). + * Can be called with a game loaded. + * Enabling hardcore with a game loaded will raise an RC_CLIENT_EVENT_RESET + * event. Processing will be disabled until rc_client_reset is called. + */ +RC_EXPORT void RC_CCONV rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +RC_EXPORT int RC_CCONV rc_client_get_hardcore_enabled(const rc_client_t* client); + +/** + * Sets whether encore mode is enabled (off by default). + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +RC_EXPORT void RC_CCONV rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +RC_EXPORT int RC_CCONV rc_client_get_encore_mode_enabled(const rc_client_t* client); + +/** + * Sets whether unofficial achievements should be loaded. + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +RC_EXPORT void RC_CCONV rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +RC_EXPORT int RC_CCONV rc_client_get_unofficial_enabled(const rc_client_t* client); + +/** + * Sets whether spectator mode is enabled (off by default). + * If enabled, events for achievement unlocks and leaderboard submissions will be + * raised, but server calls to actually perform the unlock/submit will not occur. + * Can be modified while a game is loaded. Evaluated at unlock/submit time. + * Cannot be modified if disabled before a game is loaded. + */ +RC_EXPORT void RC_CCONV rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +RC_EXPORT int RC_CCONV rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +RC_EXPORT void RC_CCONV rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +RC_EXPORT void* RC_CCONV rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +RC_EXPORT void RC_CCONV rc_client_set_host(rc_client_t* client, const char* hostname); + +typedef uint64_t rc_clock_t; +typedef rc_clock_t (RC_CCONV *rc_get_time_millisecs_func_t)(const rc_client_t* client); + +/** + * Specifies a function that returns a value that increases once per millisecond. + */ +RC_EXPORT void RC_CCONV rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler); + +/** + * Marks an async process as aborted. The associated callback will not be called. + */ +RC_EXPORT void RC_CCONV rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/** + * Gets a clause that can be added to the User-Agent to identify the version of rcheevos being used. + */ +RC_EXPORT size_t RC_CCONV rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +RC_EXPORT void RC_CCONV rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback); +enum { + RC_CLIENT_LOG_LEVEL_NONE = 0, + RC_CLIENT_LOG_LEVEL_ERROR = 1, + RC_CLIENT_LOG_LEVEL_WARN = 2, + RC_CLIENT_LOG_LEVEL_INFO = 3, + RC_CLIENT_LOG_LEVEL_VERBOSE = 4, + NUM_RC_CLIENT_LOG_LEVELS = 5 +}; + +/*****************************************************************************\ +| User | +\*****************************************************************************/ + +/** + * Attempt to login a user. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Attempt to login a user. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Logout the user. + */ +RC_EXPORT void RC_CCONV rc_client_logout(rc_client_t* client); + +typedef struct rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; + /* minimum version: 12.0 */ + const char* avatar_url; +} rc_client_user_t; + +/** + * Gets information about the logged in user. Will return NULL if the user is not logged in. + */ +RC_EXPORT const rc_client_user_t* RC_CCONV rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size); + +typedef struct rc_client_user_game_summary_t { + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + + uint32_t points_core; + uint32_t points_unlocked; + + /* minimum version: 12.1 */ + time_t beaten_time; + time_t completed_time; +} rc_client_user_game_summary_t; + +/** + * Gets a breakdown of the number of achievements in the game, and how many the user has unlocked. + * Used for the "You have unlocked X of Y achievements" message shown when the game starts. + */ +RC_EXPORT void RC_CCONV rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); + +typedef struct rc_client_all_user_progress_entry_t { + uint32_t game_id; + uint32_t num_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unlocked_achievements_hardcore; +} rc_client_all_user_progress_entry_t; + +typedef struct rc_client_all_user_progress_t { + rc_client_all_user_progress_entry_t* entries; + uint32_t num_entries; +} rc_client_all_user_progress_t; + +/** + * Callback that is fired when an all progress query completes. list may be null if the query failed. + */ +typedef void(RC_CCONV* rc_client_fetch_all_user_progress_callback_t)(int result, const char* error_message, + rc_client_all_user_progress_t* list, + rc_client_t* client, void* callback_userdata); + +/** + * Starts an asynchronous request for all progress for the given console. + * This query returns the total number of achievements for all games tracked by this console, as well as + * the user's achievement unlock count for both softcore and hardcore modes. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV +rc_client_begin_fetch_all_user_progress(rc_client_t* client, uint32_t console_id, + rc_client_fetch_all_user_progress_callback_t callback, void* callback_userdata); + +/** + * Destroys a previously-allocated result from the rc_client_begin_fetch_all_progress_list() callback. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_all_user_progress(rc_client_all_user_progress_t* list); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +#ifdef RC_CLIENT_SUPPORTS_HASH +/** + * Start loading an unidentified game. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata); + +struct rc_hash_callbacks; +/** + * Provide callback functions for interacting with the file system and processing disc-based files when generating hashes. + */ +RC_EXPORT void rc_client_set_hash_callbacks(rc_client_t* client, const struct rc_hash_callbacks* callbacks); +#endif + +/** + * Start loading a game. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Gets the current progress of the asynchronous load game process. + */ +RC_EXPORT int RC_CCONV rc_client_get_load_game_state(const rc_client_t* client); +enum { + RC_CLIENT_LOAD_GAME_STATE_NONE, + RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN, + RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, /* [deprecated] - game data is now returned by identify call */ + RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, + RC_CLIENT_LOAD_GAME_STATE_DONE, + RC_CLIENT_LOAD_GAME_STATE_ABORTED +}; + +/** + * Determines if a game was successfully identified and loaded. + */ +RC_EXPORT int RC_CCONV rc_client_is_game_loaded(const rc_client_t* client); + +/** + * Unloads the current game. + */ +RC_EXPORT void RC_CCONV rc_client_unload_game(rc_client_t* client); + +typedef struct rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; + /* minimum version: 12.0 */ + const char* badge_url; +} rc_client_game_t; + +/** + * Get information about the current game. Returns NULL if no game is loaded. + * NOTE: returns a dummy game record if an unidentified game is loaded. + */ +RC_EXPORT const rc_client_game_t* RC_CCONV rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size); + +#ifdef RC_CLIENT_SUPPORTS_HASH +/** + * Changes the active disc in a multi-disc game. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); +#endif + +/** + * Changes the active disc in a multi-disc game. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); +/* this function was renamed in rcheevos 12.0 */ +#define rc_client_begin_change_media_from_hash rc_client_begin_change_media + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +typedef struct rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + + uint32_t num_achievements; + uint32_t num_leaderboards; + + /* minimum version: 12.0 */ + const char* badge_url; +} rc_client_subset_t; + +RC_EXPORT const rc_client_subset_t* RC_CCONV rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); + +RC_EXPORT void RC_CCONV rc_client_get_user_subset_summary(const rc_client_t* client, uint32_t subset_id, rc_client_user_game_summary_t* summary); + +typedef struct rc_client_subset_list_t { + const rc_client_subset_t** subsets; + uint32_t num_subsets; +} rc_client_subset_list_t; + +/** + * Creates a list of subsets for the currently loaded game. + * Returns an allocated list that must be free'd by calling rc_client_destroy_subset_list. + */ +RC_EXPORT rc_client_subset_list_t* RC_CCONV rc_client_create_subset_list(rc_client_t* client); + +/** + * Destroys a list allocated by rc_client_create_subset_list_list. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_subset_list(rc_client_subset_list_t* list); + +/*****************************************************************************\ +| Fetch Game Hashes | +\*****************************************************************************/ + +typedef struct rc_client_hash_library_entry_t { + char hash[33]; + uint32_t game_id; +} rc_client_hash_library_entry_t; + +typedef struct rc_client_hash_library_t { + rc_client_hash_library_entry_t* entries; + uint32_t num_entries; +} rc_client_hash_library_t; + +/** + * Callback that is fired when a hash library request completes. list may be null if the query failed. + */ +typedef void(RC_CCONV* rc_client_fetch_hash_library_callback_t)(int result, const char* error_message, + rc_client_hash_library_t* list, rc_client_t* client, + void* callback_userdata); + +/** + * Starts an asynchronous request for all hashes for the given console. + * This request returns a mapping from hashes to the game's unique identifier. A single game may have multiple + * hashes in the case of multi-disc games, or variants that are still compatible with the same achievement set. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_hash_library( + rc_client_t* client, uint32_t console_id, rc_client_fetch_hash_library_callback_t callback, void* callback_userdata); + +/** + * Destroys a previously-allocated result from the rc_client_destroy_hash_library() callback. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_hash_library(rc_client_hash_library_t* list); + +/*****************************************************************************\ +| Fetch Game Titles | +\*****************************************************************************/ + +typedef struct rc_client_game_title_entry_t { + uint32_t game_id; + const char* title; + char badge_name[16]; +} rc_client_game_title_entry_t; + +typedef struct rc_client_game_title_list_t { + rc_client_game_title_entry_t* entries; + uint32_t num_entries; +} rc_client_game_title_list_t; + +/** + * Callback that is fired when a game titles request completes. list may be null if the query failed. + */ +typedef void(RC_CCONV* rc_client_fetch_game_titles_callback_t)(int result, const char* error_message, + rc_client_game_title_list_t* list, rc_client_t* client, + void* callback_userdata); + +/** + * Starts an asynchronous request for titles and badge names for the specified games. + * The caller must provide an array of game IDs and the number of IDs in the array. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_game_titles( + rc_client_t* client, const uint32_t* game_ids, uint32_t num_game_ids, + rc_client_fetch_game_titles_callback_t callback, void* callback_userdata); + +/** + * Destroys a previously-allocated result from the rc_client_begin_fetch_game_titles() callback. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_game_title_list(rc_client_game_title_list_t* list); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE = 0, /* unprocessed */ + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE = 1, /* eligible to trigger */ + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED = 2, /* earned by user */ + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED = 3, /* not supported by this version of the runtime */ + NUM_RC_CLIENT_ACHIEVEMENT_STATES = 4 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE = 0, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL = (1 << 1), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE | RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL +}; + +enum { + RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD = 0, + RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE = 1, + RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION = 2, + RC_CLIENT_ACHIEVEMENT_TYPE_WIN = 3 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0, + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED = 8, + NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 9 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE = 0, + RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE = (1 << 1), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE | RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE +}; + +typedef struct rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; + /* minimum version: 11.1 */ + float rarity; + float rarity_hardcore; + uint8_t type; + /* minimum version: 12.0 */ + const char* badge_url; + const char* badge_locked_url; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +RC_EXPORT const rc_client_achievement_t* RC_CCONV rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size); + +typedef struct rc_client_achievement_bucket_t { + const rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_achievement_bucket_t; + +typedef struct rc_client_achievement_list_t { + const rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_achievement_list_t; + +enum { + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE = 0, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS = 1 +}; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +RC_EXPORT rc_client_achievement_list_t* RC_CCONV rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_create_achievement_list. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_achievement_list(rc_client_achievement_list_t* list); + +/** + * Returns non-zero if there are any achievements that can be queried through rc_client_create_achievement_list(). + */ +RC_EXPORT int RC_CCONV rc_client_has_achievements(rc_client_t* client); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_STATE_INACTIVE = 0, + RC_CLIENT_LEADERBOARD_STATE_ACTIVE = 1, + RC_CLIENT_LEADERBOARD_STATE_TRACKING = 2, + RC_CLIENT_LEADERBOARD_STATE_DISABLED = 3, + NUM_RC_CLIENT_LEADERBOARD_STATES = 4 +}; + +enum { + RC_CLIENT_LEADERBOARD_FORMAT_TIME = 0, + RC_CLIENT_LEADERBOARD_FORMAT_SCORE = 1, + RC_CLIENT_LEADERBOARD_FORMAT_VALUE = 2, + NUM_RC_CLIENT_LEADERBOARD_FORMATS = 3 +}; + +#define RC_CLIENT_LEADERBOARD_DISPLAY_SIZE 24 + +typedef struct rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t format; + uint8_t lower_is_better; +} rc_client_leaderboard_t; + +/** + * Get information about a leaderboard. Returns NULL if not found. + */ +RC_EXPORT const rc_client_leaderboard_t* RC_CCONV rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id); + +typedef struct rc_client_leaderboard_tracker_t { + char display[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + uint32_t id; +} rc_client_leaderboard_tracker_t; + +typedef struct rc_client_leaderboard_bucket_t { + const rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_leaderboard_bucket_t; + +typedef struct rc_client_leaderboard_list_t { + const rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_leaderboard_list_t; + +enum { + RC_CLIENT_LEADERBOARD_BUCKET_UNKNOWN = 0, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE = 1, + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4, + NUM_RC_CLIENT_LEADERBOARD_BUCKETS = 5 +}; + +enum { + RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE = 0, + RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING = 1 +}; + +/** + * Creates a list of leaderboards matching the specified grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +RC_EXPORT rc_client_leaderboard_list_t* RC_CCONV rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_create_leaderboard_list. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +/** + * Returns non-zero if the current game has any leaderboards. + */ +RC_EXPORT int RC_CCONV rc_client_has_leaderboards(rc_client_t* client); + +typedef struct rc_client_leaderboard_entry_t { + const char* user; + char display[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + time_t submitted; + uint32_t rank; + uint32_t index; +} rc_client_leaderboard_entry_t; + +typedef struct rc_client_leaderboard_entry_list_t { + rc_client_leaderboard_entry_t* entries; + uint32_t num_entries; + uint32_t total_entries; + int32_t user_index; +} rc_client_leaderboard_entry_list_t; + +typedef void (RC_CCONV *rc_client_fetch_leaderboard_entries_callback_t)(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server containing the logged-in user. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Gets the URL for the profile picture of the user associated to a leaderboard entry. + * Returns RC_OK on success. + */ +RC_EXPORT int RC_CCONV rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size); + +/** + * Destroys a list allocated by rc_client_begin_fetch_leaderboard_entries or rc_client_begin_fetch_leaderboard_entries_around_user. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list); + +/** + * Used for scoreboard events. Contains the response from the server when a leaderboard entry is submitted. + * NOTE: This structure is only valid within the event callback. If you want to make use of the data outside + * of the callback, you should create copies of both the top entries and usernames within. + */ +typedef struct rc_client_leaderboard_scoreboard_entry_t { + /* The user associated to the entry */ + const char* username; + /* The rank of the entry */ + uint32_t rank; + /* The value of the entry */ + char score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; +} rc_client_leaderboard_scoreboard_entry_t; + +typedef struct rc_client_leaderboard_scoreboard_t { + /* The ID of the leaderboard which was submitted */ + uint32_t leaderboard_id; + /* The value that was submitted */ + char submitted_score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + /* The player's best submitted value */ + char best_score[RC_CLIENT_LEADERBOARD_DISPLAY_SIZE]; + /* The player's new rank within the leaderboard */ + uint32_t new_rank; + /* The total number of entries in the leaderboard */ + uint32_t num_entries; + + /* An array of the top entries for the leaderboard */ + rc_client_leaderboard_scoreboard_entry_t* top_entries; + /* The number of items in the top_entries array */ + uint32_t num_top_entries; +} rc_client_leaderboard_scoreboard_t; + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +/** + * Returns non-zero if the current game supports rich presence. + */ +RC_EXPORT int RC_CCONV rc_client_has_rich_presence(rc_client_t* client); + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +RC_EXPORT size_t RC_CCONV rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Processing | +\*****************************************************************************/ + +enum { + RC_CLIENT_EVENT_TYPE_NONE = 0, + RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED = 1, /* [achievement] was earned by the player */ + RC_CLIENT_EVENT_LEADERBOARD_STARTED = 2, /* [leaderboard] attempt has started */ + RC_CLIENT_EVENT_LEADERBOARD_FAILED = 3, /* [leaderboard] attempt failed */ + RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED = 4, /* [leaderboard] attempt submitted */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW = 5, /* [achievement] challenge indicator should be shown */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE = 6, /* [achievement] challenge indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW = 7, /* progress indicator should be shown for [achievement] */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE = 8, /* progress indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE = 9, /* progress indicator should be updated to reflect new badge/progress for [achievement] */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 10, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 11, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD = 13, /* [leaderboard_scoreboard] possibly-new ranking received for [leaderboard] */ + RC_CLIENT_EVENT_RESET = 14, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 15, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 16, /* an API response returned a [server_error] and will not be retried */ + RC_CLIENT_EVENT_DISCONNECTED = 17, /* an unlock request could not be completed and is pending */ + RC_CLIENT_EVENT_RECONNECTED = 18, /* all pending unlocks have been completed */ + RC_CLIENT_EVENT_SUBSET_COMPLETED = 19 /* all achievements for the subset have been earned */ +}; + +typedef struct rc_client_server_error_t { + const char* error_message; + const char* api; + int result; + uint32_t related_id; +} rc_client_server_error_t; + +typedef struct rc_client_event_t { + uint32_t type; + + rc_client_achievement_t* achievement; + rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_tracker_t* leaderboard_tracker; + rc_client_leaderboard_scoreboard_t* leaderboard_scoreboard; + rc_client_server_error_t* server_error; + rc_client_subset_t* subset; + +} rc_client_event_t; + +/** + * Callback used to notify the client when certain events occur. + */ +typedef void (RC_CCONV *rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +RC_EXPORT void RC_CCONV rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +RC_EXPORT void RC_CCONV rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); + +/** + * Specifies whether rc_client is allowed to read memory outside of rc_client_do_frame/rc_client_idle. + */ +RC_EXPORT void RC_CCONV rc_client_set_allow_background_memory_reads(rc_client_t* client, int allowed); + +/** + * Determines if there are any active achievements/leaderboards/rich presence that need processing. + */ +RC_EXPORT int RC_CCONV rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +RC_EXPORT void RC_CCONV rc_client_do_frame(rc_client_t* client); + +/** + * Processes the periodic queue. + * Called internally by rc_client_do_frame. + * Should be explicitly called if rc_client_do_frame is not being called because emulation is paused. + */ +RC_EXPORT void RC_CCONV rc_client_idle(rc_client_t* client); + +/** + * Determines if a sufficient amount of frames have been processed since the last call to rc_client_can_pause. + * Should not be called unless the client is trying to pause. + * If false is returned, and frames_remaining is not NULL, frames_remaining will be set to the number of frames + * still required before pause is allowed, which can be converted to a time in seconds for displaying to the user. + */ +RC_EXPORT int RC_CCONV rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +RC_EXPORT void RC_CCONV rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +RC_EXPORT size_t RC_CCONV rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + * [deprecated] use rc_client_serialize_progress_sized instead + */ +RC_EXPORT int RC_CCONV rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +RC_EXPORT int RC_CCONV rc_client_serialize_progress_sized(rc_client_t* client, uint8_t* buffer, size_t buffer_size); + +/** + * Deserializes the runtime state from a buffer. + * Returns RC_OK on success, or an error indicator. + * [deprecated] use rc_client_deserialize_progress_sized instead + */ +RC_EXPORT int RC_CCONV rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +RC_EXPORT int RC_CCONV rc_client_deserialize_progress_sized(rc_client_t* client, const uint8_t* serialized, size_t serialized_size); + +RC_END_C_DECLS + +#endif /* RC_RUNTIME_H */ diff --git a/src/rcheevos/include/rc_client_raintegration.h b/src/rcheevos/include/rc_client_raintegration.h new file mode 100644 index 0000000000..847c8ad225 --- /dev/null +++ b/src/rcheevos/include/rc_client_raintegration.h @@ -0,0 +1,101 @@ +#ifndef RC_CLIENT_RAINTEGRATION_H +#define RC_CLIENT_RAINTEGRATION_H + +#ifndef _WIN32 + #undef RC_CLIENT_SUPPORTS_RAINTEGRATION /* Windows required for RAIntegration */ +#endif + +#include + +#include "rc_export.h" + +RC_BEGIN_C_DECLS + +typedef struct rc_client_t rc_client_t; /* forward reference; in rc_client.h */ + +/* types needed to implement raintegration */ + +typedef struct rc_client_raintegration_menu_item_t { + const char* label; + uint32_t id; + uint8_t checked; + uint8_t enabled; +} rc_client_raintegration_menu_item_t; + +typedef struct rc_client_raintegration_menu_t { + rc_client_raintegration_menu_item_t* items; + uint32_t num_items; +} rc_client_raintegration_menu_t; + +enum { + RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_NONE = 0, + RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_PUBLISHED = 1, + RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_LOCAL = 2, + RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_MODIFIED = 3, + RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_INSECURE = 4, +}; + +enum { + RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE = 0, + RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED = 1, /* [menu_item] checked changed */ + RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED = 2, /* hardcore was enabled or disabled */ + RC_CLIENT_RAINTEGRATION_EVENT_PAUSE = 3, /* emulated system should be paused */ + RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED = 4 /* one or more items were added/removed from the menu and it should be rebuilt */ +}; + +typedef struct rc_client_raintegration_event_t { + uint32_t type; + + const rc_client_raintegration_menu_item_t* menu_item; +} rc_client_raintegration_event_t; + +typedef void (RC_CCONV *rc_client_raintegration_event_handler_t)(const rc_client_raintegration_event_t* event, + rc_client_t* client); + +typedef void (RC_CCONV *rc_client_raintegration_write_memory_func_t)(uint32_t address, uint8_t* buffer, + uint32_t num_bytes, rc_client_t* client); + +typedef void (RC_CCONV* rc_client_raintegration_get_game_name_func_t)(char* buffer, uint32_t buffer_size, rc_client_t* client); + +/* types needed to integrate raintegration */ + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +#ifndef RC_CLIENT_SUPPORTS_EXTERNAL + #define RC_CLIENT_SUPPORTS_EXTERNAL /* external rc_client required for RAIntegration */ +#endif + +#include /* HWND */ + +#include "rc_client.h" + +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_raintegration(rc_client_t* client, + const wchar_t* search_directory, HWND main_window_handle, + const char* client_name, const char* client_version, + rc_client_callback_t callback, void* callback_userdata); + +RC_EXPORT void RC_CCONV rc_client_unload_raintegration(rc_client_t* client); + +RC_EXPORT void RC_CCONV rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle); + +RC_EXPORT const rc_client_raintegration_menu_t* RC_CCONV rc_client_raintegration_get_menu(const rc_client_t* client); + +RC_EXPORT void RC_CCONV rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu); +RC_EXPORT void RC_CCONV rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menu_item); +RC_EXPORT int RC_CCONV rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t menu_item_id); + +RC_EXPORT void RC_CCONV rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler); +RC_EXPORT void RC_CCONV rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler); +RC_EXPORT void RC_CCONV rc_client_raintegration_set_console_id(rc_client_t* client, uint32_t console_id); +RC_EXPORT int RC_CCONV rc_client_raintegration_has_modifications(const rc_client_t* client); + +RC_EXPORT void RC_CCONV rc_client_raintegration_set_event_handler(rc_client_t* client, + rc_client_raintegration_event_handler_t handler); + +RC_EXPORT int RC_CCONV rc_client_raintegration_get_achievement_state(const rc_client_t* client, uint32_t achievement_id); + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ + +RC_END_C_DECLS + +#endif /* RC_CLIENT_RAINTEGRATION_H */ diff --git a/src/rcheevos/include/rc_consoles.h b/src/rcheevos/include/rc_consoles.h new file mode 100644 index 0000000000..5f788855b1 --- /dev/null +++ b/src/rcheevos/include/rc_consoles.h @@ -0,0 +1,138 @@ +#ifndef RC_CONSOLES_H +#define RC_CONSOLES_H + +#include "rc_export.h" + +#include + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Console identifiers | +\*****************************************************************************/ + +enum { + RC_CONSOLE_UNKNOWN = 0, + RC_CONSOLE_MEGA_DRIVE = 1, + RC_CONSOLE_NINTENDO_64 = 2, + RC_CONSOLE_SUPER_NINTENDO = 3, + RC_CONSOLE_GAMEBOY = 4, + RC_CONSOLE_GAMEBOY_ADVANCE = 5, + RC_CONSOLE_GAMEBOY_COLOR = 6, + RC_CONSOLE_NINTENDO = 7, + RC_CONSOLE_PC_ENGINE = 8, + RC_CONSOLE_SEGA_CD = 9, + RC_CONSOLE_SEGA_32X = 10, + RC_CONSOLE_MASTER_SYSTEM = 11, + RC_CONSOLE_PLAYSTATION = 12, + RC_CONSOLE_ATARI_LYNX = 13, + RC_CONSOLE_NEOGEO_POCKET = 14, + RC_CONSOLE_GAME_GEAR = 15, + RC_CONSOLE_GAMECUBE = 16, + RC_CONSOLE_ATARI_JAGUAR = 17, + RC_CONSOLE_NINTENDO_DS = 18, + RC_CONSOLE_WII = 19, + RC_CONSOLE_WII_U = 20, + RC_CONSOLE_PLAYSTATION_2 = 21, + RC_CONSOLE_XBOX = 22, + RC_CONSOLE_MAGNAVOX_ODYSSEY2 = 23, + RC_CONSOLE_POKEMON_MINI = 24, + RC_CONSOLE_ATARI_2600 = 25, + RC_CONSOLE_MS_DOS = 26, + RC_CONSOLE_ARCADE = 27, + RC_CONSOLE_VIRTUAL_BOY = 28, + RC_CONSOLE_MSX = 29, + RC_CONSOLE_COMMODORE_64 = 30, + RC_CONSOLE_ZX81 = 31, + RC_CONSOLE_ORIC = 32, + RC_CONSOLE_SG1000 = 33, + RC_CONSOLE_VIC20 = 34, + RC_CONSOLE_AMIGA = 35, + RC_CONSOLE_ATARI_ST = 36, + RC_CONSOLE_AMSTRAD_PC = 37, + RC_CONSOLE_APPLE_II = 38, + RC_CONSOLE_SATURN = 39, + RC_CONSOLE_DREAMCAST = 40, + RC_CONSOLE_PSP = 41, + RC_CONSOLE_CDI = 42, + RC_CONSOLE_3DO = 43, + RC_CONSOLE_COLECOVISION = 44, + RC_CONSOLE_INTELLIVISION = 45, + RC_CONSOLE_VECTREX = 46, + RC_CONSOLE_PC8800 = 47, + RC_CONSOLE_PC9800 = 48, + RC_CONSOLE_PCFX = 49, + RC_CONSOLE_ATARI_5200 = 50, + RC_CONSOLE_ATARI_7800 = 51, + RC_CONSOLE_X68K = 52, + RC_CONSOLE_WONDERSWAN = 53, + RC_CONSOLE_CASSETTEVISION = 54, + RC_CONSOLE_SUPER_CASSETTEVISION = 55, + RC_CONSOLE_NEO_GEO_CD = 56, + RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57, + RC_CONSOLE_FM_TOWNS = 58, + RC_CONSOLE_ZX_SPECTRUM = 59, + RC_CONSOLE_GAME_AND_WATCH = 60, + RC_CONSOLE_NOKIA_NGAGE = 61, + RC_CONSOLE_NINTENDO_3DS = 62, + RC_CONSOLE_SUPERVISION = 63, + RC_CONSOLE_SHARPX1 = 64, + RC_CONSOLE_TIC80 = 65, + RC_CONSOLE_THOMSONTO8 = 66, + RC_CONSOLE_PC6000 = 67, + RC_CONSOLE_PICO = 68, + RC_CONSOLE_MEGADUCK = 69, + RC_CONSOLE_ZEEBO = 70, + RC_CONSOLE_ARDUBOY = 71, + RC_CONSOLE_WASM4 = 72, + RC_CONSOLE_ARCADIA_2001 = 73, + RC_CONSOLE_INTERTON_VC_4000 = 74, + RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER = 75, + RC_CONSOLE_PC_ENGINE_CD = 76, + RC_CONSOLE_ATARI_JAGUAR_CD = 77, + RC_CONSOLE_NINTENDO_DSI = 78, + RC_CONSOLE_TI83 = 79, + RC_CONSOLE_UZEBOX = 80, + RC_CONSOLE_FAMICOM_DISK_SYSTEM = 81, + + RC_CONSOLE_HUBS = 100, + RC_CONSOLE_EVENTS = 101, + RC_CONSOLE_STANDALONE = 102 +}; + +RC_EXPORT const char* RC_CCONV rc_console_name(uint32_t console_id); + +/*****************************************************************************\ +| Memory mapping | +\*****************************************************************************/ + +enum { + RC_MEMORY_TYPE_SYSTEM_RAM, /* normal system memory */ + RC_MEMORY_TYPE_SAVE_RAM, /* memory that persists between sessions */ + RC_MEMORY_TYPE_VIDEO_RAM, /* memory reserved for graphical processing */ + RC_MEMORY_TYPE_READONLY, /* memory that maps to read only data */ + RC_MEMORY_TYPE_HARDWARE_CONTROLLER, /* memory for interacting with system components */ + RC_MEMORY_TYPE_VIRTUAL_RAM, /* secondary address space that maps to real memory in system RAM */ + RC_MEMORY_TYPE_UNUSED /* these addresses don't really exist */ +}; + +typedef struct rc_memory_region_t { + uint32_t start_address; /* first address of block as queried by RetroAchievements */ + uint32_t end_address; /* last address of block as queried by RetroAchievements */ + uint32_t real_address; /* real address for first address of block */ + uint8_t type; /* RC_MEMORY_TYPE_ for block */ + const char* description; /* short description of block */ +} +rc_memory_region_t; + +typedef struct rc_memory_regions_t { + const rc_memory_region_t* region; + uint32_t num_regions; +} +rc_memory_regions_t; + +RC_EXPORT const rc_memory_regions_t* RC_CCONV rc_console_memory_regions(uint32_t console_id); + +RC_END_C_DECLS + +#endif /* RC_CONSOLES_H */ diff --git a/src/rcheevos/include/rc_error.h b/src/rcheevos/include/rc_error.h new file mode 100644 index 0000000000..5d253442da --- /dev/null +++ b/src/rcheevos/include/rc_error.h @@ -0,0 +1,59 @@ +#ifndef RC_ERROR_H +#define RC_ERROR_H + +#include "rc_export.h" + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Return values | +\*****************************************************************************/ + +enum { + RC_OK = 0, + RC_INVALID_FUNC_OPERAND = -1, + RC_INVALID_MEMORY_OPERAND = -2, + RC_INVALID_CONST_OPERAND = -3, + RC_INVALID_FP_OPERAND = -4, + RC_INVALID_CONDITION_TYPE = -5, + RC_INVALID_OPERATOR = -6, + RC_INVALID_REQUIRED_HITS = -7, + RC_DUPLICATED_START = -8, + RC_DUPLICATED_CANCEL = -9, + RC_DUPLICATED_SUBMIT = -10, + RC_DUPLICATED_VALUE = -11, + RC_DUPLICATED_PROGRESS = -12, + RC_MISSING_START = -13, + RC_MISSING_CANCEL = -14, + RC_MISSING_SUBMIT = -15, + RC_MISSING_VALUE = -16, + RC_INVALID_LBOARD_FIELD = -17, + RC_MISSING_DISPLAY_STRING = -18, + RC_OUT_OF_MEMORY = -19, + RC_INVALID_VALUE_FLAG = -20, + RC_MISSING_VALUE_MEASURED = -21, + RC_MULTIPLE_MEASURED = -22, + RC_INVALID_MEASURED_TARGET = -23, + RC_INVALID_COMPARISON = -24, + RC_INVALID_STATE = -25, + RC_INVALID_JSON = -26, + RC_API_FAILURE = -27, + RC_LOGIN_REQUIRED = -28, + RC_NO_GAME_LOADED = -29, + RC_HARDCORE_DISABLED = -30, + RC_ABORTED = -31, + RC_NO_RESPONSE = -32, + RC_ACCESS_DENIED = -33, + RC_INVALID_CREDENTIALS = -34, + RC_EXPIRED_TOKEN = -35, + RC_INSUFFICIENT_BUFFER = -36, + RC_INVALID_VARIABLE_NAME = -37, + RC_UNKNOWN_VARIABLE_NAME = -38, + RC_NOT_FOUND = -39 +}; + +RC_EXPORT const char* RC_CCONV rc_error_str(int ret); + +RC_END_C_DECLS + +#endif /* RC_ERROR_H */ diff --git a/src/rcheevos/include/rc_export.h b/src/rcheevos/include/rc_export.h new file mode 100644 index 0000000000..9471acd480 --- /dev/null +++ b/src/rcheevos/include/rc_export.h @@ -0,0 +1,100 @@ +#ifndef RC_EXPORT_H +#define RC_EXPORT_H + +/* These macros control how callbacks and public functions are defined */ + +/* RC_SHARED should be defined when building rcheevos as a shared library (e.g. dll/dylib/so). External code should not define this macro. */ +/* RC_STATIC should be defined when building rcheevos as a static library. External code should also define this macro. */ +/* RC_IMPORT should be defined for external code using rcheevos as a shared library. */ + +/* For compatibility, if none of these three macros are defined, then the build is assumed to be RC_STATIC */ + +#if !defined(RC_SHARED) && !defined(RC_STATIC) && !defined(RC_IMPORT) + #define RC_STATIC +#endif + +#if (defined(RC_SHARED) && defined(RC_STATIC)) || (defined(RC_SHARED) && defined(RC_IMPORT)) || (defined(RC_STATIC) && defined(RC_IMPORT)) + #error RC_SHARED, RC_STATIC, and RC_IMPORT are mutually exclusive +#endif + +/* RC_BEGIN_C_DECLS and RC_END_C_DECLS should be used for all headers, to enforce C linkage and the C calling convention */ +/* RC_BEGIN_C_DECLS should be placed after #include's and before header declarations */ +/* RC_END_C_DECLS should be placed after header declarations */ + +/* example usage + * + * #ifndef RC_HEADER_H + * #define RC_HEADER_H + * + * #include + * + * RC_BEGIN_C_DECLS + * + * uint8_t rc_function(void); + * + * RC_END_C_DECLS + * + * #endif + */ + +#if defined(__cplusplus) && !defined(CXX_BUILD) + #define RC_BEGIN_C_DECLS extern "C" { + #define RC_END_C_DECLS } +#else + #define RC_BEGIN_C_DECLS + #define RC_END_C_DECLS +#endif + +/* RC_CCONV should be used for public functions and callbacks, to enforce the cdecl calling convention, if applicable */ +/* RC_CCONV should be placed after the return type, and between the ( and * for callbacks */ + +/* example usage */ +/* void RC_CCONV rc_function(void) */ +/* void (RC_CCONV *rc_callback)(void) */ + +#if defined(_WIN32) + /* Windows compilers will ignore __cdecl when not applicable */ + #define RC_CCONV __cdecl +#elif defined(__GNUC__) && defined(__i386__) + /* GNU C compilers will warn if cdecl is defined on an unsupported platform */ + #define RC_CCONV __attribute__((cdecl)) +#else + #define RC_CCONV +#endif + +/* RC_EXPORT should be used for public functions */ +/* RC_EXPORT will provide necessary hints for shared library usage, if applicable */ +/* RC_EXPORT should be placed before the return type */ + +/* example usage */ +/* RC_EXPORT void rc_function(void) */ + +#ifdef RC_SHARED + #if defined(_WIN32) + #define RC_EXPORT __declspec(dllexport) + #elif defined(__GNUC__) && __GNUC__ >= 4 + #define RC_EXPORT __attribute__((visibility("default"))) + #else + #define RC_EXPORT + #endif +#endif + +#ifdef RC_IMPORT + #if defined(_WIN32) + #define RC_EXPORT __declspec(dllimport) + #elif defined(__GNUC__) && __GNUC__ >= 4 + #define RC_EXPORT __attribute__((visibility("default"))) + #else + #define RC_EXPORT + #endif +#endif + +#ifdef RC_STATIC + #if defined(__GNUC__) && __GNUC__ >= 4 + #define RC_EXPORT __attribute__((visibility("default"))) + #else + #define RC_EXPORT + #endif +#endif + +#endif /* RC_EXPORT_H */ diff --git a/src/rcheevos/include/rc_hash.h b/src/rcheevos/include/rc_hash.h new file mode 100644 index 0000000000..be1d378123 --- /dev/null +++ b/src/rcheevos/include/rc_hash.h @@ -0,0 +1,200 @@ +#ifndef RC_HASH_H +#define RC_HASH_H + +#include +#include +#include + +#include "rc_consoles.h" + +RC_BEGIN_C_DECLS + + struct rc_hash_iterator; + + /* ===================================================== */ + + typedef void (RC_CCONV *rc_hash_message_callback_deprecated)(const char*); + + /* specifies a function to call when an error occurs to display the error message */ + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_error_message_callback(rc_hash_message_callback_deprecated callback); + + /* specifies a function to call for verbose logging */ + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void rc_hash_init_verbose_message_callback(rc_hash_message_callback_deprecated callback); + + /* ===================================================== */ + + /* opens a file */ + typedef void* (RC_CCONV *rc_hash_filereader_open_file_handler)(const char* path_utf8); + + /* moves the file pointer - standard fseek parameters */ + typedef void (RC_CCONV *rc_hash_filereader_seek_handler)(void* file_handle, int64_t offset, int origin); + + /* locates the file pointer */ + typedef int64_t (RC_CCONV *rc_hash_filereader_tell_handler)(void* file_handle); + + /* reads the specified number of bytes from the file starting at the read pointer. + * returns the number of bytes actually read. + */ + typedef size_t (RC_CCONV *rc_hash_filereader_read_handler)(void* file_handle, void* buffer, size_t requested_bytes); + + /* closes the file */ + typedef void (RC_CCONV *rc_hash_filereader_close_file_handler)(void* file_handle); + + typedef struct rc_hash_filereader + { + rc_hash_filereader_open_file_handler open; + rc_hash_filereader_seek_handler seek; + rc_hash_filereader_tell_handler tell; + rc_hash_filereader_read_handler read; + rc_hash_filereader_close_file_handler close; + } rc_hash_filereader_t; + + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_custom_filereader(struct rc_hash_filereader* reader); + + /* ===================================================== */ + +#ifndef RC_HASH_NO_DISC + + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ + #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) /* the first data/audio track of the second session */ + + /* opens a track from the specified file. see the RC_HASH_CDTRACK_ defines for special tracks. + * returns a handle to be passed to the other functions, or NULL if the track could not be opened. + */ + typedef void* (RC_CCONV *rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); + typedef void* (RC_CCONV* rc_hash_cdreader_open_track_iterator_handler)(const char* path, uint32_t track, const struct rc_hash_iterator* iterator); + + /* attempts to read the specified number of bytes from the file starting at the specified absolute sector. + * returns the number of bytes actually read. + */ + typedef size_t (RC_CCONV *rc_hash_cdreader_read_sector_handler)(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes); + + /* closes the track handle */ + typedef void (RC_CCONV *rc_hash_cdreader_close_track_handler)(void* track_handle); + + /* gets the absolute sector index for the first sector of a track */ + typedef uint32_t(RC_CCONV *rc_hash_cdreader_first_track_sector_handler)(void* track_handle); + + typedef struct rc_hash_cdreader + { + rc_hash_cdreader_open_track_handler open_track; + rc_hash_cdreader_read_sector_handler read_sector; + rc_hash_cdreader_close_track_handler close_track; + rc_hash_cdreader_first_track_sector_handler first_track_sector; + rc_hash_cdreader_open_track_iterator_handler open_track_iterator; + } rc_hash_cdreader_t; + + RC_EXPORT void RC_CCONV rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader); + /* [deprecated] don't set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_default_cdreader(void); + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader); + +#endif /* RC_HASH_NO_DISC */ + +#ifndef RC_HASH_NO_ENCRYPTED + + /* specifies a function called to obtain a 3DS CIA decryption normal key. + * this key would be derived from slot0x3DKeyX and the common key specified by the passed index. + * the normal key should be written in big endian format + * returns non-zero on success, or zero on failure. + */ + typedef int (RC_CCONV *rc_hash_3ds_get_cia_normal_key_func)(uint8_t common_key_index, uint8_t out_normal_key[16]); + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_3ds_get_cia_normal_key_func(rc_hash_3ds_get_cia_normal_key_func func); + + /* specifies a function called to obtain 3DS NCCH decryption normal keys. + * the primary key will always use slot0x2CKeyX and the passed primary KeyY. + * the secondary key will use the KeyX slot passed + * the secondary KeyY will be identical to the primary keyY if the passed program id is NULL + * if the program id is not null, then the secondary KeyY will be obtained with "seed crypto" + * with "seed crypto" the 8 byte program id can be used to obtain a 16 byte "seed" within the seeddb.bin firmware file + * the primary KeyY then the seed will then be hashed with SHA256, and the upper 16 bytes of the digest will be the secondary KeyY used + * the normal keys should be written in big endian format + * returns non-zero on success, or zero on failure. + */ + typedef int (RC_CCONV *rc_hash_3ds_get_ncch_normal_keys_func)(uint8_t primary_key_y[16], uint8_t secondary_key_x_slot, uint8_t* optional_program_id, + uint8_t out_primary_key[16], uint8_t out_secondary_key[16]); + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func); + +#endif /* RC_HASH_NO_ENCRYPTED */ + +/* ===================================================== */ + +typedef void (RC_CCONV* rc_hash_message_callback_func)(const char*, const struct rc_hash_iterator* iterator); + +typedef struct rc_hash_callbacks { + rc_hash_message_callback_func verbose_message; + rc_hash_message_callback_func error_message; + + rc_hash_filereader_t filereader; +#ifndef RC_HASH_NO_DISC + rc_hash_cdreader_t cdreader; +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + struct rc_hash_encryption_callbacks { + rc_hash_3ds_get_cia_normal_key_func get_3ds_cia_normal_key; + rc_hash_3ds_get_ncch_normal_keys_func get_3ds_ncch_normal_keys; + } encryption; +#endif +} rc_hash_callbacks_t; + +/* data for rc_hash_iterate + */ +typedef struct rc_hash_iterator { + const uint8_t* buffer; + size_t buffer_size; + uint8_t consoles[12]; + int index; + const char* path; + void* userdata; + + rc_hash_callbacks_t callbacks; +} rc_hash_iterator_t; + +/* initializes a rc_hash_iterator + * - path must be provided + * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) + */ +RC_EXPORT void RC_CCONV rc_hash_initialize_iterator(rc_hash_iterator_t* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); + +/* releases resources associated to a rc_hash_iterator + */ +RC_EXPORT void RC_CCONV rc_hash_destroy_iterator(rc_hash_iterator_t* iterator); + +/* generates the next hash for the data in the rc_hash_iterator. + * returns non-zero if a hash was generated, or zero if no more hashes can be generated for the data. + */ +RC_EXPORT int RC_CCONV rc_hash_iterate(char hash[33], rc_hash_iterator_t* iterator); + +/* generates a hash for the data in the rc_hash_iterator. + * returns non-zero if a hash was generated. + */ +RC_EXPORT int RC_CCONV rc_hash_generate(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); + +/* ===================================================== */ + +/* generates a hash from a block of memory. + * returns non-zero on success, or zero on failure. + */ +/* [deprecated] use rc_hash_generate instead */ +RC_EXPORT int RC_CCONV rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size); + +/* generates a hash from a file. + * returns non-zero on success, or zero on failure. + */ +/* [deprecated] use rc_hash_generate instead */ +RC_EXPORT int RC_CCONV rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path); + +/* ===================================================== */ + +RC_END_C_DECLS + +#endif /* RC_HASH_H */ diff --git a/src/rcheevos/include/rc_runtime.h b/src/rcheevos/include/rc_runtime.h new file mode 100644 index 0000000000..88b12dea27 --- /dev/null +++ b/src/rcheevos/include/rc_runtime.h @@ -0,0 +1,148 @@ +#ifndef RC_RUNTIME_H +#define RC_RUNTIME_H + +#include "rc_error.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Forward Declarations (defined in rc_runtime_types.h) | +\*****************************************************************************/ + +#ifndef RC_RUNTIME_TYPES_H /* prevents pedantic redefinition error */ + +typedef struct rc_trigger_t rc_trigger_t; +typedef struct rc_lboard_t rc_lboard_t; +typedef struct rc_richpresence_t rc_richpresence_t; +typedef struct rc_memref_t rc_memref_t; +typedef struct rc_value_t rc_value_t; + +#endif + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address. If + * num_bytes is greater than 1, the value is read in little-endian from + * memory. + */ +typedef uint32_t(RC_CCONV *rc_runtime_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +typedef struct rc_runtime_trigger_t { + uint32_t id; + rc_trigger_t* trigger; + void* buffer; + rc_memref_t* invalid_memref; + uint8_t md5[16]; + int32_t serialized_size; +} +rc_runtime_trigger_t; + +typedef struct rc_runtime_lboard_t { + uint32_t id; + int32_t value; + rc_lboard_t* lboard; + void* buffer; + rc_memref_t* invalid_memref; + uint8_t md5[16]; + uint32_t serialized_size; +} +rc_runtime_lboard_t; + +typedef struct rc_runtime_richpresence_t { + rc_richpresence_t* richpresence; + void* buffer; + uint8_t md5[16]; +} +rc_runtime_richpresence_t; + +typedef struct rc_runtime_t { + rc_runtime_trigger_t* triggers; + uint32_t trigger_count; + uint32_t trigger_capacity; + + rc_runtime_lboard_t* lboards; + uint32_t lboard_count; + uint32_t lboard_capacity; + + rc_runtime_richpresence_t* richpresence; + + struct rc_memrefs_t* memrefs; + + uint8_t owns_self; +} +rc_runtime_t; + +RC_EXPORT rc_runtime_t* RC_CCONV rc_runtime_alloc(void); +RC_EXPORT void RC_CCONV rc_runtime_init(rc_runtime_t* runtime); +RC_EXPORT void RC_CCONV rc_runtime_destroy(rc_runtime_t* runtime); + +RC_EXPORT int RC_CCONV rc_runtime_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT void RC_CCONV rc_runtime_deactivate_achievement(rc_runtime_t* runtime, uint32_t id); +RC_EXPORT rc_trigger_t* RC_CCONV rc_runtime_get_achievement(const rc_runtime_t* runtime, uint32_t id); +RC_EXPORT int RC_CCONV rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target); +RC_EXPORT int RC_CCONV rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char *buffer, size_t buffer_size); + +RC_EXPORT int RC_CCONV rc_runtime_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT void RC_CCONV rc_runtime_deactivate_lboard(rc_runtime_t* runtime, uint32_t id); +RC_EXPORT rc_lboard_t* RC_CCONV rc_runtime_get_lboard(const rc_runtime_t* runtime, uint32_t id); +RC_EXPORT int RC_CCONV rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format); + + +RC_EXPORT int RC_CCONV rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, void* unused_L); + +enum { + RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, /* from WAITING, PAUSED, or PRIMED to ACTIVE */ + RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, + RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, + RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, + RC_RUNTIME_EVENT_LBOARD_STARTED, + RC_RUNTIME_EVENT_LBOARD_CANCELED, + RC_RUNTIME_EVENT_LBOARD_UPDATED, + RC_RUNTIME_EVENT_LBOARD_TRIGGERED, + RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, + RC_RUNTIME_EVENT_LBOARD_DISABLED, + RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED +}; + +typedef struct rc_runtime_event_t { + uint32_t id; + int32_t value; + uint8_t type; +} +rc_runtime_event_t; + +typedef void (RC_CCONV *rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); + +RC_EXPORT void RC_CCONV rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, void* unused_L); +RC_EXPORT void RC_CCONV rc_runtime_reset(rc_runtime_t* runtime); + +typedef int (RC_CCONV *rc_runtime_validate_address_t)(uint32_t address); +RC_EXPORT void RC_CCONV rc_runtime_validate_addresses(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); +RC_EXPORT void RC_CCONV rc_runtime_invalidate_address(rc_runtime_t* runtime, uint32_t address); + +RC_EXPORT uint32_t RC_CCONV rc_runtime_progress_size(const rc_runtime_t* runtime, void* unused_L); + +/* [deprecated] use rc_runtime_serialize_progress_sized instead */ +RC_EXPORT int RC_CCONV rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, void* unused_L); +RC_EXPORT int RC_CCONV rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, void* unused_L); + +/* [deprecated] use rc_runtime_deserialize_progress_sized instead */ +RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, void* unused_L); +RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* serialized, uint32_t serialized_size, void* unused_L); + +RC_END_C_DECLS + +#endif /* RC_RUNTIME_H */ diff --git a/src/rcheevos/include/rc_runtime_types.h b/src/rcheevos/include/rc_runtime_types.h new file mode 100644 index 0000000000..e17797063c --- /dev/null +++ b/src/rcheevos/include/rc_runtime_types.h @@ -0,0 +1,452 @@ +#ifndef RC_RUNTIME_TYPES_H +#define RC_RUNTIME_TYPES_H + +#include "rc_error.h" + +#include +#include + +RC_BEGIN_C_DECLS + +#ifndef RC_RUNTIME_H /* prevents pedantic redefiniton error */ + +typedef struct rc_trigger_t rc_trigger_t; +typedef struct rc_lboard_t rc_lboard_t; +typedef struct rc_richpresence_t rc_richpresence_t; +typedef struct rc_memref_t rc_memref_t; +typedef struct rc_value_t rc_value_t; + +#endif + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address. If + * num_bytes is greater than 1, the value is read in little-endian from + * memory. + */ +typedef uint32_t(RC_CCONV* rc_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); + +/*****************************************************************************\ +| Memory References | +\*****************************************************************************/ + +/* Sizes. */ +enum { + RC_MEMSIZE_8_BITS, + RC_MEMSIZE_16_BITS, + RC_MEMSIZE_24_BITS, + RC_MEMSIZE_32_BITS, + RC_MEMSIZE_LOW, + RC_MEMSIZE_HIGH, + RC_MEMSIZE_BIT_0, + RC_MEMSIZE_BIT_1, + RC_MEMSIZE_BIT_2, + RC_MEMSIZE_BIT_3, + RC_MEMSIZE_BIT_4, + RC_MEMSIZE_BIT_5, + RC_MEMSIZE_BIT_6, + RC_MEMSIZE_BIT_7, + RC_MEMSIZE_BITCOUNT, + RC_MEMSIZE_16_BITS_BE, + RC_MEMSIZE_24_BITS_BE, + RC_MEMSIZE_32_BITS_BE, + RC_MEMSIZE_FLOAT, + RC_MEMSIZE_MBF32, + RC_MEMSIZE_MBF32_LE, + RC_MEMSIZE_FLOAT_BE, + RC_MEMSIZE_DOUBLE32, + RC_MEMSIZE_DOUBLE32_BE, + RC_MEMSIZE_VARIABLE +}; + +typedef struct rc_memref_value_t { + /* The current value of this memory reference. */ + uint32_t value; + /* The last differing value of this memory reference. */ + uint32_t prior; + + /* The size of the value. (RC_MEMSIZE_*) */ + uint8_t size; + /* True if the value changed this frame. */ + uint8_t changed; + /* The value type of the value. (RC_VALUE_TYPE_*) */ + uint8_t type; + /* The type of memref (RC_MEMREF_TYPE_*) */ + uint8_t memref_type; +} +rc_memref_value_t; + +struct rc_memref_t { + /* The current value at the specified memory address. */ + rc_memref_value_t value; + + /* The memory address of this variable. */ + uint32_t address; +}; + +/*****************************************************************************\ +| Operands | +\*****************************************************************************/ + +/* types */ +enum { + RC_OPERAND_ADDRESS, /* The value of a live address in RAM. */ + RC_OPERAND_DELTA, /* The value last known at this address. */ + RC_OPERAND_CONST, /* A 32-bit unsigned integer. */ + RC_OPERAND_FP, /* A floating point value. */ + RC_OPERAND_FUNC, /* A function that provides the value. */ + RC_OPERAND_PRIOR, /* The last differing value at this address. */ + RC_OPERAND_BCD, /* The BCD-decoded value of a live address in RAM. */ + RC_OPERAND_INVERTED, /* The twos-complement value of a live address in RAM. */ + RC_OPERAND_RECALL /* The value captured by the last RC_CONDITION_REMEMBER condition */ +}; + +typedef struct rc_operand_t { + union { + /* A value read from memory. */ + rc_memref_t* memref; + + /* An integer value. */ + uint32_t num; + + /* A floating point value. */ + double dbl; + } value; + + /* specifies which member of the value union is being used (RC_OPERAND_*) */ + uint8_t type; + + /* the RC_MEMSIZE of the operand specified in the condition definition - memref.size may differ */ + uint8_t size; + + /* specifies how to read the memref for some types (RC_OPERAND_*) */ + uint8_t memref_access_type; + + /* if set, this operand is combining the current condition with the previous one */ + uint8_t is_combining; +} +rc_operand_t; + +RC_EXPORT int RC_CCONV rc_operand_is_memref(const rc_operand_t* operand); + +/*****************************************************************************\ +| Conditions | +\*****************************************************************************/ + +/* types */ +enum { + RC_CONDITION_STANDARD, /* this should always be 0 */ + RC_CONDITION_PAUSE_IF, + RC_CONDITION_RESET_IF, + RC_CONDITION_MEASURED_IF, + RC_CONDITION_TRIGGER, + RC_CONDITION_MEASURED, + RC_CONDITION_ADD_SOURCE, + RC_CONDITION_SUB_SOURCE, + RC_CONDITION_ADD_ADDRESS, + RC_CONDITION_REMEMBER, + RC_CONDITION_ADD_HITS, + RC_CONDITION_SUB_HITS, + RC_CONDITION_RESET_NEXT_IF, + RC_CONDITION_AND_NEXT, + RC_CONDITION_OR_NEXT +}; + +/* operators */ +enum { + RC_OPERATOR_EQ, + RC_OPERATOR_LT, + RC_OPERATOR_LE, + RC_OPERATOR_GT, + RC_OPERATOR_GE, + RC_OPERATOR_NE, + RC_OPERATOR_NONE, + RC_OPERATOR_MULT, + RC_OPERATOR_DIV, + RC_OPERATOR_AND, + RC_OPERATOR_XOR, + RC_OPERATOR_MOD, + RC_OPERATOR_ADD, + RC_OPERATOR_SUB, + + RC_OPERATOR_SUB_PARENT, /* internal use */ + RC_OPERATOR_ADD_ACCUMULATOR, /* internal use */ + RC_OPERATOR_SUB_ACCUMULATOR, /* internal use */ + RC_OPERATOR_INDIRECT_READ /* internal use */ +}; + +typedef struct rc_condition_t rc_condition_t; + +struct rc_condition_t { + /* The condition's operands. */ + rc_operand_t operand1; + rc_operand_t operand2; + + /* Required hits to fire this condition. */ + uint32_t required_hits; + /* Number of hits so far. */ + uint32_t current_hits; + + /* The next condition in the chain. */ + rc_condition_t* next; + + /* The type of the condition. (RC_CONDITION_*) */ + uint8_t type; + + /* The comparison operator to use. (RC_OPERATOR_*) */ + uint8_t oper; /* operator is a reserved word in C++. */ + + /* Will be non-zero if the condition evaluated true on the last check. + * - The lowest bit indicates whether the condition itself was true. + * - The second lowest bit will only ever be set on ResetIf conditions. + * If set, it indicates that the condition was responsible for resetting the + * trigger. A reset clears all hit counts, so the condition may not appear to + * be true just from looking at it (in which case the lower bit will be 0). + * Also, the condition might have only met its required_hits target though + * an AddHits chain which will have also been reset. + */ + uint8_t is_true; + + /* Unique identifier of optimized comparator to use. (RC_PROCESSING_COMPARE_*) */ + uint8_t optimized_comparator; +}; + +/*****************************************************************************\ +| Condition sets | +\*****************************************************************************/ + +typedef struct rc_condset_t rc_condset_t; + +struct rc_condset_t { + /* The next condition set in the chain. */ + rc_condset_t* next; + + /* The first condition in this condition set. Then follow ->next chain. */ + rc_condition_t* conditions; + + /* The number of pause conditions in this condition set. */ + /* The first pause condition is at "this + RC_ALIGN(sizeof(this)). */ + uint16_t num_pause_conditions; + + /* The number of reset conditions in this condition set. */ + uint16_t num_reset_conditions; + + /* The number of hittarget conditions in this condition set. */ + uint16_t num_hittarget_conditions; + + /* The number of non-hittarget measured conditions in this condition set. */ + uint16_t num_measured_conditions; + + /* The number of other conditions in this condition set. */ + uint16_t num_other_conditions; + + /* The number of indirect conditions in this condition set. */ + uint16_t num_indirect_conditions; + + /* True if any condition in the set is a pause condition. */ + uint8_t has_pause; /* DEPRECATED - just check num_pause_conditions != 0 */ + /* True if the set is currently paused. */ + uint8_t is_paused; +}; + +/*****************************************************************************\ +| Trigger | +\*****************************************************************************/ + +enum { + RC_TRIGGER_STATE_INACTIVE, /* achievement is not being processed */ + RC_TRIGGER_STATE_WAITING, /* achievement cannot trigger until it has been false for at least one frame */ + RC_TRIGGER_STATE_ACTIVE, /* achievement is active and may trigger */ + RC_TRIGGER_STATE_PAUSED, /* achievement is currently paused and will not trigger */ + RC_TRIGGER_STATE_RESET, /* achievement hit counts were reset */ + RC_TRIGGER_STATE_TRIGGERED, /* achievement has triggered */ + RC_TRIGGER_STATE_PRIMED, /* all non-Trigger conditions are true */ + RC_TRIGGER_STATE_DISABLED /* achievement cannot be processed at this time */ +}; + +struct rc_trigger_t { + /* The main condition set. */ + rc_condset_t* requirement; + + /* The list of sub condition sets in this test. */ + rc_condset_t* alternative; + + /* The current state of the MEASURED condition. */ + uint32_t measured_value; + + /* The target state of the MEASURED condition */ + uint32_t measured_target; + + /* The current state of the trigger */ + uint8_t state; + + /* True if at least one condition has a non-zero hit count */ + uint8_t has_hits; + + /* True if the measured value should be displayed as a percentage */ + uint8_t measured_as_percent; + + /* True if the trigger has its own rc_memrefs_t */ + uint8_t has_memrefs; +}; + +RC_EXPORT int RC_CCONV rc_trigger_size(const char* memaddr); +RC_EXPORT rc_trigger_t* RC_CCONV rc_parse_trigger(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_evaluate_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, void* unused_L); +RC_EXPORT int RC_CCONV rc_test_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, void* unused_L); +RC_EXPORT void RC_CCONV rc_reset_trigger(rc_trigger_t* self); + +/*****************************************************************************\ +| Values | +\*****************************************************************************/ + +#define RC_VALUE_MAX_NAME_LENGTH 15 + +struct rc_value_t { + /* The current value of the variable. */ + rc_memref_value_t value; + + /* True if the value has its own rc_memrefs_t */ + uint8_t has_memrefs; + + /* The list of possible values (traverse next chain, pick max). */ + rc_condset_t* conditions; + + /* The name of the variable. */ + const char* name; + + /* The next variable in the chain. */ + rc_value_t* next; +}; + +RC_EXPORT int RC_CCONV rc_value_size(const char* memaddr); +RC_EXPORT rc_value_t* RC_CCONV rc_parse_value(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT int32_t RC_CCONV rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, void* unused_L); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +/* Return values for rc_evaluate_lboard. */ +enum { + RC_LBOARD_STATE_INACTIVE, /* leaderboard is not being processed */ + RC_LBOARD_STATE_WAITING, /* leaderboard cannot activate until the start condition has been false for at least one frame */ + RC_LBOARD_STATE_ACTIVE, /* leaderboard is active and may start */ + RC_LBOARD_STATE_STARTED, /* leaderboard attempt in progress */ + RC_LBOARD_STATE_CANCELED, /* leaderboard attempt canceled */ + RC_LBOARD_STATE_TRIGGERED, /* leaderboard attempt complete, value should be submitted */ + RC_LBOARD_STATE_DISABLED /* leaderboard cannot be processed at this time */ +}; + +struct rc_lboard_t { + rc_trigger_t start; + rc_trigger_t submit; + rc_trigger_t cancel; + rc_value_t value; + rc_value_t* progress; + + uint8_t state; + uint8_t has_memrefs; +}; + +RC_EXPORT int RC_CCONV rc_lboard_size(const char* memaddr); +RC_EXPORT rc_lboard_t* RC_CCONV rc_parse_lboard(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_evaluate_lboard(rc_lboard_t* lboard, int32_t* value, rc_peek_t peek, void* peek_ud, void* unused_L); +RC_EXPORT void RC_CCONV rc_reset_lboard(rc_lboard_t* lboard); + +/*****************************************************************************\ +| Value formatting | +\*****************************************************************************/ + +/* Supported formats. */ +enum { + RC_FORMAT_FRAMES, + RC_FORMAT_SECONDS, + RC_FORMAT_CENTISECS, + RC_FORMAT_SCORE, + RC_FORMAT_VALUE, + RC_FORMAT_MINUTES, + RC_FORMAT_SECONDS_AS_MINUTES, + RC_FORMAT_FLOAT1, + RC_FORMAT_FLOAT2, + RC_FORMAT_FLOAT3, + RC_FORMAT_FLOAT4, + RC_FORMAT_FLOAT5, + RC_FORMAT_FLOAT6, + RC_FORMAT_FIXED1, + RC_FORMAT_FIXED2, + RC_FORMAT_FIXED3, + RC_FORMAT_TENS, + RC_FORMAT_HUNDREDS, + RC_FORMAT_THOUSANDS, + RC_FORMAT_UNSIGNED_VALUE, + RC_FORMAT_UNFORMATTED +}; + +RC_EXPORT int RC_CCONV rc_parse_format(const char* format_str); +RC_EXPORT int RC_CCONV rc_format_value(char* buffer, int size, int32_t value, int format); + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +typedef struct rc_richpresence_lookup_item_t rc_richpresence_lookup_item_t; + +struct rc_richpresence_lookup_item_t { + uint32_t first; + uint32_t last; + rc_richpresence_lookup_item_t* left; + rc_richpresence_lookup_item_t* right; + const char* label; +}; + +typedef struct rc_richpresence_lookup_t rc_richpresence_lookup_t; + +struct rc_richpresence_lookup_t { + rc_richpresence_lookup_item_t* root; + rc_richpresence_lookup_t* next; + const char* name; + const char* default_label; + uint8_t format; +}; + +typedef struct rc_richpresence_display_part_t rc_richpresence_display_part_t; + +struct rc_richpresence_display_part_t { + rc_richpresence_display_part_t* next; + const char* text; + rc_richpresence_lookup_t* lookup; + rc_operand_t value; + uint8_t display_type; +}; + +typedef struct rc_richpresence_display_t rc_richpresence_display_t; + +struct rc_richpresence_display_t { + rc_trigger_t trigger; + rc_richpresence_display_t* next; + rc_richpresence_display_part_t* display; + uint8_t has_required_hits; +}; + +struct rc_richpresence_t { + rc_richpresence_display_t* first_display; + rc_richpresence_lookup_t* first_lookup; + rc_value_t* values; + uint8_t has_memrefs; +}; + +RC_EXPORT int RC_CCONV rc_richpresence_size(const char* script); +RC_EXPORT int RC_CCONV rc_richpresence_size_lines(const char* script, int* lines_read); +RC_EXPORT rc_richpresence_t* RC_CCONV rc_parse_richpresence(void* buffer, const char* script, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L); +RC_EXPORT void RC_CCONV rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, void* unused_L); +RC_EXPORT int RC_CCONV rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L); +RC_EXPORT void RC_CCONV rc_reset_richpresence(rc_richpresence_t* self); + +RC_END_C_DECLS + +#endif /* RC_RUNTIME_TYPES_H */ diff --git a/src/rcheevos/include/rc_util.h b/src/rcheevos/include/rc_util.h new file mode 100644 index 0000000000..18d27e0fc7 --- /dev/null +++ b/src/rcheevos/include/rc_util.h @@ -0,0 +1,51 @@ +#ifndef RC_UTIL_H +#define RC_UTIL_H + +#include "rc_export.h" + +#include +#include + +RC_BEGIN_C_DECLS + +/** + * A block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_buffer_chunk_t { + /* The current location where data is being written */ + uint8_t* write; + /* The first byte past the end of data where writing cannot occur */ + uint8_t* end; + /* The first byte of the data */ + uint8_t* start; + /* The next block in the allocated memory chain */ + struct rc_buffer_chunk_t* next; +} +rc_buffer_chunk_t; + +/** + * A preallocated block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_buffer_t { + /* The chunk data (will point at the local data member) */ + struct rc_buffer_chunk_t chunk; + /* Small chunk of memory pre-allocated for the chunk */ + uint8_t data[256]; +} +rc_buffer_t; + +void rc_buffer_init(rc_buffer_t* buffer); +void rc_buffer_destroy(rc_buffer_t* buffer); +uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount); +void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end); +void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount); +char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src); +char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len); + +uint32_t rc_djb2(const char* input); + +void rc_format_md5(char checksum[33], const uint8_t digest[16]); + +RC_END_C_DECLS + +#endif /* RC_UTIL_H */ diff --git a/src/rcheevos/include/rcheevos.h b/src/rcheevos/include/rcheevos.h new file mode 100644 index 0000000000..0d96002148 --- /dev/null +++ b/src/rcheevos/include/rcheevos.h @@ -0,0 +1,8 @@ +#ifndef RCHEEVOS_H +#define RCHEEVOS_H + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "rc_consoles.h" + +#endif /* RCHEEVOS_H */ diff --git a/src/rcheevos/src/rapi/rc_api_common.c b/src/rcheevos/src/rapi/rc_api_common.c new file mode 100644 index 0000000000..3a261fba46 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_common.c @@ -0,0 +1,1379 @@ +#include "rc_api_common.h" +#include "rc_api_request.h" +#include "rc_api_runtime.h" + +#include "../rc_compat.h" + +#include +#include +#include +#include + +#define RETROACHIEVEMENTS_HOST "https://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org" +#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org" +rc_api_host_t g_host = { NULL, NULL }; + +/* --- rc_json --- */ + +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen); +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); + +static int rc_json_match_char(rc_json_iterator_t* iterator, char c) +{ + if (iterator->json < iterator->end && *iterator->json == c) { + ++iterator->json; + return 1; + } + + return 0; +} + +static void rc_json_skip_whitespace(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json)) + ++iterator->json; +} + +static int rc_json_find_substring(rc_json_iterator_t* iterator, const char* substring) +{ + const char first = *substring; + const size_t substring_len = strlen(substring); + const char* end = iterator->end - substring_len; + + while (iterator->json <= end) { + if (*iterator->json == first) { + if (memcmp(iterator->json, substring, substring_len) == 0) + return 1; + } + + ++iterator->json; + } + + return 0; +} + +static int rc_json_find_closing_quote(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end) { + if (*iterator->json == '"') + return 1; + + if (*iterator->json == '\\') { + ++iterator->json; + if (iterator->json == iterator->end) + return 0; + } + + if (*iterator->json == '\0') + return 0; + + ++iterator->json; + } + + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + int result; + + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; + + field->value_start = iterator->json; + + switch (*iterator->json) + { + case '"': /* quoted string */ + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; + break; + + case '-': + case '+': /* signed number */ + ++iterator->json; + /* fallthrough to number */ + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': /* number */ + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + + if (rc_json_match_char(iterator, '.')) { + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + } + break; + + case '[': /* array */ + result = rc_json_parse_array(iterator, field); + if (result != RC_OK) + return result; + + break; + + case '{': /* object */ + result = rc_json_parse_object(iterator, NULL, 0, &field->array_size); + if (result != RC_OK) + return result; + + break; + + default: /* non-quoted text [true,false,null] */ + if (!isalpha((unsigned char)*iterator->json)) + return RC_INVALID_JSON; + + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; + break; + } + + field->value_end = iterator->json; + return RC_OK; +} + +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_field_t unused_field; + int result; + + if (!rc_json_match_char(iterator, '[')) + return RC_INVALID_JSON; + + field->array_size = 0; + + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; + + do + { + rc_json_skip_whitespace(iterator); + + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; + + ++field->array_size; + + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); + + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; + + return RC_OK; +} + +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, '"')) + return RC_INVALID_JSON; + + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) + return RC_INVALID_JSON; + ++iterator->json; + } + + if (iterator->json == iterator->end) + return RC_INVALID_JSON; + + field->name_len = iterator->json - field->name; + ++iterator->json; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ':')) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); + + if (rc_json_parse_field(iterator, field) < 0) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); + + return RC_OK; +} + +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, uint32_t* fields_seen) { + size_t i; + uint32_t num_fields = 0; + rc_json_field_t field; + int result; + + if (fields_seen) + *fields_seen = 0; + + for (i = 0; i < field_count; ++i) + fields[i].value_start = fields[i].value_end = NULL; + + if (!rc_json_match_char(iterator, '{')) + return RC_INVALID_JSON; + + if (rc_json_match_char(iterator, '}')) /* empty object */ + return RC_OK; + + do + { + result = rc_json_get_next_field(iterator, &field); + if (result != RC_OK) + return result; + + for (i = 0; i < field_count; ++i) { + if (!fields[i].value_start && fields[i].name_len == field.name_len && + memcmp(fields[i].name, field.name, field.name_len) == 0) { + fields[i].value_start = field.value_start; + fields[i].value_end = field.value_end; + fields[i].array_size = field.array_size; + break; + } + } + + ++num_fields; + + } while (rc_json_match_char(iterator, ',')); + + if (!rc_json_match_char(iterator, '}')) + return RC_INVALID_JSON; + + if (fields_seen) + *fields_seen = num_fields; + + return RC_OK; +} + +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{')) + return 0; + + return (rc_json_get_next_field(iterator, field) == RC_OK); +} + +int rc_json_get_object_string_length(const char* json) { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */ + + rc_json_parse_object(&iterator, NULL, 0, NULL); + + if (iterator.json == json) /* not JSON */ + return (int)strlen(json); + + return (int)(iterator.json - json); +} + +static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + + /* assume the title contains the most appropriate message to display to the user */ + if (rc_json_find_substring(&iterator, "")) { + const char* title_start = iterator.json + 7; + if (rc_json_find_substring(&iterator, "")) { + response->error_message = rc_buffer_strncpy(&response->buffer, title_start, iterator.json - title_start); + response->succeeded = 0; + return RC_INVALID_JSON; + } + } + + /* title not found, return the first line of the response (up to 200 characters) */ + iterator.json = server_response->body; + + while (iterator.json < iterator.end && *iterator.json != '\n' && + iterator.json - server_response->body < 200) { + ++iterator.json; + } + + if (iterator.json > server_response->body && iterator.json[-1] == '\r') + --iterator.json; + + if (iterator.json > server_response->body) + response->error_message = rc_buffer_strncpy(&response->buffer, server_response->body, iterator.json - server_response->body); + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +static int rc_json_convert_error_code(const char* server_error_code) +{ + switch (server_error_code[0]) { + case 'a': + if (strcmp(server_error_code, "access_denied") == 0) + return RC_ACCESS_DENIED; + break; + + case 'e': + if (strcmp(server_error_code, "expired_token") == 0) + return RC_EXPIRED_TOKEN; + break; + + case 'i': + if (strcmp(server_error_code, "invalid_credentials") == 0) + return RC_INVALID_CREDENTIALS; + if (strcmp(server_error_code, "invalid_parameter") == 0) + return RC_INVALID_STATE; + break; + + case 'm': + if (strcmp(server_error_code, "missing_parameter") == 0) + return RC_INVALID_STATE; + break; + + case 'n': + if (strcmp(server_error_code, "not_found") == 0) + return RC_NOT_FOUND; + break; + + default: + break; + } + + return RC_API_FAILURE; +} + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) { + int result; + +#ifndef NDEBUG + if (field_count < 2) + return RC_INVALID_STATE; + if (strcmp(fields[0].name, "Success") != 0) + return RC_INVALID_STATE; + if (strcmp(fields[1].name, "Error") != 0) + return RC_INVALID_STATE; +#endif + + response->error_message = NULL; + + if (!server_response) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR || + server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) { + /* client provided error message is passed as the response body */ + response->error_message = server_response->body; + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + if (!server_response->body || !*server_response->body) { + /* expect valid HTTP status codes to have bodies that we can extract the message from, + * but provide some default messages in case they don't. */ + switch (server_response->http_status_code) { + case 504: /* 504 Gateway Timeout */ + case 522: /* 522 Connection Timed Out */ + case 524: /* 524 A Timeout Occurred */ + response->error_message = "Request has timed out."; + break; + + case 521: /* 521 Web Server is Down */ + case 523: /* 523 Origin is Unreachable */ + response->error_message = "Could not connect to server."; + break; + + default: + break; + } + + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + if (*server_response->body != '{') { + result = rc_json_extract_html_error(response, server_response); + } + else { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + result = rc_json_parse_object(&iterator, fields, field_count, NULL); + + rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); + rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); + + /* Code will be the third field in the fields array, but may not always be present */ + if (field_count > 2 && strcmp(fields[2].name, "Code") == 0) { + rc_json_get_optional_string(&response->error_code, response, &fields[2], "Code", NULL); + if (response->error_code != NULL) + result = rc_json_convert_error_code(response->error_code); + } + } + + return result; +} + +static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { + const char* not_found = " not found in response"; + const size_t not_found_len = strlen(not_found); + const size_t field_len = strlen(field->name); + + uint8_t* write = rc_buffer_reserve(&response->buffer, field_len + not_found_len + 1); + if (write) { + response->error_message = (char*)write; + memcpy(write, field->name, field_len); + write += field_len; + memcpy(write, not_found, not_found_len + 1); + write += not_found_len + 1; + rc_buffer_consume(&response->buffer, (uint8_t*)response->error_message, write); + } + + response->succeeded = 0; + return 0; +} + +int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { + rc_json_iterator_t iterator; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!field->value_start) + return rc_json_missing_field(response, field); + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = field->value_start; + iterator.end = field->value_end; + return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK); +} + +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) + return 0; + + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); + + return 1; +} + +int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_iterator_t iterator; + rc_json_field_t array; + rc_json_field_t value; + uint32_t* entry; + + memset(&array, 0, sizeof(array)); + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) + return RC_MISSING_VALUE; + + if (*num_entries) { + *entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t)); + if (!*entries) + return RC_OUT_OF_MEMORY; + + value.name = field_name; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array.value_start; + iterator.end = array.value_end; + + entry = *entries; + while (rc_json_get_array_entry_value(&value, &iterator)) { + if (!rc_json_get_unum(entry, &value, field_name)) + return RC_MISSING_VALUE; + + ++entry; + } + } + else { + *entries = NULL; + } + + return RC_OK; +} + +int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#endif + + if (!rc_json_get_optional_array(num_entries, array_field, field, field_name)) + return rc_json_missing_field(response, field); + + return 1; +} + +int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* array_field, const rc_json_field_t* field, const char* field_name) { +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!field->value_start || *field->value_start != '[') { + *num_entries = 0; + return 0; + } + + memcpy(array_field, field, sizeof(*array_field)); + ++array_field->value_start; /* skip [ */ + + *num_entries = field->array_size; + return 1; +} + +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) + return 0; + + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; + + rc_json_skip_whitespace(iterator); + + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); + + return 1; +} + +static uint32_t rc_json_decode_hex4(const char* input) { + char hex[5]; + + memcpy(hex, input, 4); + hex[4] = '\0'; + + return (uint32_t)strtoul(hex, NULL, 16); +} + +static int rc_json_ucs32_to_utf8(uint8_t* dst, uint32_t ucs32_char) { + if (ucs32_char < 0x80) { + dst[0] = (ucs32_char & 0x7F); + return 1; + } + + if (ucs32_char < 0x0800) { + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xC0 | (ucs32_char & 0x1F); + return 2; + } + + if (ucs32_char < 0x010000) { + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xE0 | (ucs32_char & 0x0F); + return 3; + } + + if (ucs32_char < 0x200000) { + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF0 | (ucs32_char & 0x07); + return 4; + } + + if (ucs32_char < 0x04000000) { + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xF8 | (ucs32_char & 0x03); + return 5; + } + + dst[5] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[4] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[3] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[2] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[1] = 0x80 | (ucs32_char & 0x3F); ucs32_char >>= 6; + dst[0] = 0xFC | (ucs32_char & 0x01); + return 6; +} + +int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + size_t len = field->value_end - field->value_start; + char* dst; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = NULL; + return 0; + } + + if (len == 4 && memcmp(field->value_start, "null", 4) == 0) { + *out = NULL; + return 1; + } + + if (*src == '\"') { + ++src; + + if (*src == '\"') { + /* simple optimization for empty string - don't allocate space */ + *out = ""; + return 1; + } + + *out = dst = (char*)rc_buffer_reserve(buffer, len - 1); /* -2 for quotes, +1 for null terminator */ + + do { + if (*src == '\\') { + ++src; + if (*src == 'n') { + /* newline */ + ++src; + *dst++ = '\n'; + continue; + } + + if (*src == 'r') { + /* carriage return */ + ++src; + *dst++ = '\r'; + continue; + } + + if (*src == 'u') { + /* unicode character */ + uint32_t ucs32_char = rc_json_decode_hex4(src + 1); + src += 5; + + if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) { + /* surrogate lead - look for surrogate tail */ + if (ucs32_char < 0xDC00 && src[0] == '\\' && src[1] == 'u') { + const uint32_t surrogate = rc_json_decode_hex4(src + 2); + src += 6; + + if (surrogate >= 0xDC00 && surrogate < 0xE000) { + /* found a surrogate tail, merge them */ + ucs32_char = (((ucs32_char - 0xD800) << 10) | (surrogate - 0xDC00)) + 0x10000; + } + } + + if (!(ucs32_char & 0xFFFF0000)) { + /* invalid surrogate pair, fallback to replacement char */ + ucs32_char = 0xFFFD; + } + } + + dst += rc_json_ucs32_to_utf8((unsigned char*)dst, ucs32_char); + continue; + } + + if (*src == 't') { + /* tab */ + ++src; + *dst++ = '\t'; + continue; + } + + /* just an escaped character, fallthrough to normal copy */ + } + + *dst++ = *src++; + } while (*src != '\"'); + + } else { + *out = dst = (char*)rc_buffer_reserve(buffer, len + 1); /* +1 for null terminator */ + memcpy(dst, src, len); + dst += len; + } + + *dst++ = '\0'; + rc_buffer_consume(buffer, (uint8_t*)(*out), (uint8_t*)dst); + return 1; +} + +int rc_json_field_string_matches(const rc_json_field_t* field, const char* text) { + int is_quoted = 0; + const char* ptr = field->value_start; + if (!ptr) + return 0; + + if (*ptr == '"') { + is_quoted = 1; + ++ptr; + } + + while (ptr < field->value_end) { + if (*ptr != *text) { + if (*ptr != '\\') { + if (*ptr == '"' && is_quoted && (*text == '\0')) { + is_quoted = 0; + ++ptr; + continue; + } + + return 0; + } + + ++ptr; + switch (*ptr) { + case 'n': + if (*text != '\n') + return 0; + break; + case 'r': + if (*text != '\r') + return 0; + break; + case 't': + if (*text != '\t') + return 0; + break; + default: + if (*text != *ptr) + return 0; + break; + } + } + + ++text; + ++ptr; + } + + return !is_quoted && (*text == '\0'); +} + +void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value) { + if (!rc_json_get_string(out, &response->buffer, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_string(out, &response->buffer, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + int32_t value = 0; + int negative = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + /* assert: string contains only numerals and an optional sign per rc_json_parse_field */ + if (*src == '-') { + negative = 1; + ++src; + } else if (*src == '+') { + ++src; + } else if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + if (negative) + *out = -value; + else + *out = value; + + return 1; +} + +void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_num(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_num(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + uint32_t value = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + /* assert: string contains only numerals per rc_json_parse_field */ + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + *out = value; + return 1; +} + +void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value) { + if (!rc_json_get_unum(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_unum(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name) { + int32_t whole, fraction, fraction_denominator; + const char* decimal = field->value_start; + + if (!decimal) { + *out = 0.0f; + return 0; + } + + if (!rc_json_get_num(&whole, field, field_name)) + return 0; + + while (decimal < field->value_end && *decimal != '.') + ++decimal; + + fraction = 0; + fraction_denominator = 1; + if (decimal) { + ++decimal; + while (decimal < field->value_end && *decimal >= '0' && *decimal <= '9') { + fraction *= 10; + fraction += *decimal - '0'; + fraction_denominator *= 10; + ++decimal; + } + } + + if (whole < 0) + fraction = -fraction; + + *out = (float)whole + ((float)fraction / (float)fraction_denominator); + return 1; +} + +void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value) { + if (!rc_json_get_float(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_float(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name) { + struct tm tm; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (*field->value_start == '\"') { + memset(&tm, 0, sizeof(tm)); + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */ + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 || + /* NOTE: relies on sscanf stopping when it sees a non-digit after the seconds. could be 'Z', '.', '+', or '-' */ + sscanf_s(field->value_start + 1, "%d-%d-%dT%d:%d:%d", /* ISO format "2013-10-20T22:12:21.000000Z */ + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { + tm.tm_mon--; /* 0-based */ + tm.tm_year -= 1900; /* 1900 based */ + + /* mktime converts a struct tm to a time_t using the local timezone. + * the input string is UTC. since timegm is not universally cross-platform, + * figure out the offset between UTC and local time by applying the + * timezone conversion twice and manually removing the difference */ + { + time_t local_timet = mktime(&tm); + time_t skewed_timet, tz_offset; + struct tm gmt_tm; + gmtime_s(&gmt_tm, &local_timet); + skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */ + tz_offset = skewed_timet - local_timet; + *out = local_timet - tz_offset; + } + + return 1; + } + } + + *out = 0; + return 0; +} + +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_datetime(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) { + const char* src = field->value_start; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (src) { + const size_t len = field->value_end - field->value_start; + if (len == 4 && strncasecmp(src, "true", 4) == 0) { + *out = 1; + return 1; + } else if (len == 5 && strncasecmp(src, "false", 5) == 0) { + *out = 0; + return 1; + } else if (len == 1) { + *out = (*src != '0'); + return 1; + } + } + + *out = 0; + return 0; +} + +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value) { + if (!rc_json_get_bool(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_bool(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + +void rc_json_extract_filename(rc_json_field_t* field) { + if (field->value_end) { + const char* str = field->value_end; + + /* remove the extension */ + while (str > field->value_start && str[-1] != '/') { + --str; + if (*str == '.') { + field->value_end = str; + break; + } + } + + /* find the path separator */ + while (str > field->value_start && str[-1] != '/') + --str; + + field->value_start = str; + } +} + +/* --- rc_api_request --- */ + +void rc_api_destroy_request(rc_api_request_t* request) +{ + rc_buffer_destroy(&request->buffer); +} + +/* --- rc_url_builder --- */ + +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size) { + rc_buffer_chunk_t* used_buffer; + + memset(builder, 0, sizeof(*builder)); + builder->buffer = buffer; + builder->write = builder->start = (char*)rc_buffer_reserve(buffer, estimated_size); + + used_buffer = &buffer->chunk; + while (used_buffer && used_buffer->write != (uint8_t*)builder->write) + used_buffer = used_buffer->next; + + builder->end = (used_buffer) ? (char*)used_buffer->end : builder->start + estimated_size; +} + +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder) { + rc_url_builder_append(builder, "", 1); + + if (builder->result != RC_OK) + return NULL; + + rc_buffer_consume(builder->buffer, (uint8_t*)builder->start, (uint8_t*)builder->write); + return builder->start; +} + +static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) { + if (builder->result == RC_OK) { + size_t remaining = builder->end - builder->write; + if (remaining < amount) { + const size_t used = builder->write - builder->start; + const size_t current_size = builder->end - builder->start; + const size_t buffer_prefix_size = sizeof(rc_buffer_chunk_t); + char* new_start; + size_t new_size = (current_size < 256) ? 256 : current_size * 2; + do { + remaining = new_size - used; + if (remaining >= amount) + break; + + new_size *= 2; + } while (1); + + /* rc_buffer_reserve will align to 256 bytes after including the buffer prefix. attempt to account for that */ + if ((remaining - amount) > buffer_prefix_size) + new_size -= buffer_prefix_size; + + new_start = (char*)rc_buffer_reserve(builder->buffer, new_size); + if (!new_start) { + builder->result = RC_OUT_OF_MEMORY; + return RC_OUT_OF_MEMORY; + } + + if (new_start != builder->start) { + memcpy(new_start, builder->start, used); + builder->start = new_start; + builder->write = new_start + used; + } + + builder->end = builder->start + new_size; + } + } + + return builder->result; +} + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str) { + static const char hex[] = "0123456789abcdef"; + const char* start = str; + size_t len = 0; + for (;;) { + const char c = *str++; + switch (c) { + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + case '-': case '_': case '.': case '~': + len++; + continue; + + case '\0': + if (len) + rc_url_builder_append(builder, start, len); + + return; + + default: + if (rc_url_builder_reserve(builder, len + 3) != RC_OK) + return; + + if (len) { + memcpy(builder->write, start, len); + builder->write += len; + } + + if (c == ' ') { + *builder->write++ = '+'; + } else { + *builder->write++ = '%'; + *builder->write++ = hex[((unsigned char)c) >> 4]; + *builder->write++ = hex[c & 0x0F]; + } + break; + } + + start = str; + len = 0; + } +} + +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len) { + if (rc_url_builder_reserve(builder, len) == RC_OK) { + memcpy(builder->write, data, len); + builder->write += len; + } +} + +static int rc_url_builder_append_param_equals(rc_api_url_builder_t* builder, const char* param) { + size_t param_len = strlen(param); + + if (rc_url_builder_reserve(builder, param_len + 2) == RC_OK) { + if (builder->write > builder->start) { + if (builder->write[-1] != '?') + *builder->write++ = '&'; + } + + memcpy(builder->write, param, param_len); + builder->write += param_len; + *builder->write++ = '='; + } + + return builder->result; +} + +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value) { + if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%u", value); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value) { + if (rc_url_builder_append_param_equals(builder, param) == RC_OK) { + char num[16]; + int chars = snprintf(num, sizeof(num), "%d", value); + rc_url_builder_append(builder, num, chars); + } +} + +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value) { + rc_url_builder_append_param_equals(builder, param); + rc_url_builder_append_encoded_str(builder, value); +} + +void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host) { + #define DOREQUEST_ENDPOINT "/dorequest.php" + rc_buffer_init(&request->buffer); + + if (!host || !host->host) { + request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; + } + else { + const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT); + const size_t host_len = strlen(host->host); + const size_t protocol_len = (strstr(host->host, "://")) ? 0 : 7; + const size_t url_len = protocol_len + host_len + endpoint_len; + uint8_t* url = rc_buffer_reserve(&request->buffer, url_len); + + if (protocol_len) + memcpy(url, "http://", protocol_len); + + memcpy(url + protocol_len, host->host, host_len); + memcpy(url + protocol_len + host_len, DOREQUEST_ENDPOINT, endpoint_len); + rc_buffer_consume(&request->buffer, url, url + url_len); + + request->url = (char*)url; + } + #undef DOREQUEST_ENDPOINT +} + +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token) { + if (!username || !*username || !api_token || !*api_token) { + builder->result = RC_INVALID_STATE; + return 0; + } + + rc_url_builder_append_str_param(builder, "r", api); + rc_url_builder_append_str_param(builder, "u", username); + rc_url_builder_append_str_param(builder, "t", api_token); + + return (builder->result == RC_OK); +} + +/* --- Set Host --- */ + +static void rc_api_update_host(const char** host, const char* hostname) { + if (*host != NULL) + free((void*)*host); + + if (hostname != NULL) { + if (strstr(hostname, "://")) { + *host = strdup(hostname); + } + else { + const size_t hostname_len = strlen(hostname); + if (hostname_len == 0) { + *host = NULL; + } + else { + char* newhost = (char*)malloc(hostname_len + 7 + 1); + if (newhost) { + memcpy(newhost, "http://", 7); + memcpy(&newhost[7], hostname, hostname_len + 1); + *host = newhost; + } + else { + *host = NULL; + } + } + } + } + else { + *host = NULL; + } +} + +const char* rc_api_default_host(void) { + return RETROACHIEVEMENTS_HOST; +} + +void rc_api_set_host(const char* hostname) { + if (hostname && strcmp(hostname, RETROACHIEVEMENTS_HOST) == 0) + hostname = NULL; + + rc_api_update_host(&g_host.host, hostname); + + if (!hostname) { + /* also clear out the image hostname */ + rc_api_set_image_host(NULL); + } + else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if just pointing at the non-HTTPS host, explicitly use the default image host + * so it doesn't try to use the web host directly */ + rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL); + } +} + +void rc_api_set_image_host(const char* hostname) { + rc_api_update_host(&g_host.media_host, hostname); +} + +/* --- Fetch Image --- */ + +int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) { + return rc_api_init_fetch_image_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_buffer_init(&request->buffer); + rc_url_builder_init(&builder, &request->buffer, 64); + + if (host && host->media_host) { + /* custom media host provided */ + if (!strstr(host->host, "://")) + rc_url_builder_append(&builder, "http://", 7); + rc_url_builder_append(&builder, host->media_host, strlen(host->media_host)); + } + else if (host && host->host) { + if (strcmp(host->host, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if host specifically set to non-ssl host, and no media host provided, use non-ssl media host */ + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST_NONSSL, sizeof(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL) - 1); + } + else if (strcmp(host->host, RETROACHIEVEMENTS_HOST) == 0) { + /* if host specifically set to ssl host, and no media host provided, use media host */ + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); + } + else { + /* custom host and no media host provided. assume custom host is also media host */ + if (!strstr(host->host, "://")) + rc_url_builder_append(&builder, "http://", 7); + rc_url_builder_append(&builder, host->host, strlen(host->host)); + } + } + else { + /* no custom host provided */ + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); + } + + switch (api_params->image_type) + { + case RC_IMAGE_TYPE_GAME: + rc_url_builder_append(&builder, "/Images/", 8); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED: + rc_url_builder_append(&builder, "/Badge/", 7); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, "_lock.png", 9); + break; + + case RC_IMAGE_TYPE_USER: + rc_url_builder_append(&builder, "/UserPic/", 9); + rc_url_builder_append(&builder, api_params->image_name, strlen(api_params->image_name)); + rc_url_builder_append(&builder, ".png", 4); + break; + + default: + return RC_INVALID_STATE; + } + + request->url = rc_url_builder_finalize(&builder); + request->post_data = NULL; + + return builder.result; +} + +const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name) { + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result == RC_OK) + return rc_buffer_strcpy(buffer, request.url); + + return NULL; +} diff --git a/src/rcheevos/src/rapi/rc_api_common.h b/src/rcheevos/src/rapi/rc_api_common.h new file mode 100644 index 0000000000..78e10d49b9 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_common.h @@ -0,0 +1,88 @@ +#ifndef RC_API_COMMON_H +#define RC_API_COMMON_H + +#include "rc_api_request.h" + +#include +#include + +RC_BEGIN_C_DECLS + +#define RC_CONTENT_TYPE_URLENCODED "application/x-www-form-urlencoded" + +typedef struct rc_api_url_builder_t { + char* write; + char* start; + char* end; + /* pointer to a preallocated rc_buffer_t */ + rc_buffer_t* buffer; + int result; +} +rc_api_url_builder_t; + +void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, size_t estimated_size); +void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); +const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); + +extern rc_api_host_t g_host; + +#define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} + +typedef struct rc_json_field_t { + const char* value_start; + const char* value_end; + const char* name; + size_t name_len; + uint32_t array_size; +} +rc_json_field_t; + +typedef struct rc_json_iterator_t { + const char* json; + const char* end; +} +rc_json_iterator_t; + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count); +int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); +int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name); +void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value); +void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value); +void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value); +void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value); +void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* iterator, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); +int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); +int rc_json_get_object_string_length(const char* json); +int rc_json_field_string_matches(const rc_json_field_t* field, const char* text); + +void rc_json_extract_filename(rc_json_field_t* field); + +void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); +void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value); +void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value); +void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value); + +const char* rc_api_default_host(void); +void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host); +int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token); + +const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name); + +RC_END_C_DECLS + +#endif /* RC_API_COMMON_H */ diff --git a/src/rcheevos/src/rapi/rc_api_editor.c b/src/rcheevos/src/rapi/rc_api_editor.c new file mode 100644 index 0000000000..775f73de8e --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_editor.c @@ -0,0 +1,625 @@ +#include "rc_api_editor.h" +#include "rc_api_common.h" +#include "rc_api_runtime.h" + +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include + +/* --- Fetch Code Notes --- */ + +int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params) { + return rc_api_init_fetch_code_notes_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_code_notes_request_hosted(rc_api_request_t* request, + const rc_api_fetch_code_notes_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "codenotes2"); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_code_notes_server_response(response, &response_obj); +} + +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; + rc_api_code_note_t* note; + const char* address_str; + const char* last_author = ""; + size_t last_author_len = 0; + size_t len; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("CodeNotes") + }; + + rc_json_field_t note_fields[] = { + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Note") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_array(&response->num_notes, &array_field, &response->response, &fields[2], "CodeNotes")) + return RC_MISSING_VALUE; + + if (response->num_notes) { + response->notes = (rc_api_code_note_t*)rc_buffer_alloc(&response->response.buffer, response->num_notes * sizeof(rc_api_code_note_t)); + if (!response->notes) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + note = response->notes; + while (rc_json_get_array_entry_object(note_fields, sizeof(note_fields) / sizeof(note_fields[0]), &iterator)) { + /* an empty note represents a record that was deleted on the server */ + /* a note set to '' also represents a deleted note (remnant of a bug) */ + /* NOTE: len will include the quotes */ + if (note_fields[2].value_start) { + len = note_fields[2].value_end - note_fields[2].value_start; + if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { + --response->num_notes; + continue; + } + } + + if (!rc_json_get_required_string(&address_str, &response->response, ¬e_fields[0], "Address")) + return RC_MISSING_VALUE; + note->address = (uint32_t)strtoul(address_str, NULL, 16); + if (!rc_json_get_required_string(¬e->note, &response->response, ¬e_fields[2], "Note")) + return RC_MISSING_VALUE; + + len = note_fields[1].value_end - note_fields[1].value_start; + if (len == last_author_len && memcmp(note_fields[1].value_start, last_author, len) == 0) { + note->author = last_author; + } + else { + if (!rc_json_get_required_string(¬e->author, &response->response, ¬e_fields[1], "User")) + return RC_MISSING_VALUE; + + if (note->author == NULL) { + /* ensure we don't pass NULL out to client */ + last_author = note->author = ""; + last_author_len = 0; + } else { + last_author = note->author; + last_author_len = len; + } + } + + ++note; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Code Note --- */ + +int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params) { + return rc_api_init_update_code_note_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_update_code_note_request_hosted(rc_api_request_t* request, + const rc_api_update_code_note_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitcodenote", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "m", api_params->address); + + if (api_params->note && *api_params->note) + rc_url_builder_append_str_param(&builder, "n", api_params->note); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_code_note_server_response(response, &response_obj); +} + +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") + /* unused fields + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("Note") + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + return RC_OK; +} + +void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Achievement --- */ + +static const char* rc_type_string(uint32_t type) { + switch (type) { + case RC_ACHIEVEMENT_TYPE_MISSABLE: return "missable"; + case RC_ACHIEVEMENT_TYPE_PROGRESSION: return "progression"; + case RC_ACHIEVEMENT_TYPE_WIN: return "win_condition"; + default: return ""; + } +} + +int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params) { + return rc_api_init_update_achievement_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_update_achievement_request_hosted(rc_api_request_t* request, + const rc_api_update_achievement_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0 || api_params->category == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description || !*api_params->description) + return RC_INVALID_STATE; + if (!api_params->trigger || !*api_params->trigger) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadachievement", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->achievement_id) + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "m", api_params->trigger); + rc_url_builder_append_unum_param(&builder, "z", api_params->points); + rc_url_builder_append_unum_param(&builder, "f", api_params->category); + if (api_params->badge) + rc_url_builder_append_str_param(&builder, "b", api_params->badge); + rc_url_builder_append_str_param(&builder, "x", rc_type_string(api_params->type)); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->trigger, (int)strlen(api_params->trigger)); + snprintf(buffer, sizeof(buffer), "%u", api_params->points); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + snprintf(buffer, sizeof(buffer), "%u", api_params->points * 3); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_finish(&md5, hash); + rc_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_achievement_server_response(response, &response_obj); +} + +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->achievement_id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Leaderboard --- */ + +int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params) { + return rc_api_init_update_leaderboard_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_update_leaderboard_request_hosted(rc_api_request_t* request, + const rc_api_update_leaderboard_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t hash[16]; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + if (!api_params->title || !*api_params->title) + return RC_INVALID_STATE; + if (!api_params->description) + return RC_INVALID_STATE; + if (!api_params->start_trigger || !*api_params->start_trigger) + return RC_INVALID_STATE; + if (!api_params->submit_trigger || !*api_params->submit_trigger) + return RC_INVALID_STATE; + if (!api_params->cancel_trigger || !*api_params->cancel_trigger) + return RC_INVALID_STATE; + if (!api_params->value_definition || !*api_params->value_definition) + return RC_INVALID_STATE; + if (!api_params->format || !*api_params->format) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "uploadleaderboard", api_params->username, api_params->api_token)) + return builder.result; + + if (api_params->leaderboard_id) + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "n", api_params->title); + rc_url_builder_append_str_param(&builder, "d", api_params->description); + rc_url_builder_append_str_param(&builder, "s", api_params->start_trigger); + rc_url_builder_append_str_param(&builder, "b", api_params->submit_trigger); + rc_url_builder_append_str_param(&builder, "c", api_params->cancel_trigger); + rc_url_builder_append_str_param(&builder, "l", api_params->value_definition); + rc_url_builder_append_num_param(&builder, "w", api_params->lower_is_better); + rc_url_builder_append_str_param(&builder, "f", api_params->format); + + /* Evaluate the signature. */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + md5_append(&md5, (md5_byte_t*)"SECRET", 6); + snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)"SEC", 3); + md5_append(&md5, (md5_byte_t*)api_params->start_trigger, (int)strlen(api_params->start_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->submit_trigger, (int)strlen(api_params->submit_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->cancel_trigger, (int)strlen(api_params->cancel_trigger)); + md5_append(&md5, (md5_byte_t*)api_params->value_definition, (int)strlen(api_params->value_definition)); + md5_append(&md5, (md5_byte_t*)"RE2", 3); + md5_append(&md5, (md5_byte_t*)api_params->format, (int)strlen(api_params->format)); + md5_finish(&md5, hash); + rc_format_md5(buffer, hash); + rc_url_builder_append_str_param(&builder, "h", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_leaderboard_server_response(response, &response_obj); +} + +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardID") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->leaderboard_id, &response->response, &fields[2], "LeaderboardID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Update Rich Presence --- */ + +int rc_api_init_update_rich_presence_request(rc_api_request_t* request, const rc_api_update_rich_presence_request_t* api_params) { + return rc_api_init_update_rich_presence_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_update_rich_presence_request_hosted(rc_api_request_t* request, + const rc_api_update_rich_presence_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + if (!api_params->script) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitrichpresence", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_str_param(&builder, "d", api_params->script); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_update_rich_presence_server_response(rc_api_update_rich_presence_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + return RC_OK; +} + +void rc_api_destroy_update_rich_presence_response(rc_api_update_rich_presence_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Badge Range --- */ + +int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params) { + return rc_api_init_fetch_badge_range_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_badge_range_request_hosted(rc_api_request_t* request, + const rc_api_fetch_badge_range_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "badgeiter"); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + (void)api_params; + + return builder.result; +} + +int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_badge_range_server_response(response, &response_obj); +} + +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("FirstBadge"), + RC_JSON_NEW_FIELD("NextBadge") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->first_badge_id, &response->response, &fields[2], "FirstBadge")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->next_badge_id, &response->response, &fields[3], "NextBadge")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Add Game Hash --- */ + +int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params) { + return rc_api_init_add_game_hash_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_add_game_hash_request_hosted(rc_api_request_t* request, + const rc_api_add_game_hash_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + if (!api_params->hash || !*api_params->hash) + return RC_INVALID_STATE; + if (api_params->game_id == 0 && (!api_params->title || !*api_params->title)) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 128); + if (!rc_api_url_build_dorequest(&builder, "submitgametitle", api_params->username, api_params->api_token)) + return builder.result; + + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + rc_url_builder_append_str_param(&builder, "m", api_params->hash); + if (api_params->title) + rc_url_builder_append_str_param(&builder, "i", api_params->title); + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + if (api_params->hash_description && *api_params->hash_description) + rc_url_builder_append_str_param(&builder, "d", api_params->hash_description); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_add_game_hash_server_response(response, &response_obj); +} + +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response) { + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("GameID") + /* unused fields + RC_JSON_NEW_FIELD("MD5"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("Success") + */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[0], "GameID")) + return RC_MISSING_VALUE; + + return RC_OK; +} + +void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rapi/rc_api_info.c b/src/rcheevos/src/rapi/rc_api_info.c new file mode 100644 index 0000000000..84423ae0e2 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_info.c @@ -0,0 +1,582 @@ +#include "rc_api_info.h" +#include "rc_api_common.h" +#include "rc_api_runtime.h" + +#include "rc_runtime_types.h" + +#include "../rc_compat.h" + +#include +#include + +/* --- Fetch Achievement Info --- */ + +int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params) { + return rc_api_init_fetch_achievement_info_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_achievement_info_request_hosted(rc_api_request_t* request, + const rc_api_fetch_achievement_info_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementwondata", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + + if (api_params->friends_only) + rc_url_builder_append_unum_param(&builder, "f", 1); + if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + rc_url_builder_append_unum_param(&builder, "c", api_params->count); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_achievement_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_achievement_awarded_entry_t* entry; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("Response") + /* unused fields + RC_JSON_NEW_FIELD("Offset"), + RC_JSON_NEW_FIELD("Count"), + RC_JSON_NEW_FIELD("FriendsOnly") + * unused fields */ + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("NumEarned"), + RC_JSON_NEW_FIELD("TotalPlayers"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("RecentWinner") /* array */ + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("DateAwarded"), + RC_JSON_NEW_FIELD("AvatarUrl") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[2], "AchievementID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[3], "Response")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->num_awarded, &response->response, &response_fields[0], "NumEarned")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->num_players, &response->response, &response_fields[1], "TotalPlayers")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_array(&response->num_recently_awarded, &array_field, &response->response, &response_fields[3], "RecentWinner")) + return RC_MISSING_VALUE; + + if (response->num_recently_awarded) { + response->recently_awarded = (rc_api_achievement_awarded_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_recently_awarded * sizeof(rc_api_achievement_awarded_entry_t)); + if (!response->recently_awarded) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->recently_awarded; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[1], "DateAwarded")) + return RC_MISSING_VALUE; + entry->awarded = (time_t)timet; + + rc_json_get_optional_string(&entry->avatar_url, &response->response, &entry_fields[2], "AvatarUrl", NULL); + if (!entry->avatar_url) + entry->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, entry->username); + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Leaderboard Info --- */ + +int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params) { + return rc_api_init_fetch_leaderboard_info_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_leaderboard_info_request_hosted(rc_api_request_t* request, + const rc_api_fetch_leaderboard_info_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "lbinfo"); + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + + if (api_params->username) + rc_url_builder_append_str_param(&builder, "u", api_params->username); + else if (api_params->first_entry > 1) + rc_url_builder_append_unum_param(&builder, "o", api_params->first_entry - 1); /* number of entries to skip */ + + rc_url_builder_append_unum_param(&builder, "c", api_params->count); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_leaderboard_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_lboard_info_entry_t* entry; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; + int result; + size_t len; + char format[16]; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardData") + }; + + rc_json_field_t leaderboarddata_fields[] = { + RC_JSON_NEW_FIELD("LBID"), + RC_JSON_NEW_FIELD("LBFormat"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("LBTitle"), + RC_JSON_NEW_FIELD("LBDesc"), + RC_JSON_NEW_FIELD("LBMem"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("LBAuthor"), + RC_JSON_NEW_FIELD("LBCreated"), + RC_JSON_NEW_FIELD("LBUpdated"), + RC_JSON_NEW_FIELD("Entries"), /* array */ + RC_JSON_NEW_FIELD("TotalEntries") + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Index"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("DateSubmitted"), + RC_JSON_NEW_FIELD("AvatarUrl") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_object(leaderboarddata_fields, sizeof(leaderboarddata_fields) / sizeof(leaderboarddata_fields[0]), &response->response, &fields[2], "LeaderboardData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &leaderboarddata_fields[0], "LBID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->lower_is_better, &response->response, &leaderboarddata_fields[2], "LowerIsBetter")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &leaderboarddata_fields[3], "LBTitle")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->description, &response->response, &leaderboarddata_fields[4], "LBDesc")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->definition, &response->response, &leaderboarddata_fields[5], "LBMem")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->game_id, &response->response, &leaderboarddata_fields[6], "GameID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->author, &response->response, &leaderboarddata_fields[7], "LBAuthor")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->created, &response->response, &leaderboarddata_fields[8], "LBCreated")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_datetime(&response->updated, &response->response, &leaderboarddata_fields[9], "LBUpdated")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->total_entries, &response->response, &leaderboarddata_fields[11], "TotalEntries")) + return RC_MISSING_VALUE; + + if (!leaderboarddata_fields[1].value_end) + return RC_MISSING_VALUE; + len = leaderboarddata_fields[1].value_end - leaderboarddata_fields[1].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboarddata_fields[1].value_start + 1, len); + format[len] = '\0'; + response->format = rc_parse_format(format); + } + else { + response->format = RC_FORMAT_VALUE; + } + + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &leaderboarddata_fields[10], "Entries")) + return RC_MISSING_VALUE; + + if (response->num_entries) { + response->entries = (rc_api_lboard_info_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_lboard_info_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->entries; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->index, &response->response, &entry_fields[2], "Index")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[3], "Score")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[4], "DateSubmitted")) + return RC_MISSING_VALUE; + entry->submitted = (time_t)timet; + + rc_json_get_optional_string(&entry->avatar_url, &response->response, &entry_fields[5], "AvatarUrl", NULL); + if (!entry->avatar_url) + entry->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, entry->username); + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Games List --- */ + +int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params) { + return rc_api_init_fetch_games_list_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_games_list_request_hosted(rc_api_request_t* request, + const rc_api_fetch_games_list_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->console_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameslist"); + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_games_list_server_response(response, &response_obj); +} + +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_game_list_entry_t* entry; + rc_json_iterator_t iterator; + rc_json_field_t field; + int result; + char* end; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + rc_buffer_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); + + response->entries = (rc_api_game_list_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->id = strtol(field.name, &end, 10); + + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) + return RC_MISSING_VALUE; + + ++entry; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Game Titles --- */ + +int rc_api_init_fetch_game_titles_request(rc_api_request_t* request, const rc_api_fetch_game_titles_request_t* api_params) { + return rc_api_init_fetch_game_titles_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_titles_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_titles_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + char num[16]; + uint32_t i; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->num_game_ids == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameinfolist"); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_ids[0]); + + for (i = 1; i < api_params->num_game_ids; i++) { + int chars = snprintf(num, sizeof(num), "%u", api_params->game_ids[i]); + rc_url_builder_append(&builder, ",", 1); + rc_url_builder_append(&builder, num, chars); + } + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_game_titles_server_response(rc_api_fetch_game_titles_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_game_title_entry_t* entry; + rc_json_iterator_t iterator; + rc_json_field_t array_field; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ImageIcon") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + + if (response->num_entries) { + response->entries = (rc_api_game_title_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_title_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->entries; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&entry->id, &response->response, &entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&entry->title, &response->response, &entry_fields[1], "Title")) + return RC_MISSING_VALUE; + + /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ + rc_json_extract_filename(&entry_fields[2]); + if (!rc_json_get_required_string(&entry->image_name, &response->response, &entry_fields[2], "ImageIcon")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_titles_response(rc_api_fetch_game_titles_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Game Hashes --- */ + +int rc_api_init_fetch_hash_library_request(rc_api_request_t* request, + const rc_api_fetch_hash_library_request_t* api_params) +{ + return rc_api_init_fetch_hash_library_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_hash_library_request_hosted(rc_api_request_t* request, + const rc_api_fetch_hash_library_request_t* api_params, + const rc_api_host_t* host) +{ + rc_api_url_builder_t builder; + rc_api_url_build_dorequest_url(request, host); + + /* note: unauthenticated request */ + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "hashlibrary"); + if (api_params->console_id != 0) + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_fetch_hash_library_server_response(rc_api_fetch_hash_library_response_t* response, + const rc_api_server_response_t* server_response) +{ + rc_api_hash_library_entry_t* entry; + rc_json_iterator_t iterator; + rc_json_field_t field; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("MD5List"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = + rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "MD5List"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + if (response->num_entries > 0) { + rc_buffer_reserve(&response->response.buffer, response->num_entries * (33 + sizeof(rc_api_hash_library_entry_t))); + + response->entries = (rc_api_hash_library_entry_t*)rc_buffer_alloc( + &response->response.buffer, response->num_entries * sizeof(rc_api_hash_library_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->hash = rc_buffer_strncpy(&response->response.buffer, field.name, field.name_len); + + field.name = ""; + if (!rc_json_get_unum(&entry->game_id, &field, "")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_hash_library_response(rc_api_fetch_hash_library_response_t* response) +{ + rc_buffer_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rapi/rc_api_runtime.c b/src/rcheevos/src/rapi/rc_api_runtime.c new file mode 100644 index 0000000000..99a001db2f --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_runtime.c @@ -0,0 +1,901 @@ +#include "rc_api_runtime.h" +#include "rc_api_common.h" + +#include "rc_runtime.h" +#include "rc_runtime_types.h" +#include "../rc_compat.h" +#include "../rhash/md5.h" + +#include +#include +#include + +/* --- Resolve Hash --- */ + +int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) { + return rc_api_init_resolve_hash_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request, + const rc_api_resolve_hash_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (!api_params->game_hash || !*api_params->game_hash) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameid"); + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_resolve_hash_server_response(response, &response_obj); +} + +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("GameID") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID"); + return RC_OK; +} + +void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Game Data --- */ + +int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) { + return rc_api_init_fetch_game_data_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_data_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0 && (!api_params->game_hash || !api_params->game_hash[0])) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + else + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_game_data_server_response(response, &response_obj); +} + +static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) { + rc_json_iterator_t iterator; + const char* last_author = ""; + const char* last_author_field = ""; + size_t last_author_len = 0; + uint32_t timet; + size_t len; + + rc_json_field_t achievement_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Flags"), + RC_JSON_NEW_FIELD("Points"), + RC_JSON_NEW_FIELD("MemAddr"), + RC_JSON_NEW_FIELD("Author"), + RC_JSON_NEW_FIELD("BadgeName"), + RC_JSON_NEW_FIELD("Created"), + RC_JSON_NEW_FIELD("Modified"), + RC_JSON_NEW_FIELD("Type"), + RC_JSON_NEW_FIELD("Rarity"), + RC_JSON_NEW_FIELD("RarityHardcore"), + RC_JSON_NEW_FIELD("BadgeURL"), + RC_JSON_NEW_FIELD("BadgeLockedURL") + }; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field->value_start; + iterator.end = array_field->value_end; + + while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&achievement->id, response, &achievement_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->title, response, &achievement_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->description, response, &achievement_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->category, response, &achievement_fields[3], "Flags")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->points, response, &achievement_fields[4], "Points")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->definition, response, &achievement_fields[5], "MemAddr")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->badge_name, response, &achievement_fields[7], "BadgeName")) + return RC_MISSING_VALUE; + + rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", ""); + if (!achievement->badge_url[0]) + achievement->badge_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name); + + rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", ""); + if (!achievement->badge_locked_url[0]) + achievement->badge_locked_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name); + + len = achievement_fields[6].value_end - achievement_fields[6].value_start; + if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { + achievement->author = last_author; + } + else { + if (!rc_json_get_required_string(&achievement->author, response, &achievement_fields[6], "Author")) + return RC_MISSING_VALUE; + + if (achievement->author == NULL) { + /* ensure we don't pass NULL out to client */ + last_author = achievement->author = ""; + last_author_len = 0; + } else { + last_author = achievement->author; + last_author_field = achievement_fields[6].value_start; + last_author_len = len; + } + } + + if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created")) + return RC_MISSING_VALUE; + achievement->created = (time_t)timet; + if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified")) + return RC_MISSING_VALUE; + achievement->updated = (time_t)timet; + + if (rc_json_field_string_matches(&achievement_fields[10], "")) + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; + else if (rc_json_field_string_matches(&achievement_fields[10], "progression")) + achievement->type = RC_ACHIEVEMENT_TYPE_PROGRESSION; + else if (rc_json_field_string_matches(&achievement_fields[10], "missable")) + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + else if (rc_json_field_string_matches(&achievement_fields[10], "win_condition")) + achievement->type = RC_ACHIEVEMENT_TYPE_WIN; + else + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; + + /* legacy support : if title contains[m], change type to missable and remove[m] from title */ + if (memcmp(achievement->title, "[m]", 3) == 0) { + len = 3; + while (achievement->title[len] == ' ') + ++len; + achievement->title += len; + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + } + else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) { + len = strlen(achievement->title) - 3; + while (achievement->title[len - 1] == ' ') + --len; + ((char*)achievement->title)[len] = '\0'; + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + } + + rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0); + rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0); + + ++achievement; + } + + return RC_OK; +} + +static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) { + rc_json_iterator_t iterator; + size_t len; + int result; + char format[16]; + + rc_json_field_t leaderboard_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Mem"), + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("Hidden") + }; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field->value_start; + iterator.end = array_field->value_end; + + while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&leaderboard->id, response, &leaderboard_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->title, response, &leaderboard_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->description, response, &leaderboard_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->definition, response, &leaderboard_fields[3], "Mem")) + return RC_MISSING_VALUE; + rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0); + leaderboard->lower_is_better = (uint8_t)result; + rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0); + leaderboard->hidden = (uint8_t)result; + + if (!leaderboard_fields[4].value_end) + return RC_MISSING_VALUE; + len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboard_fields[4].value_start + 1, len); + format[len] = '\0'; + leaderboard->format = rc_parse_format(format); + } + else { + leaderboard->format = RC_FORMAT_VALUE; + } + + ++leaderboard; + } + + return RC_OK; +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + size_t len; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), + RC_JSON_NEW_FIELD("PatchData") /* nested object */ + }; + + rc_json_field_t patchdata_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("ImageIconURL"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards"), /* array */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[3], "PatchData")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID")) + return RC_MISSING_VALUE; + + /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ + rc_json_extract_filename(&patchdata_fields[3]); + rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", ""); + rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", ""); + if (!response->image_url[0]) + response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name); + + /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards. + determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) + and add space for the structures. */ + len = patchdata_fields[5].value_end - patchdata_fields[5].value_start; /* rich presence */ + + len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* achievements */ + patchdata_fields[6].array_size * (80 - sizeof(rc_api_achievement_definition_t)); + + len += (patchdata_fields[7].value_end - patchdata_fields[7].value_start) - /* leaderboards */ + patchdata_fields[7].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); + + rc_buffer_reserve(&response->response.buffer, len); + /* end estimation */ + + rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[5], "RichPresencePatch", ""); + if (!response->rich_presence_script) + response->rich_presence_script = ""; + + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[6], "Achievements")) + return RC_MISSING_VALUE; + + if (response->num_achievements) { + response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t)); + if (!response->achievements) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_achievements(&response->response, response->achievements, &array_field); + if (result != RC_OK) + return result; + } + + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[7], "Leaderboards")) + return RC_MISSING_VALUE; + + if (response->num_leaderboards) { + response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + if (!response->leaderboards) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_leaderboards(&response->response, response->leaderboards, &array_field); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Game Sets --- */ + +int rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params) { + return rc_api_init_fetch_game_sets_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_sets_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (!api_params->game_id && (!api_params->game_hash || !api_params->game_hash[0])) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementsets", api_params->username, api_params->api_token)) { + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + else + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +static int rc_api_process_fetch_game_sets_achievement_sets(rc_api_fetch_game_sets_response_t* response, + rc_api_achievement_set_definition_t* subset, + rc_json_field_t* subset_array_field) { + rc_json_iterator_t iterator; + rc_json_field_t array_field; + size_t len; + int result; + + rc_json_field_t subset_fields[] = { + RC_JSON_NEW_FIELD("AchievementSetId"), + RC_JSON_NEW_FIELD("GameId"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Type"), + RC_JSON_NEW_FIELD("ImageIconUrl"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ + }; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = subset_array_field->value_start; + iterator.end = subset_array_field->value_end; + + while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "AchievementSetId")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&subset->game_id, &response->response, &subset_fields[1], "GameId")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[2], "Title")) + return RC_MISSING_VALUE; + if (!subset->title || !subset->title[0]) + subset->title = response->title; + + if (rc_json_field_string_matches(&subset_fields[3], "core")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_CORE; + else if (rc_json_field_string_matches(&subset_fields[3], "bonus")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS; + else if (rc_json_field_string_matches(&subset_fields[3], "specialty")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_SPECIALTY; + else if (rc_json_field_string_matches(&subset_fields[3], "exclusive")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE; + else + subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS; + + if (rc_json_field_string_matches(&subset_fields[4], response->image_url)) { + subset->image_url = response->image_url; + subset->image_name = response->image_name; + } + else { + if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[4], "ImageIconUrl")) + return RC_MISSING_VALUE; + rc_json_extract_filename(&subset_fields[4]); + rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[4], "ImageIconUrl", ""); + } + + /* estimate the amount of space necessary to store the achievements, and leaderboards. + determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) + and add space for the structures. */ + len = (subset_fields[5].value_end - subset_fields[5].value_start) - /* achievements */ + subset_fields[5].array_size * (80 - sizeof(rc_api_achievement_definition_t)); + len += (subset_fields[6].value_end - subset_fields[6].value_start) - /* leaderboards */ + subset_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); + + rc_buffer_reserve(&response->response.buffer, len); + /* end estimation */ + + if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[5], "Achievements")) + return RC_MISSING_VALUE; + + if (subset->num_achievements) { + subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t)); + if (!subset->achievements) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_achievements(&response->response, subset->achievements, &array_field); + if (result != RC_OK) + return result; + } + + if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[6], "Leaderboards")) + return RC_MISSING_VALUE; + + if (subset->num_leaderboards) { + subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + if (!subset->leaderboards) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_leaderboards(&response->response, subset->leaderboards, &array_field); + if (result != RC_OK) + return result; + } + + ++subset; + } + + return RC_OK; +} + +int rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), + RC_JSON_NEW_FIELD("GameId"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleId"), + RC_JSON_NEW_FIELD("ImageIconUrl"), + RC_JSON_NEW_FIELD("RichPresenceGameId"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Sets") /* array */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[3], "GameId")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &fields[4], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->console_id, &response->response, &fields[5], "ConsoleId")) + return RC_MISSING_VALUE; + + rc_json_get_required_string(&response->image_url, &response->response, &fields[6], "ImageIconUrl"); + rc_json_extract_filename(&fields[6]); + rc_json_get_required_string(&response->image_name, &response->response, &fields[6], "ImageIconUrl"); + + rc_json_get_optional_unum(&response->session_game_id, &fields[7], "RichPresenceGameId", response->id); + + rc_json_get_optional_string(&response->rich_presence_script, &response->response, &fields[8], "RichPresencePatch", ""); + if (!response->rich_presence_script) + response->rich_presence_script = ""; + + rc_json_get_optional_array(&response->num_sets, &array_field, &fields[9], "Sets"); + if (response->num_sets) { + response->sets = (rc_api_achievement_set_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_sets * sizeof(rc_api_achievement_set_definition_t)); + if (!response->sets) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_sets_achievement_sets(response, response->sets, &array_field); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Ping --- */ + +int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) { + return rc_api_init_ping_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_ping_request_hosted(rc_api_request_t* request, + const rc_api_ping_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + if (api_params->rich_presence && *api_params->rich_presence) + rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); + + if (api_params->game_hash && *api_params->game_hash) { + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore); + rc_url_builder_append_str_param(&builder, "x", api_params->game_hash); + } + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_ping_server_response(response, &response_obj); +} + +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); +} + +void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Award Achievement --- */ + +int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) { + return rc_api_init_award_achievement_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_award_achievement_request_hosted(rc_api_request_t* request, + const rc_api_award_achievement_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t digest[16]; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->achievement_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 96); + if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id); + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); + if (api_params->game_hash && *api_params->game_hash) + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + if (api_params->seconds_since_unlock) + rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_unlock); + + /* Evaluate the signature. */ + md5_init(&md5); + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + if (api_params->seconds_since_unlock) { + /* second achievement id is needed by delegated unlock. including it here allows overloading + * the hash generating code on the server */ + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_unlock); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + } + md5_finish(&md5, digest); + rc_format_md5(buffer, digest); + rc_url_builder_append_str_param(&builder, "v", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_award_achievement_server_response(response, &response_obj); +} + +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("AchievementsRemaining") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!response->response.succeeded) { + if (response->response.error_message && + memcmp(response->response.error_message, "User already has", 16) == 0) { + /* not really an error, the achievement is unlocked, just not by the current call. + * hardcore: User already has hardcore and regular achievements awarded. + * non-hardcore: User already has this achievement awarded. + */ + response->response.succeeded = 1; + } else { + return result; + } + } + + rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); + rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1); + + return RC_OK; +} + +void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Submit Leaderboard Entry --- */ + +int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) { + return rc_api_init_submit_lboard_entry_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request, + const rc_api_submit_lboard_entry_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + char buffer[33]; + md5_state_t md5; + md5_byte_t digest[16]; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->leaderboard_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 96); + if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id); + rc_url_builder_append_num_param(&builder, "s", api_params->score); + + if (api_params->game_hash && *api_params->game_hash) + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + if (api_params->seconds_since_completion) + rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_completion); + + /* Evaluate the signature. */ + md5_init(&md5); + snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); + snprintf(buffer, sizeof(buffer), "%d", api_params->score); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + if (api_params->seconds_since_completion) { + snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_completion); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + } + md5_finish(&md5, digest); + rc_format_md5(buffer, digest); + rc_url_builder_append_str_param(&builder, "v", buffer); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_submit_lboard_entry_server_response(response, &response_obj); +} + +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_lboard_entry_t* entry; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + const char* str; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") /* nested object */ + }; + + rc_json_field_t response_fields[] = { + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("BestScore"), + RC_JSON_NEW_FIELD("RankInfo"), /* nested object */ + RC_JSON_NEW_FIELD("TopEntries") /* array */ + /* unused fields + RC_JSON_NEW_FIELD("LBData"), / * array * / + RC_JSON_NEW_FIELD("ScoreFormatted"), + RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * / + * unused fields */ + }; + + /* unused fields + rc_json_field_t lbdata_fields[] = { + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LeaderboardID"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("LowerIsBetter") + }; + * unused fields */ + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Score") + /* unused fields + RC_JSON_NEW_FIELD("DateSubmitted") + * unused fields */ + }; + + rc_json_field_t rank_info_fields[] = { + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("NumEntries") + /* unused fields + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("UserRank") + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries")) + return RC_MISSING_VALUE; + response->num_entries = (unsigned)atoi(str); + + if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries")) + return RC_MISSING_VALUE; + + if (response->num_top_entries) { + response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t)); + if (!response->top_entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->top_entries; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rapi/rc_api_user.c b/src/rcheevos/src/rapi/rc_api_user.c new file mode 100644 index 0000000000..323756cfd1 --- /dev/null +++ b/src/rcheevos/src/rapi/rc_api_user.c @@ -0,0 +1,483 @@ +#include "rc_api_user.h" +#include "rc_api_common.h" +#include "rc_api_runtime.h" +#include "rc_consoles.h" + +#include "../rc_version.h" + +#include +#include + +/* --- Login --- */ + +int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params) { + return rc_api_init_login_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_login_request_hosted(rc_api_request_t* request, + const rc_api_login_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (!api_params->username || !*api_params->username) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "login2"); + rc_url_builder_append_str_param(&builder, "u", api_params->username); + + if (api_params->password && api_params->password[0]) + rc_url_builder_append_str_param(&builder, "p", api_params->password); + else if (api_params->api_token && api_params->api_token[0]) + rc_url_builder_append_str_param(&builder, "t", api_params->api_token); + else + return RC_INVALID_STATE; + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_login_server_response(response, &response_obj); +} + +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Token"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), + RC_JSON_NEW_FIELD("Messages"), + RC_JSON_NEW_FIELD("AvatarUrl") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_string(&response->username, &response->response, &fields[3], "User")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->api_token, &response->response, &fields[4], "Token")) + return RC_MISSING_VALUE; + + rc_json_get_optional_unum(&response->score, &fields[5], "Score", 0); + rc_json_get_optional_unum(&response->score_softcore, &fields[6], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[7], "Messages", 0); + + /* For the highest level of backwards compatibility, we have decided to just send the + * display_name back to the client as the "case-corrected username", and allow it to + * be used as the Username field for the other APIs. */ + response->display_name = response->username; + + rc_json_get_optional_string(&response->avatar_url, &response->response, &fields[8], "AvatarUrl", NULL); + if (!response->avatar_url) + response->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, response->username); + + return RC_OK; +} + +void rc_api_destroy_login_response(rc_api_login_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Start Session --- */ + +int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params) { + return rc_api_init_start_session_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_start_session_request_hosted(rc_api_request_t* request, + const rc_api_start_session_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->game_id == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "startsession", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + + if (api_params->game_hash && *api_params->game_hash) { + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore); + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + } + + rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_start_session_server_response(response, &response_obj); +} + +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { + rc_api_unlock_entry_t* unlock; + rc_json_field_t array_field; + rc_json_iterator_t iterator; + uint32_t timet; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Unlocks"), + RC_JSON_NEW_FIELD("HardcoreUnlocks"), + RC_JSON_NEW_FIELD("ServerNow") + }; + + rc_json_field_t unlock_entry_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("When") + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (rc_json_get_optional_array(&response->num_unlocks, &array_field, &fields[2], "Unlocks") && response->num_unlocks) { + response->unlocks = (rc_api_unlock_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + if (rc_json_get_optional_array(&response->num_hardcore_unlocks, &array_field, &fields[3], "HardcoreUnlocks") && response->num_hardcore_unlocks) { + response->hardcore_unlocks = (rc_api_unlock_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_hardcore_unlocks * sizeof(rc_api_unlock_entry_t)); + if (!response->hardcore_unlocks) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + unlock = response->hardcore_unlocks; + while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + return RC_MISSING_VALUE; + unlock->when = (time_t)timet; + + ++unlock; + } + } + + rc_json_get_optional_unum(&timet, &fields[4], "ServerNow", 0); + response->server_now = (time_t)timet; + + return RC_OK; +} + +void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch User Unlocks --- */ + +int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params) { + return rc_api_init_fetch_user_unlocks_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_user_unlocks_request_hosted(rc_api_request_t* request, + const rc_api_fetch_user_unlocks_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "unlocks", api_params->username, api_params->api_token)) { + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_user_unlocks_server_response(response, &response_obj); +} + +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response) { + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("UserUnlocks") + /* unused fields + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("HardcoreMode") + * unused fields */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + result = rc_json_get_required_unum_array(&response->achievement_ids, &response->num_achievement_ids, &response->response, &fields[2], "UserUnlocks"); + return result; +} + +void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Followed Users --- */ + +int rc_api_init_fetch_followed_users_request(rc_api_request_t* request, const rc_api_fetch_followed_users_request_t* api_params) { + return rc_api_init_fetch_followed_users_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_followed_users_request_hosted(rc_api_request_t* request, + const rc_api_fetch_followed_users_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "getfriendlist", api_params->username, api_params->api_token)) { + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_users_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; + rc_api_followed_user_t* user; + uint32_t timet; + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Friends") + }; + + rc_json_field_t followed_user_entry_fields[] = { + RC_JSON_NEW_FIELD("Friend"), + RC_JSON_NEW_FIELD("AvatarUrl"), + RC_JSON_NEW_FIELD("RAPoints"), + RC_JSON_NEW_FIELD("LastSeen"), + RC_JSON_NEW_FIELD("LastSeenTime"), + RC_JSON_NEW_FIELD("LastGameId"), + RC_JSON_NEW_FIELD("LastGameTitle"), + RC_JSON_NEW_FIELD("LastGameIconUrl"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_array(&response->num_users, &array_field, &response->response, &fields[2], "Friends")) + return RC_MISSING_VALUE; + + if (response->num_users) { + response->users = (rc_api_followed_user_t*)rc_buffer_alloc(&response->response.buffer, response->num_users * sizeof(rc_api_followed_user_t)); + if (!response->users) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + user = response->users; + while (rc_json_get_array_entry_object(followed_user_entry_fields, sizeof(followed_user_entry_fields) / sizeof(followed_user_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&user->display_name, &response->response, &followed_user_entry_fields[0], "Friend")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&user->avatar_url, &response->response, &followed_user_entry_fields[1], "AvatarUrl")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&user->score, &response->response, &followed_user_entry_fields[2], "RAPoints")) + return RC_MISSING_VALUE; + + rc_json_get_optional_string(&user->recent_activity.description, &response->response, &followed_user_entry_fields[3], "LastSeen", NULL); + + rc_json_get_optional_unum(&timet, &followed_user_entry_fields[4], "LastSeenTime", 0); + user->recent_activity.when = (time_t)timet; + + rc_json_get_optional_unum(&user->recent_activity.context_id, &followed_user_entry_fields[5], "LastGameId", 0); + rc_json_get_optional_string(&user->recent_activity.context, &response->response, &followed_user_entry_fields[6], "LastGameTitle", NULL); + rc_json_get_optional_string(&user->recent_activity.context_image_url, &response->response, &followed_user_entry_fields[7], "LastGameIconUrl", NULL); + + ++user; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_followed_users_response(rc_api_fetch_followed_users_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch All Progress --- */ + +int rc_api_init_fetch_all_user_progress_request(rc_api_request_t* request, + const rc_api_fetch_all_user_progress_request_t* api_params) +{ + return rc_api_init_fetch_all_user_progress_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_all_user_progress_request_hosted(rc_api_request_t* request, + const rc_api_fetch_all_user_progress_request_t* api_params, + const rc_api_host_t* host) +{ + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->console_id == RC_CONSOLE_UNKNOWN) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "allprogress", api_params->username, api_params->api_token)) + { + rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_all_user_progress_server_response(rc_api_fetch_all_user_progress_response_t* response, + const rc_api_server_response_t* server_response) +{ + rc_api_all_user_progress_entry_t* entry; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response"), + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("Achievements"), + RC_JSON_NEW_FIELD("Unlocked"), + RC_JSON_NEW_FIELD("UnlockedHardcore"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = + rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + + if (response->num_entries > 0) { + rc_json_iterator_t iterator; + rc_json_field_t field; + char* end; + + rc_buffer_reserve(&response->response.buffer, response->num_entries * sizeof(rc_api_all_user_progress_entry_t)); + + response->entries = (rc_api_all_user_progress_entry_t*)rc_buffer_alloc( + &response->response.buffer, response->num_entries * sizeof(rc_api_all_user_progress_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator, &field)) + { + entry->game_id = strtol(field.name, &end, 10); + + field.name = ""; + if (!rc_json_get_required_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), + &response->response, &field, "")) + { + return RC_MISSING_VALUE; + } + + rc_json_get_optional_unum(&entry->num_achievements, &entry_fields[0], "Achievements", 0); + rc_json_get_optional_unum(&entry->num_unlocked_achievements, &entry_fields[1], "Unlocked", 0); + rc_json_get_optional_unum(&entry->num_unlocked_achievements_hardcore, &entry_fields[2], "UnlockedHardcore", 0); + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_all_user_progress_response(rc_api_fetch_all_user_progress_response_t* response) +{ + rc_buffer_destroy(&response->response.buffer); +} diff --git a/src/rcheevos/src/rc_client.c b/src/rcheevos/src/rc_client.c new file mode 100644 index 0000000000..22a4ade167 --- /dev/null +++ b/src/rcheevos/src/rc_client.c @@ -0,0 +1,6775 @@ +#include "rc_client_internal.h" + +#include "rc_api_info.h" +#include "rc_api_runtime.h" +#include "rc_api_user.h" +#include "rc_consoles.h" +#include "rc_hash.h" +#include "rc_version.h" + +#include "rapi/rc_api_common.h" + +#include "rcheevos/rc_internal.h" + +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif + +#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 +#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ + +#define RC_MINIMUM_UNPAUSED_FRAMES 20 +#define RC_PAUSE_DECAY_MULTIPLIER 4 + +enum { + RC_CLIENT_ASYNC_NOT_ABORTED = 0, + RC_CLIENT_ASYNC_ABORTED = 1, + RC_CLIENT_ASYNC_DESTROYED = 2 +}; + +typedef struct rc_client_generic_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_generic_callback_data_t; + +typedef struct rc_client_pending_media_t +{ +#ifdef RC_CLIENT_SUPPORTS_HASH + const char* file_path; + uint8_t* data; + size_t data_size; +#endif + const char* hash; + rc_client_callback_t callback; + void* callback_userdata; +} rc_client_pending_media_t; + +typedef struct rc_client_load_state_t +{ + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + + rc_client_game_info_t* game; + rc_client_subset_info_t* subset; + rc_client_game_hash_t* hash; + +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_iterator_t hash_iterator; + rc_client_game_hash_t* tried_hashes[4]; +#endif + rc_client_pending_media_t* pending_media; + + rc_api_start_session_response_t *start_session_response; + + rc_client_async_handle_t async_handle; + + uint8_t progress; + uint8_t outstanding_requests; +#ifdef RC_CLIENT_SUPPORTS_HASH + uint8_t hash_console_id; +#endif +} rc_client_load_state_t; + +static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state); +static void rc_client_begin_fetch_game_sets(rc_client_load_state_t* callback_data); +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when); +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id); +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +/* ===== natvis extensions ===== */ + +typedef struct __rc_client_achievement_state_enum_t { uint8_t value; } __rc_client_achievement_state_enum_t; +typedef struct __rc_client_achievement_category_enum_t { uint8_t value; } __rc_client_achievement_category_enum_t; +typedef struct __rc_client_achievement_type_enum_t { uint8_t value; } __rc_client_achievement_type_enum_t; +typedef struct __rc_client_achievement_bucket_enum_t { uint8_t value; } __rc_client_achievement_bucket_enum_t; +typedef struct __rc_client_achievement_unlocked_enum_t { uint8_t value; } __rc_client_achievement_unlocked_enum_t; +typedef struct __rc_client_leaderboard_state_enum_t { uint8_t value; } __rc_client_leaderboard_state_enum_t; +typedef struct __rc_client_leaderboard_format_enum_t { uint8_t value; } __rc_client_leaderboard_format_enum_t; +typedef struct __rc_client_log_level_enum_t { uint8_t value; } __rc_client_log_level_enum_t; +typedef struct __rc_client_event_type_enum_t { uint8_t value; } __rc_client_event_type_enum_t; +typedef struct __rc_client_load_game_state_enum_t { uint8_t value; } __rc_client_load_game_state_enum_t; +typedef struct __rc_client_user_state_enum_t { uint8_t value; } __rc_client_user_state_enum_t; +typedef struct __rc_client_mastery_state_enum_t { uint8_t value; } __rc_client_mastery_state_enum_t; +typedef struct __rc_client_spectator_mode_enum_t { uint8_t value; } __rc_client_spectator_mode_enum_t; +typedef struct __rc_client_disconnect_enum_t { uint8_t value; } __rc_client_disconnect_enum_t; +typedef struct __rc_client_leaderboard_tracker_list_t { rc_client_leaderboard_tracker_info_t* first; } __rc_client_leaderboard_tracker_list_t; +typedef struct __rc_client_subset_info_list_t { rc_client_subset_info_t* first; } __rc_client_subset_info_list_t; +typedef struct __rc_client_media_hash_list_t { rc_client_media_hash_t* first; } __rc_client_media_hash_list_t; +typedef struct __rc_client_subset_info_achievements_list_t { rc_client_subset_info_t info; } __rc_client_subset_info_achievements_list_t; +typedef struct __rc_client_subset_info_leaderboards_list_t { rc_client_subset_info_t info; } __rc_client_subset_info_leaderboards_list_t; +typedef struct __rc_client_scheduled_callback_list_t { rc_client_state_t state; } __rc_client_scheduled_callback_list_t; +typedef struct __rc_client_game_hash_list_t { rc_client_t client; } __rc_client_game_hash_list_t; + +static void rc_client_natvis_helper(const rc_client_event_t* event, rc_client_t* client) +{ + struct natvis_extensions { + __rc_client_achievement_state_enum_t achievement_state; + __rc_client_achievement_category_enum_t achievement_category; + __rc_client_achievement_type_enum_t achievement_type; + __rc_client_achievement_bucket_enum_t achievement_bucket; + __rc_client_achievement_unlocked_enum_t achievement_unlocked; + __rc_client_leaderboard_state_enum_t leaderboard_state; + __rc_client_leaderboard_format_enum_t leaderboard_format; + __rc_client_log_level_enum_t log_level; + __rc_client_event_type_enum_t event_type; + __rc_client_load_game_state_enum_t load_game_state; + __rc_client_user_state_enum_t user_state; + __rc_client_mastery_state_enum_t mastery_state; + __rc_client_spectator_mode_enum_t spectator_mode; + __rc_client_disconnect_enum_t disconnect; + __rc_client_leaderboard_tracker_list_t leaderboard_tracker_list; + __rc_client_subset_info_list_t subset_info_list; + __rc_client_media_hash_list_t media_hash_list; + __rc_client_subset_info_achievements_list_t subset_info_achievements_list; + __rc_client_subset_info_leaderboards_list_t subset_info_leaderboards_list; + __rc_client_scheduled_callback_list_t scheduled_callback_list; + __rc_client_game_hash_list_t client_game_hash_list; + } natvis; + + memset(&natvis, 0, sizeof(natvis)); + (void)event; + (void)client; + + /* this code should never be executed. it just ensures these constants get defined for + * the natvis VisualStudio extension as they're not used directly in the code. */ + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD; + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE; + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION; + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_WIN; + natvis.achievement_category.value = RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE; + natvis.event_type.value = RC_CLIENT_EVENT_TYPE_NONE; +} + +/* ===== Construction/Destruction ===== */ + +static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ + (void)event; + (void)client; +} + +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function) +{ + rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t)); + if (!client) + return NULL; + + client->state.hardcore = 1; + client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + client->state.allow_background_memory_reads = 1; + + client->callbacks.read_memory = read_memory_function; + client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_natvis_helper; + client->callbacks.event_handler = rc_client_dummy_event_handler; + rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); + rc_client_set_get_time_millisecs_function(client, NULL); + + rc_mutex_init(&client->state.mutex); + + rc_buffer_init(&client->state.buffer); + + return client; +} + +void rc_client_destroy(rc_client_t* client) +{ + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + { + size_t i; + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i]) + client->state.async_handles[i]->aborted = RC_CLIENT_ASYNC_DESTROYED; + } + + if (client->state.load) { + client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_DESTROYED; + client->state.load = NULL; + } + } + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + rc_client_unload_raintegration(client); +#endif + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->destroy) + client->state.external_client->destroy(); +#endif + + rc_buffer_destroy(&client->state.buffer); + + rc_mutex_destroy(&client->state.mutex); + + free(client); +} + +/* ===== Logging ===== */ + +void rc_client_log_message(const rc_client_t* client, const char* message) +{ + if (client->callbacks.log_call) + client->callbacks.log_call(message, client); +} + +static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args) +{ + if (client->callbacks.log_call) { + char buffer[2048]; + +#ifdef __STDC_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + vsprintf(buffer, format, args); +#endif + + client->callbacks.log_call(buffer, client); + } +} + +#ifdef RC_NO_VARIADIC_MACROS + +void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +#else + +void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...) +{ + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); +} + +#endif /* RC_NO_VARIADIC_MACROS */ + +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + client->callbacks.log_call = callback; + client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->enable_logging) + client->state.external_client->enable_logging(client, level, callback); +#endif +} + +/* ===== Common ===== */ + +static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec now; + (void)client; + + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + + /* round nanoseconds to nearest millisecond and add to seconds */ + return ((rc_clock_t)now.tv_sec * 1000 + ((rc_clock_t)now.tv_nsec / 1000000)); +#elif defined(_WIN32) + static LARGE_INTEGER freq; + LARGE_INTEGER ticks; + + (void)client; + + /* Frequency is the number of ticks per second and is guaranteed to not change. */ + if (!freq.QuadPart) { + if (!QueryPerformanceFrequency(&freq)) + return 0; + + /* convert to number of ticks per millisecond to simplify later calculations */ + freq.QuadPart /= 1000; + } + + if (!QueryPerformanceCounter(&ticks)) + return 0; + + return (rc_clock_t)(ticks.QuadPart / freq.QuadPart); +#else + const clock_t clock_now = clock(); + + (void)client; + + if (sizeof(clock_t) == 4) { + static uint32_t clock_wraps = 0; + static clock_t last_clock = 0; + static time_t last_timet = 0; + const time_t time_now = time(NULL); + + if (last_timet != 0) { + const time_t seconds_per_clock_t = (time_t)(((uint64_t)1 << 32) / CLOCKS_PER_SEC); + if (clock_now < last_clock) { + /* clock() has wrapped */ + ++clock_wraps; + } + else if (time_now - last_timet > seconds_per_clock_t) { + /* it's been long enough that clock() has wrapped and is higher than the last time it was read */ + ++clock_wraps; + } + } + + last_timet = time_now; + last_clock = clock_now; + + return (rc_clock_t)((((uint64_t)clock_wraps << 32) | clock_now) / (CLOCKS_PER_SEC / 1000)); + } + else { + return (rc_clock_t)(clock_now / (CLOCKS_PER_SEC / 1000)); + } +#endif +} + +void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler) +{ + client->callbacks.get_time_millisecs = handler ? handler : rc_client_clock_get_now_millisecs; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_get_time_millisecs) + client->state.external_client->set_get_time_millisecs(client, handler); +#endif +} + +int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted; + + rc_mutex_lock(&client->state.mutex); + aborted = async_handle->aborted; + rc_mutex_unlock(&client->state.mutex); + + return aborted; +} + +static void rc_client_begin_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + size_t i; + + rc_mutex_lock(&client->state.mutex); + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (!client->state.async_handles[i]) { + client->state.async_handles[i] = async_handle; + break; + } + } + rc_mutex_unlock(&client->state.mutex); +} + +static int rc_client_end_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted = async_handle->aborted; + + /* if client was destroyed, mutex doesn't exist and we don't need to remove the handle from the collection */ + if (aborted != RC_CLIENT_ASYNC_DESTROYED) { + size_t i; + + rc_mutex_lock(&client->state.mutex); + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i] == async_handle) { + client->state.async_handles[i] = NULL; + break; + } + } + aborted = async_handle->aborted; + + rc_mutex_unlock(&client->state.mutex); + } + + return aborted; +} + +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + if (async_handle && client) { +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->abort_async) { + client->state.external_client->abort_async(async_handle); + return; + } +#endif + + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = RC_CLIENT_ASYNC_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } +} + +static int rc_client_async_handle_valid(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int valid = 0; + size_t i; + + /* there is a small window of opportunity where the client could have been destroyed before calling + * this function, but this function assumes the possibility that the handle has been destroyed, so + * we can't check it for RC_CLIENT_ASYNC_DESTROYED before attempting to scan the client data */ + rc_mutex_lock(&client->state.mutex); + + for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) { + if (client->state.async_handles[i] == async_handle) { + valid = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return valid; +} + +static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) +{ + if (!response->succeeded) { + if (*result == RC_OK) { + *result = RC_API_FAILURE; + if (!response->error_message) + return "Unexpected API failure with no error message"; + } + + if (response->error_message) + return response->error_message; + } + + (void)http_status_code; + + if (*result != RC_OK) + return rc_error_str(*result); + + return NULL; +} + +static void rc_client_raise_server_error_event(rc_client_t* client, + const char* api, uint32_t related_id, int result, const char* error_message) +{ + rc_client_server_error_t server_error; + rc_client_event_t client_event; + + server_error.api = api; + server_error.error_message = error_message; + server_error.result = result; + server_error.related_id = related_id; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_SERVER_ERROR; + client_event.server_error = &server_error; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_update_disconnect_state(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + uint8_t new_state = RC_CLIENT_DISCONNECT_HIDDEN; + + rc_mutex_lock(&client->state.mutex); + + scheduled_callback = client->state.scheduled_callbacks; + for (; scheduled_callback; scheduled_callback = scheduled_callback->next) { + if (scheduled_callback->callback == rc_client_award_achievement_retry || + scheduled_callback->callback == rc_client_submit_leaderboard_entry_retry) { + new_state = RC_CLIENT_DISCONNECT_VISIBLE; + break; + } + } + + if ((client->state.disconnect & RC_CLIENT_DISCONNECT_VISIBLE) != new_state) { + if (new_state == RC_CLIENT_DISCONNECT_VISIBLE) + client->state.disconnect = RC_CLIENT_DISCONNECT_HIDDEN | RC_CLIENT_DISCONNECT_SHOW_PENDING; + else + client->state.disconnect = RC_CLIENT_DISCONNECT_VISIBLE | RC_CLIENT_DISCONNECT_HIDE_PENDING; + } + else { + client->state.disconnect = new_state; + } + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_raise_disconnect_events(rc_client_t* client) +{ + rc_client_event_t client_event; + uint8_t new_state; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.disconnect & RC_CLIENT_DISCONNECT_SHOW_PENDING) + new_state = RC_CLIENT_DISCONNECT_VISIBLE; + else + new_state = RC_CLIENT_DISCONNECT_HIDDEN; + client->state.disconnect = new_state; + + rc_mutex_unlock(&client->state.mutex); + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = (new_state == RC_CLIENT_DISCONNECT_VISIBLE) ? + RC_CLIENT_EVENT_DISCONNECTED : RC_CLIENT_EVENT_RECONNECTED; + client->callbacks.event_handler(&client_event, client); +} + +static int rc_client_should_retry(const rc_api_server_response_t* server_response) +{ + switch (server_response->http_status_code) { + case 502: /* 502 Bad Gateway */ + /* nginx connection pool full */ + return 1; + + case 503: /* 503 Service Temporarily Unavailable */ + /* site is in maintenance mode */ + return 1; + + case 504: /* 504 Gateway Timeout */ + /* timeout between web server and database server */ + return 1; + + case 429: /* 429 Too Many Requests */ + /* too many unlocks occurred at the same time */ + return 1; + + case 521: /* 521 Web Server is Down */ + /* cloudfare could not find the server */ + return 1; + + case 522: /* 522 Connection Timed Out */ + /* timeout connecting to server from cloudfare */ + return 1; + + case 523: /* 523 Origin is Unreachable */ + /* cloudfare cannot find server */ + return 1; + + case 524: /* 524 A Timeout Occurred */ + /* connection to server from cloudfare was dropped before request was completed */ + return 1; + + case 525: /* 525 SSL Handshake Failed */ + /* web server worker connection pool is exhausted */ + return 1; + + case RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR: + /* client provided non-HTTP error (explicitly retryable) */ + return 1; + + case RC_API_SERVER_RESPONSE_CLIENT_ERROR: + /* client provided non-HTTP error (implicitly non-retryable) */ + return 0; + + default: + /* assume any error not handled above where no response was received should be retried */ + if (server_response->body_length == 0 || !server_response->body || !server_response->body[0]) + return 1; + + return 0; + } +} + +static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + if (!buffer) + return RC_INVALID_STATE; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + result = rc_api_init_fetch_image_request_hosted(&request, &image_request, NULL); + if (result == RC_OK) + snprintf(buffer, buffer_size, "%s", request.url); + + rc_api_destroy_request(&request); + return result; +} + +/* ===== User ===== */ + +static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data; + rc_client_t* client = login_callback_data->client; + rc_api_login_response_t login_response; + rc_client_load_state_t* load_state; + const char* error_message; + int result; + + result = rc_client_end_async(client, &login_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + rc_client_logout(client); /* logout will reset the user state and call the load game callback */ + + free(login_callback_data); + return; + } + + if (client->state.user == RC_CLIENT_USER_STATE_NONE) { + /* logout was called */ + if (login_callback_data->callback) + login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata); + + free(login_callback_data); + /* logout call will immediately abort load game before this callback gets called */ + return; + } + + result = rc_api_process_login_server_response(&login_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response); + if (error_message) { + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_NONE; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message); + if (login_callback_data->callback) + login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_sets(load_state); + } + else { + client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username); + + if (strcmp(login_response.username, login_response.display_name) == 0) + client->user.display_name = client->user.username; + else + client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name); + + client->user.avatar_url = rc_buffer_strcpy(&client->state.buffer, login_response.avatar_url); + client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token); + client->user.score = login_response.score; + client->user.score_softcore = login_response.score_softcore; + client->user.num_unread_messages = login_response.num_unread_messages; + + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_sets(load_state); + + if (login_callback_data->callback) + login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); + } + + rc_api_destroy_login_response(&login_response); + free(login_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, + const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_generic_callback_data_t* callback_data; + rc_api_request_t request; + int result = rc_api_init_login_request_hosted(&request, login_request, &client->state.host); + const char* error_message = rc_error_str(result); + + if (result == RC_OK) { + rc_mutex_lock(&client->state.mutex); + + if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) { + error_message = "Login already in progress"; + result = RC_INVALID_STATE; + } + client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + rc_mutex_unlock(&client->state.mutex); + } + + if (result != RC_OK) { + callback(result, error_message, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + rc_client_begin_async(client, &callback_data->async_handle); + client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client); + + rc_api_destroy_request(&request); + + /* if the user state has changed, the async operation completed synchronously */ + rc_mutex_lock(&client->state.mutex); + if (client->state.user != RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + callback_data = NULL; + rc_mutex_unlock(&client->state.mutex); + + return callback_data ? &callback_data->async_handle : NULL; +} + +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!password || !password[0]) { + callback(RC_INVALID_STATE, "password is required", client, callback_userdata); + return NULL; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_login_with_password) + return client->state.external_client->begin_login_with_password(client, username, password, callback, callback_userdata); +#endif + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.password = password; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!token || !token[0]) { + callback(RC_INVALID_STATE, "token is required", client, callback_userdata); + return NULL; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_login_with_token) + return client->state.external_client->begin_login_with_token(client, username, token, callback, callback_userdata); +#endif + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.api_token = token; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +void rc_client_logout(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->logout) { + client->state.external_client->logout(); + return; + } +#endif + + switch (client->state.user) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name); + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + RC_CLIENT_LOG_INFO(client, "Aborting login"); + break; + } + + rc_mutex_lock(&client->state.mutex); + + client->state.user = RC_CLIENT_USER_STATE_NONE; + memset(&client->user, 0, sizeof(client->user)); + + load_state = client->state.load; + + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) + rc_client_load_error(load_state, RC_ABORTED, "Login aborted"); +} + +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) +{ + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->get_user_info_v3) + return client->state.external_client->get_user_info_v3(); + + if (client->state.external_client->get_user_info) + return rc_client_external_convert_v1_user(client, client->state.external_client->get_user_info()); + } +#endif + + return (client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; +} + +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size) +{ + if (!user) + return RC_INVALID_STATE; + + if (user->avatar_url) { + snprintf(buffer, buffer_size, "%s", user->avatar_url); + return RC_OK; + } + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); +} + +static void rc_client_subset_get_user_game_summary(const rc_client_t* client, + const rc_client_subset_info_t* subset, rc_client_user_game_summary_t* summary) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + time_t last_unlock_time = 0; + time_t last_progression_time = 0; + time_t first_win_time = 0; + int num_progression_achievements = 0; + int num_win_achievements = 0; + int num_unlocked_progression_achievements = 0; + const uint8_t unlock_bit = (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + for (; achievement < stop; ++achievement) { + switch (achievement->public_.category) { + case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + ++summary->num_core_achievements; + summary->points_core += achievement->public_.points; + + if (achievement->public_.unlocked & unlock_bit) { + ++summary->num_unlocked_achievements; + summary->points_unlocked += achievement->public_.points; + + if (achievement->public_.unlock_time > last_unlock_time) + last_unlock_time = achievement->public_.unlock_time; + + if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) { + ++num_unlocked_progression_achievements; + if (achievement->public_.unlock_time > last_progression_time) + last_progression_time = achievement->public_.unlock_time; + } + else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) { + if (first_win_time == 0 || achievement->public_.unlock_time < first_win_time) + first_win_time = achievement->public_.unlock_time; + } + } + + if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) + ++num_progression_achievements; + else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) + ++num_win_achievements; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + ++summary->num_unsupported_achievements; + + break; + + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + ++summary->num_unofficial_achievements; + break; + + default: + continue; + } + } + + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + if (summary->num_unlocked_achievements == summary->num_core_achievements) + summary->completed_time = last_unlock_time; + + if ((first_win_time || num_win_achievements == 0) && num_unlocked_progression_achievements == num_progression_achievements) + summary->beaten_time = (first_win_time > last_progression_time) ? first_win_time : last_progression_time; +} + +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) +{ + if (!summary) + return; + + memset(summary, 0, sizeof(*summary)); + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->get_user_game_summary_v5) { + client->state.external_client->get_user_game_summary_v5(summary); + return; + } + if (client->state.external_client->get_user_game_summary) { + client->state.external_client->get_user_game_summary(summary); + return; + } + } +#endif + + if (rc_client_is_game_loaded(client)) + rc_client_subset_get_user_game_summary(client, client->game->subsets, summary); +} + +void rc_client_get_user_subset_summary(const rc_client_t* client, uint32_t subset_id, rc_client_user_game_summary_t* summary) +{ + if (!summary) + return; + + memset(summary, 0, sizeof(*summary)); + if (!client || !subset_id) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_user_subset_summary) { + client->state.external_client->get_user_subset_summary(subset_id, summary); + return; + } +#endif + + if (rc_client_is_game_loaded(client)) { + const rc_client_subset_info_t* subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (subset->public_.id == subset_id) { + rc_client_subset_get_user_game_summary(client, subset, summary); + break; + } + } + } +} + +typedef struct rc_client_fetch_all_user_progress_callback_data_t { + rc_client_t* client; + rc_client_fetch_all_user_progress_callback_t callback; + void* callback_userdata; + uint32_t console_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_all_user_progress_callback_data_t; + +static void rc_client_fetch_all_user_progress_callback(const rc_api_server_response_t* server_response, + void* callback_data) +{ + rc_client_fetch_all_user_progress_callback_data_t* ap_callback_data = + (rc_client_fetch_all_user_progress_callback_data_t*)callback_data; + rc_client_t* client = ap_callback_data->client; + rc_api_fetch_all_user_progress_response_t ap_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &ap_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + RC_CLIENT_LOG_VERBOSE(client, "Fetch all progress aborted"); + + free(ap_callback_data); + return; + } + + result = rc_api_process_fetch_all_user_progress_server_response(&ap_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &ap_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch all progress for console %u failed: %s", ap_callback_data->console_id, + error_message); + ap_callback_data->callback(result, error_message, NULL, client, ap_callback_data->callback_userdata); + } else { + rc_client_all_user_progress_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_all_user_progress_entry_t) * ap_response.num_entries; + + list = (rc_client_all_user_progress_t*)malloc(list_size); + if (!list) { + ap_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, + ap_callback_data->callback_userdata); + } else { + rc_client_all_user_progress_entry_t* entry = list->entries = + (rc_client_all_user_progress_entry_t*)((uint8_t*)list + sizeof(*list)); + const rc_api_all_user_progress_entry_t* hlentry = ap_response.entries; + const rc_api_all_user_progress_entry_t* stop = hlentry + ap_response.num_entries; + + for (; hlentry < stop; ++hlentry, ++entry) + { + entry->game_id = hlentry->game_id; + entry->num_achievements = hlentry->num_achievements; + entry->num_unlocked_achievements = hlentry->num_unlocked_achievements; + entry->num_unlocked_achievements_hardcore = hlentry->num_unlocked_achievements_hardcore; + } + + list->num_entries = ap_response.num_entries; + + ap_callback_data->callback(RC_OK, NULL, list, client, ap_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_all_user_progress_response(&ap_response); + free(ap_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_fetch_all_user_progress(rc_client_t* client, uint32_t console_id, + rc_client_fetch_all_user_progress_callback_t callback, + void* callback_userdata) +{ + rc_api_fetch_all_user_progress_request_t api_params; + rc_client_fetch_all_user_progress_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata); + return NULL; + } else if (client->state.user != RC_CLIENT_USER_STATE_LOGGED_IN) { + callback(RC_INVALID_STATE, "client must be logged in", NULL, client, callback_userdata); + return NULL; + } + + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.console_id = console_id; + + result = rc_api_init_fetch_all_user_progress_request_hosted(&request, &api_params, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_all_user_progress_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->console_id = console_id; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_all_user_progress_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +void rc_client_destroy_all_user_progress(rc_client_all_user_progress_t* list) +{ + free(list); +} + +/* ===== Game ===== */ + +static void rc_client_free_game(rc_client_game_info_t* game) +{ + rc_runtime_destroy(&game->runtime); + + rc_buffer_destroy(&game->buffer); + + free(game); +} + +static void rc_client_free_load_state(rc_client_load_state_t* load_state) +{ + if (load_state->game) + rc_client_free_game(load_state->game); + + if (load_state->start_session_response) { + rc_api_destroy_start_session_response(load_state->start_session_response); + free(load_state->start_session_response); + } + +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_destroy_iterator(&load_state->hash_iterator); +#endif + + free(load_state); +} + +static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests) +{ + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = state; + load_state->outstanding_requests += num_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); +} + +static int rc_client_end_load_state(rc_client_load_state_t* load_state) +{ + int remaining_requests = 0; + int aborted = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + if (load_state->outstanding_requests > 0) + --load_state->outstanding_requests; + remaining_requests = load_state->outstanding_requests; + + if (load_state->client->state.load != load_state) + aborted = 1; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (aborted) { + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. As they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) { + /* if one of the callbacks called rc_client_load_error, progress will be set to + * RC_CLIENT_LOAD_STATE_ABORTED. There's no need to call the callback with RC_ABORTED + * in that case, as it will have already been called with something more appropriate. */ + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + } + + return -1; + } + + return remaining_requests; +} + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message) +{ + int remaining_requests = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + if (load_state->client->state.load == load_state) + load_state->client->state.load = NULL; + + remaining_requests = load_state->outstanding_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(load_state->client, "Load failed (%d): %s", result, error_message); + + if (load_state->callback) + load_state->callback(result, error_message, load_state->client, load_state->callback_userdata); + + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. as they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) + rc_client_free_load_state(load_state); +} + +static void rc_client_load_aborted(rc_client_load_state_t* load_state) +{ + /* prevent callback from being called when manually aborted */ + load_state->callback = NULL; + + /* mark the game as no longer being loaded */ + rc_client_load_error(load_state, RC_ABORTED, NULL); + + /* decrement the async counter and potentially free the load_state object */ + rc_client_end_load_state(load_state); +} + +static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(achievement->trigger, memref)) { + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + + if (achievement->trigger) + achievement->trigger->state = RC_TRIGGER_STATE_DISABLED; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address); + } + } + } +} + +static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + if (!leaderboard->lboard) + continue; + + if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_value_contains_memref(&leaderboard->lboard->value, memref)) + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else + continue; + + leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address); + } + } +} + +static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id); + const uint32_t max_address = (regions && regions->num_regions > 0) ? + regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF; + uint8_t buffer[8]; + uint32_t total_count = 0; + uint32_t invalid_count = 0; + + rc_memref_list_t* memref_list = &game->runtime.memrefs->memrefs; + for (; memref_list; memref_list = memref_list->next) { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + total_count += memref_list->count; + + for (; memref < memref_end; ++memref) { + if (memref->address > max_address || + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + memref->value.type = RC_VALUE_TYPE_NONE; + + rc_client_invalidate_memref_achievements(game, client, memref); + rc_client_invalidate_memref_leaderboards(game, client, memref); + + invalid_count++; + } + } + } + + game->max_valid_address = max_address; + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count); +} + +static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_runtime_trigger_t* trigger; + rc_client_subset_info_t* subset; + + if (active_count <= game->runtime.trigger_capacity) { + if (active_count != 0) + memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t)); + } else { + if (game->runtime.triggers) + free(game->runtime.triggers); + + game->runtime.trigger_capacity = active_count; + game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t)); + } + + trigger = game->runtime.triggers; + if (!trigger) { + /* malloc failed, no way to report error, just bail */ + game->runtime.trigger_count = 0; + return; + } + + for (subset = game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + trigger->id = achievement->public_.id; + memcpy(trigger->md5, achievement->md5, 16); + trigger->trigger = achievement->trigger; + ++trigger; + } + } + } + } + + game->runtime.trigger_count = active_count; +} + +static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + ++active_count; + } + + return active_count; +} + +void rc_client_update_active_achievements(rc_client_game_info_t* game) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_count_active_achievements(subset); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if ((achievement->public_.unlocked & active_bit) == 0) { + switch (achievement->public_.state) { + case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: + rc_reset_trigger(achievement->trigger); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + ++active_count; + break; + + case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: + ++active_count; + break; + } + } + else { + achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? + achievement->unlock_time_hardcore : achievement->unlock_time_softcore; + + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + /* if it's active despite being unlocked, and we're in encore mode, leave it active */ + if (client->state.encore_mode) { + ++active_count; + continue; + } + + /* switch to inactive */ + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + + if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state)) { + /* hide any active challenge indicators */ + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client_event.achievement = &achievement->public_; + client->callbacks.event_handler(&client_event, client); + } + + achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; + } + } + } + } + + return active_count; +} + +static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client) +{ + const uint8_t active_bit = (client->state.encore_mode) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_client_toggle_hardcore_achievements(game, client, active_bit); +} + +static void rc_client_update_legacy_runtime_leaderboards(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_subset_info_t* subset; + rc_runtime_lboard_t* lboard; + + if (active_count <= game->runtime.lboard_capacity) { + if (active_count != 0) + memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t)); + } else { + if (game->runtime.lboards) + free(game->runtime.lboards); + + game->runtime.lboard_capacity = active_count; + game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t)); + } + + lboard = game->runtime.lboards; + if (!lboard) { + /* malloc failed. no way to report error, just bail */ + game->runtime.lboard_count = 0; + return; + } + + for (subset = game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + lboard->id = leaderboard->public_.id; + memcpy(lboard->md5, leaderboard->md5, 16); + lboard->lboard = leaderboard->lboard; + ++lboard; + } + } + } + } + + game->runtime.lboard_count = active_count; +} + +void rc_client_update_active_leaderboards(rc_client_game_info_t* game) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) + { + switch (leaderboard->public_.state) + { + case RC_CLIENT_LEADERBOARD_STATE_ACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + ++active_count; + break; + } + } + } + + rc_client_update_legacy_runtime_leaderboards(game, active_count); +} + +static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + const uint8_t leaderboards_allowed = + client->state.hardcore || client->state.allow_leaderboards_in_softcore; + + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + if (leaderboards_allowed) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (leaderboards_allowed) + ++active_count; + else + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + rc_client_update_legacy_runtime_leaderboards(game, active_count); +} + +static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(client->game, leaderboard); + /* fallthrough */ /* to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + game->runtime.lboard_count = 0; +} + +static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, rc_api_unlock_entry_t* unlocks, uint32_t num_unlocks, uint8_t mode) +{ + rc_client_achievement_info_t* start = subset->achievements; + rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; + rc_client_achievement_info_t* scan; + rc_api_unlock_entry_t* unlock = unlocks; + rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks; + + for (; unlock < unlock_stop; ++unlock) { + for (scan = start; scan < stop; ++scan) { + if (scan->public_.id == unlock->achievement_id) { + scan->public_.unlocked |= mode; + + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) + scan->unlock_time_hardcore = unlock->when; + if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE) + scan->unlock_time_softcore = unlock->when; + + if (scan == start) + ++start; + else if (scan + 1 == stop) + --stop; + break; + } + } + } + + if (subset->next) + rc_client_apply_unlocks(subset->next, unlocks, num_unlocks, mode); +} + +static void rc_client_free_pending_media(rc_client_pending_media_t* pending_media) +{ + if (pending_media->hash) + free((void*)pending_media->hash); +#ifdef RC_CLIENT_SUPPORTS_HASH + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); +#endif + free(pending_media); +} + +/* NOTE: address validation uses the read_memory callback to make sure the client + * will return data for the requested address. As such, this function must + * respect the `client->state.allow_background_memory_reads setting. Use + * rc_client_queue_activate_game to dispatch this function to the do_frame loop/ + */ +static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response) +{ + rc_client_t* client = load_state->client; + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + /* unlocks not available - assume malloc failed */ + if (load_state->callback) + load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); + } + else { + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks, + start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks, + start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + } + + /* make the loaded game active if another game is not aleady being loaded. */ + rc_mutex_lock(&client->state.mutex); + if (client->state.load == load_state) { + client->game = load_state->game; + client->state.frames_processed = client->state.frames_at_last_ping = 0; + } + else { + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + } + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { + /* if a change media request is pending, kick it off */ + rc_client_pending_media_t* pending_media; + + rc_mutex_lock(&load_state->client->state.mutex); + pending_media = load_state->pending_media; + load_state->pending_media = NULL; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (pending_media) { + /* rc_client_check_pending_media will fail if it can't find the game in client->game or + * client->state.load->game. since we've detached the load_state, this has to occur after + * we've made the game active. */ + if (pending_media->hash) { + rc_client_begin_change_media(client, pending_media->hash, + pending_media->callback, pending_media->callback_userdata); + } else { +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_client_begin_identify_and_change_media(client, pending_media->file_path, + pending_media->data, pending_media->data_size, + pending_media->callback, pending_media->callback_userdata); +#endif + } + rc_client_free_pending_media(pending_media); + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load != load_state) + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } + + /* if the game is still being loaded, make sure all the required memory addresses are accessible + * so we can mark achievements as unsupported before loading them into the runtime. */ + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { + /* ASSERT: client->game must be set before calling this function so the read_memory callback can query the console_id */ + rc_client_validate_addresses(load_state->game, client); + + rc_mutex_lock(&client->state.mutex); + if (client->state.load != load_state) + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } + + /* if the game is still being loaded, load any active acheivements/leaderboards into the runtime */ + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { + rc_client_activate_achievements(load_state->game, client); + rc_client_activate_leaderboards(load_state->game, client); + + /* detach the load state to indicate that loading is fully complete */ + rc_mutex_lock(&client->state.mutex); + if (client->state.load == load_state) + client->state.load = NULL; + else + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } + + /* one last sanity check to make sure the game is still being loaded. */ + if (load_state->progress == RC_CLIENT_LOAD_GAME_STATE_ABORTED) { + /* game has been unloaded, or another game is being loaded over the top of this game */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + if (load_state->hash->hash[0] != '[') { + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { + /* schedule the periodic ping */ + rc_client_scheduled_callback_data_t* callback_data = (rc_client_scheduled_callback_data_t*) + rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + + memset(callback_data, 0, sizeof(*callback_data)); + callback_data->callback = rc_client_ping; + callback_data->related_id = load_state->game->public_.id; + callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000; + rc_client_schedule_callback(client, callback_data); + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcore %s%s", load_state->game->public_.id, + client->state.hardcore ? "enabled" : "disabled", + (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id); + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + + /* detach the game object so it doesn't get freed by free_load_state */ + load_state->game = NULL; + } + } + + rc_client_free_load_state(load_state); +} + +static void rc_client_dispatch_activate_game(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data->data; + free(callback_data); + + (void)client; + (void)now; + + rc_client_activate_game(load_state, load_state->start_session_response); +} + +static void rc_client_queue_activate_game(rc_client_load_state_t* load_state) +{ + rc_client_scheduled_callback_data_t* scheduled_callback_data = + (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(rc_client_scheduled_callback_data_t)); + + if (!scheduled_callback_data) { + rc_client_load_error(load_state, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + + scheduled_callback_data->callback = rc_client_dispatch_activate_game; + scheduled_callback_data->data = load_state; + + rc_client_schedule_callback(load_state->client, scheduled_callback_data); +} + +static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_start_session_response_t start_session_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(load_state->client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_start_session_server_response(&start_session_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(load_state, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else if (outstanding_requests == 0 && load_state->client->state.allow_background_memory_reads) { + rc_client_activate_game(load_state, &start_session_response); + } + else { + load_state->start_session_response = + (rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t)); + + if (!load_state->start_session_response) { + rc_client_load_error(load_state, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + } + else { + /* safer to parse the response again than to try to copy it */ + rc_api_process_start_session_response(load_state->start_session_response, server_response->body); + } + + if (outstanding_requests == 0) { + if (load_state->client->state.allow_background_memory_reads) + rc_client_activate_game(load_state, load_state->start_session_response); + else + rc_client_queue_activate_game(load_state); + } + } + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void rc_client_begin_start_session(rc_client_load_state_t* load_state) +{ + rc_api_start_session_request_t start_session_params; + rc_client_t* client = load_state->client; + rc_api_request_t start_session_request; + int result; + + memset(&start_session_params, 0, sizeof(start_session_params)); + start_session_params.username = client->user.username; + start_session_params.api_token = client->user.token; + start_session_params.game_id = load_state->hash->game_id; + start_session_params.game_hash = load_state->hash->hash; + start_session_params.hardcore = client->state.hardcore; + + result = rc_api_init_start_session_request_hosted(&start_session_request, &start_session_params, &client->state.host); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); + rc_api_destroy_request(&start_session_request); + } +} + +static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements) +{ + const rc_api_achievement_definition_t* read; + const rc_api_achievement_definition_t* stop; + rc_client_achievement_info_t* achievements; + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* scan; + rc_buffer_t* buffer; + rc_preparse_state_t preparse; + const char* memaddr; + size_t size; + rc_trigger_t* trigger; + int trigger_size; + + subset->achievements = NULL; + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + + stop = achievement_definitions + num_achievements; + + /* if not testing unofficial, filter them out */ + if (!load_state->client->state.unofficial_enabled) { + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) + --num_achievements; + } + + subset->public_.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + } + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */ + + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */ + + sizeof(rc_client_achievement_info_t); + buffer = &load_state->game->buffer; + rc_buffer_reserve(buffer, size * num_achievements); + + /* allocate the achievement array */ + size = sizeof(rc_client_achievement_info_t) * num_achievements; + achievement = achievements = (rc_client_achievement_info_t*)rc_buffer_alloc(buffer, size); + memset(achievements, 0, size); + + /* copy the achievement data */ + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled) + continue; + + achievement->public_.title = rc_buffer_strcpy(buffer, read->title); + achievement->public_.description = rc_buffer_strcpy(buffer, read->description); + snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name); + achievement->public_.id = read->id; + achievement->public_.points = read->points; + achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ? + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + achievement->public_.rarity = read->rarity; + achievement->public_.rarity_hardcore = read->rarity_hardcore; + achievement->public_.type = read->type; /* assert: mapping is 1:1 */ + achievement->public_.badge_url = rc_buffer_strcpy(buffer, read->badge_url); + achievement->public_.badge_locked_url = rc_buffer_strcpy(buffer, read->badge_locked_url); + + memaddr = read->definition; + rc_runtime_checksum(memaddr, achievement->md5); + + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = load_state->game->runtime.memrefs; + trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + rc_parse_trigger_internal(trigger, &memaddr, &preparse.parse); + + trigger_size = preparse.parse.offset; + if (trigger_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_reset_parse_state(&preparse.parse, rc_buffer_reserve(buffer, trigger_size)); + rc_preparse_reserve_memrefs(&preparse, load_state->game->runtime.memrefs); + achievement->trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + memaddr = read->definition; + rc_parse_trigger_internal(achievement->trigger, &memaddr, &preparse.parse); + + if (preparse.parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", preparse.parse.offset, read->id); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + rc_buffer_consume(buffer, (const uint8_t*)preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); + } + + rc_destroy_preparse_state(&preparse); + } + + achievement->created_time = read->created; + achievement->updated_time = read->updated; + + scan = achievement; + while (scan > achievements) { + --scan; + if (strcmp(scan->author, read->author) == 0) { + achievement->author = scan->author; + break; + } + } + if (!achievement->author) + achievement->author = rc_buffer_strcpy(buffer, read->author); + + ++achievement; + } + + subset->achievements = achievements; +} + +uint8_t rc_client_map_leaderboard_format(int format) +{ + switch (format) { + case RC_FORMAT_SECONDS: + case RC_FORMAT_CENTISECS: + case RC_FORMAT_MINUTES: + case RC_FORMAT_SECONDS_AS_MINUTES: + case RC_FORMAT_FRAMES: + return RC_CLIENT_LEADERBOARD_FORMAT_TIME; + + case RC_FORMAT_SCORE: + return RC_CLIENT_LEADERBOARD_FORMAT_SCORE; + + case RC_FORMAT_VALUE: + case RC_FORMAT_FLOAT1: + case RC_FORMAT_FLOAT2: + case RC_FORMAT_FLOAT3: + case RC_FORMAT_FLOAT4: + case RC_FORMAT_FLOAT5: + case RC_FORMAT_FLOAT6: + case RC_FORMAT_FIXED1: + case RC_FORMAT_FIXED2: + case RC_FORMAT_FIXED3: + case RC_FORMAT_TENS: + case RC_FORMAT_HUNDREDS: + case RC_FORMAT_THOUSANDS: + case RC_FORMAT_UNSIGNED_VALUE: + default: + return RC_CLIENT_LEADERBOARD_FORMAT_VALUE; + } +} + +static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards) +{ + const rc_api_leaderboard_definition_t* read; + const rc_api_leaderboard_definition_t* stop; + rc_client_leaderboard_info_t* leaderboards; + rc_client_leaderboard_info_t* leaderboard; + rc_buffer_t* buffer; + rc_preparse_state_t preparse; + const char* memaddr; + const char* ptr; + size_t size; + rc_lboard_t* lboard; + int lboard_size; + + subset->leaderboards = NULL; + subset->public_.num_leaderboards = num_leaderboards; + + if (num_leaderboards == 0) + return; + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_lboard_t) /* lboard container */ + + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */ + + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */ + + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */ + + sizeof(rc_client_leaderboard_info_t); + rc_buffer_reserve(&load_state->game->buffer, size * num_leaderboards); + + /* allocate the achievement array */ + size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; + buffer = &load_state->game->buffer; + leaderboard = leaderboards = (rc_client_leaderboard_info_t*)rc_buffer_alloc(buffer, size); + memset(leaderboards, 0, size); + + /* copy the achievement data */ + read = leaderboard_definitions; + stop = read + num_leaderboards; + do { + leaderboard->public_.title = rc_buffer_strcpy(buffer, read->title); + leaderboard->public_.description = rc_buffer_strcpy(buffer, read->description); + leaderboard->public_.id = read->id; + leaderboard->public_.format = rc_client_map_leaderboard_format(read->format); + leaderboard->public_.lower_is_better = read->lower_is_better; + leaderboard->format = (uint8_t)read->format; + leaderboard->hidden = (uint8_t)read->hidden; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, leaderboard->md5); + + ptr = strstr(memaddr, "VAL:"); + if (ptr != NULL) { + /* calculate the DJB2 hash of the VAL portion of the string*/ + uint32_t hash = 5381; + ptr += 4; /* skip 'VAL:' */ + while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) + hash = (hash << 5) + hash + *ptr++; + leaderboard->value_djb2 = hash; + } + + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = load_state->game->runtime.memrefs; + lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(lboard, memaddr, &preparse.parse); + + lboard_size = preparse.parse.offset; + if (lboard_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_reset_parse_state(&preparse.parse, rc_buffer_reserve(buffer, lboard_size)); + rc_preparse_reserve_memrefs(&preparse, load_state->game->runtime.memrefs); + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &preparse.parse); + + if (preparse.parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", preparse.parse.offset, read->id); + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + rc_buffer_consume(buffer, (const uint8_t*)preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); + } + + rc_destroy_preparse_state(&preparse); + } + + ++leaderboard; + ++read; + } while (read < stop); + + subset->leaderboards = leaderboards; +} + +static void rc_client_fetch_game_sets_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(load_state->client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_sets_response.response); + + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message && result != RC_NOT_FOUND) { + rc_client_load_error(load_state, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else if (fetch_game_sets_response.id == 0) { + load_state->hash->game_id = 0; + rc_client_process_resolved_hash(load_state); + } + else { + rc_client_subset_info_t** next_subset; + rc_client_subset_info_t* first_subset = NULL; + uint32_t set_index; + + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = fetch_game_sets_response.id; + RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Identified game: %u \"%s\" (%s)", load_state->hash->game_id, fetch_game_sets_response.title, load_state->hash->hash); + + if (load_state->hash->hash[0] != '[') { + /* not [NO HASH] or [SUBSETxx] */ + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } + + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && + fetch_game_sets_response.console_id != load_state->game->public_.console_id) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", + fetch_game_sets_response.id, fetch_game_sets_response.console_id, load_state->game->public_.console_id); + } + + /* kick off the start session request while we process the game data */ + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1); + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + /* we can't unlock achievements without a session, lock spectator mode for the game */ + load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED; + } + else { + rc_client_begin_start_session(load_state); + } + + /* process the game data */ + next_subset = &first_subset; + for (set_index = 0; set_index < fetch_game_sets_response.num_sets; ++set_index) { + rc_api_achievement_set_definition_t* set = &fetch_game_sets_response.sets[set_index]; + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = set->id; + subset->active = 1; + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", set->image_name); + subset->public_.badge_url = rc_buffer_strcpy(&load_state->game->buffer, set->image_url); + subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, set->title); + + rc_client_copy_achievements(load_state, subset, set->achievements, set->num_achievements); + rc_client_copy_leaderboards(load_state, subset, set->leaderboards, set->num_leaderboards); + + if (set->type == RC_ACHIEVEMENT_SET_TYPE_CORE) { + if (!first_subset) + next_subset = &subset->next; + subset->next = first_subset; + first_subset = subset; + } + else { + *next_subset = subset; + next_subset = &subset->next; + } + } + + if (!first_subset) { + rc_client_load_error(load_state, RC_NOT_FOUND, "Response contained no sets"); + } else { + load_state->subset = first_subset; + + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_sets_response.title); + load_state->game->subsets = first_subset; + load_state->game->public_.badge_name = first_subset->public_.badge_name; + load_state->game->public_.badge_url = first_subset->public_.badge_url; + load_state->game->public_.console_id = fetch_game_sets_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (fetch_game_sets_response.rich_presence_script && fetch_game_sets_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_sets_response.rich_presence_script, NULL, 0); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); + } + } + + if (load_state->client->callbacks.post_process_game_sets_response) { + load_state->client->callbacks.post_process_game_sets_response(server_response, + &fetch_game_sets_response, load_state->client, load_state->callback_userdata); + } + } + + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else if (outstanding_requests == 0) { + if (load_state->client->state.allow_background_memory_reads) + rc_client_activate_game(load_state, load_state->start_session_response); + else + rc_client_queue_activate_game(load_state); + } + } + + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); +} + +static rc_client_game_info_t* rc_client_allocate_game(void) +{ + rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(*game)); + if (!game) + return NULL; + + rc_buffer_init(&game->buffer); + rc_runtime_init(&game->runtime); + + return game; +} + +static int rc_client_attach_load_state(rc_client_t* client, rc_client_load_state_t* load_state) +{ + if (client->state.load == NULL) { + rc_client_unload_game(client); + + if (load_state->game == NULL) { + load_state->game = rc_client_allocate_game(); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + return 0; + } + } + + rc_mutex_lock(&client->state.mutex); + client->state.load = load_state; + client->state.frames_processed = client->state.frames_at_last_ping = 0; + rc_mutex_unlock(&client->state.mutex); + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + return 0; + } + + return 1; +} + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + +static void rc_client_external_load_state_callback(int result, const char* error_message, rc_client_t* client, void* userdata) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)userdata; + int async_aborted; + + client = load_state->client; + async_aborted = rc_client_end_async(client, &load_state->async_handle); + if (async_aborted) { + if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during external loading"); + } + + rc_client_unload_game(client); /* unload the game from the external client */ + rc_client_free_load_state(load_state); + return; + } + + if (result != RC_OK) { + rc_client_load_error(load_state, result, error_message); + return; + } + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* keep partial game object for media_hash management */ + if (client->state.external_client) { + const rc_client_game_t* info = rc_client_get_game_info(client); + load_state->game->public_.console_id = info->console_id; + client->game = load_state->game; + load_state->game = NULL; + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + } + + rc_client_free_load_state(load_state); +} + +#endif + +static void rc_client_initialize_unknown_game(rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + char buffer[64]; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.title = ""; + game->subsets = subset; + + game->public_.title = "Unknown Game"; + game->public_.badge_name = ""; + + rc_client_get_image_url(buffer, sizeof(buffer), RC_IMAGE_TYPE_GAME, "000001"); + game->public_.badge_url = rc_buffer_strcpy(&game->buffer, buffer); +} + +static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) +{ + rc_client_t* client = load_state->client; + + if (load_state->hash->game_id == 0) { +#ifdef RC_CLIENT_SUPPORTS_HASH + char hash[33]; + + if (rc_hash_iterate(hash, &load_state->hash_iterator)) { + /* found another hash to try */ + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + rc_client_load_game(load_state, hash, NULL); + return; + } + + if (load_state->tried_hashes[1]) { + /* multiple hashes were tried, create a CSV */ + size_t i; + size_t count = 0; + char* ptr; + size_t size = 0; + + for (i = 0; i < sizeof(load_state->tried_hashes) / sizeof(load_state->tried_hashes[0]); ++i) { + if (!load_state->tried_hashes[i]) + break; + + size += strlen(load_state->tried_hashes[i]->hash) + 1; + count++; + } + + ptr = (char*)rc_buffer_alloc(&load_state->game->buffer, size); + load_state->game->public_.hash = ptr; + for (i = 0; i < count; i++) { + const size_t hash_len = strlen(load_state->tried_hashes[i]->hash); + memcpy(ptr, load_state->tried_hashes[i]->hash, hash_len); + ptr += hash_len; + *ptr++ = ','; + } + *(ptr - 1) = '\0'; + + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + } else { + /* only a single hash was tried, capture it */ + load_state->game->public_.console_id = load_state->hash_console_id; + load_state->game->public_.hash = load_state->hash->hash; + + if (client->callbacks.identify_unknown_hash) { + load_state->hash->game_id = client->callbacks.identify_unknown_hash( + load_state->hash_console_id, load_state->hash->hash, client, load_state->callback_userdata); + + if (load_state->hash->game_id != 0) { + load_state->hash->is_unknown = 1; + RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Client says to load game %u for unidentified hash %s", + load_state->hash->game_id, load_state->hash->hash); + } + } + } + + rc_hash_destroy_iterator(&load_state->hash_iterator); /* done with this now */ +#else + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + load_state->game->public_.hash = load_state->hash->hash; +#endif /* RC_CLIENT_SUPPORTS_HASH */ + + if (load_state->hash->game_id == 0) { +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + if (client->state.raintegration && client->state.raintegration->set_console_id) { + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN) + client->state.raintegration->set_console_id(load_state->game->public_.console_id); + } + #endif + if (client->state.external_client) { + if (client->state.external_client->load_unknown_game) { + client->state.external_client->load_unknown_game(load_state->game->public_.hash); + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + /* no external method specifically for unknown game, just pass the hash through to begin_load_game below */ + } + else { +#endif + rc_client_initialize_unknown_game(load_state->game); + + client->game = load_state->game; + load_state->game = NULL; + + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + } +#endif + } + } + + if (load_state->hash->hash[0] != '[') { /* not [NO HASH] or [SUBSETxx] */ + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(load_state->hash->hash, load_state->hash->game_id); + + if (client->state.external_client->begin_load_game) { + rc_client_begin_async(client, &load_state->async_handle); + client->state.external_client->begin_load_game(client, load_state->hash->hash, rc_client_external_load_state_callback, load_state); + } + return; + } +#endif + + rc_client_begin_fetch_game_sets(load_state); +} + +void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes) +{ + rc_client_game_info_t* game; + + game = rc_client_allocate_game(); + if (!game) + return; + + rc_client_initialize_unknown_game(game); + game->public_.console_id = RC_CONSOLE_UNKNOWN; + + if (strlen(tried_hashes) == 32) { /* only one hash tried, add it to the list */ + rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, tried_hashes); + game_hash->game_id = 0; + game->public_.hash = game_hash->hash; + } + else { + game->public_.hash = rc_buffer_strcpy(&game->buffer, tried_hashes); + } + + rc_client_unload_game(client); + client->game = game; +} + +static void rc_client_begin_fetch_game_sets(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_sets_request_t fetch_game_sets_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + + rc_mutex_lock(&client->state.mutex); + result = client->state.user; + if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN; + else + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA; + rc_mutex_unlock(&client->state.mutex); + + switch (result) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + /* do nothing, this function will be called again after login completes */ + return; + + default: + rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED)); + return; + } + + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = client->user.username; + fetch_game_sets_request.api_token = client->user.token; + + if (load_state->hash->is_unknown) /* lookup failed, but client provided a mapping */ + fetch_game_sets_request.game_id = load_state->hash->game_id; + else + fetch_game_sets_request.game_hash = load_state->hash->hash; + + result = rc_api_init_fetch_game_sets_request_hosted(&request, &fetch_game_sets_request, &client->state.host); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for hash %s", fetch_game_sets_request.game_hash); + + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&request, rc_client_fetch_game_sets_callback, load_state, client); + + rc_api_destroy_request(&request); +} + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL +static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + int outstanding_requests; + const char* error_message; + int result; + + result = rc_client_end_async(client, &load_state->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification"); + } else { + rc_client_free_load_state(load_state); + } + return; + } + + result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (error_message) { + rc_client_end_load_state(load_state); + rc_client_load_error(load_state, result, error_message); + } + else { + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = resolve_hash_response.game_id; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + /* have to call end_load_state after updating hash in case the load_state gets free'd */ + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_process_resolved_hash(load_state); + } + } + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} +#endif + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) +{ + rc_client_game_hash_t* game_hash; + + rc_mutex_lock(&client->state.mutex); + game_hash = client->hashes; + while (game_hash) { + if (strcasecmp(game_hash->hash, hash) == 0) + break; + + game_hash = game_hash->next; + } + + if (!game_hash) { + game_hash = (rc_client_game_hash_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); + memset(game_hash, 0, sizeof(*game_hash)); + snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash); + game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID; + game_hash->next = client->hashes; + client->hashes = game_hash; + } + rc_mutex_unlock(&client->state.mutex); + + return game_hash; +} + +void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id) +{ + /* store locally, even if passing to external client */ + rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, hash); + game_hash->game_id = game_id; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(hash, game_id); +#endif +} + +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, + const char* hash, const char* file_path) +{ + rc_client_t* client = load_state->client; + rc_client_game_hash_t* old_hash; +#ifdef RC_CLIENT_SUPPORTS_HASH + size_t i; +#endif + + if (!rc_client_attach_load_state(client, load_state)) { + rc_client_free_load_state(load_state); + return NULL; + } + + old_hash = load_state->hash; + load_state->hash = rc_client_find_game_hash(client, hash); + +#ifdef RC_CLIENT_SUPPORTS_HASH + i = 0; + do { + if (!load_state->tried_hashes[i]) { + load_state->tried_hashes[i] = load_state->hash; + break; + } + + if (load_state->tried_hashes[i] == load_state->hash) + break; + + if (++i == sizeof(load_state->tried_hashes) / sizeof(load_state->tried_hashes[0])) { + RC_CLIENT_LOG_VERBOSE(client, "tried_hashes buffer is full"); + break; + } + } while (1); +#endif + + if (file_path) { + rc_client_media_hash_t* media_hash = + (rc_client_media_hash_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(*media_hash)); + media_hash->game_hash = load_state->hash; + media_hash->path_djb2 = rc_djb2(file_path); + media_hash->next = load_state->game->media_hash; + load_state->game->media_hash = media_hash; + } + else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) { + load_state->game->media_hash->game_hash = load_state->hash; + } + + if (load_state->hash->game_id == 0) { + rc_client_process_resolved_hash(load_state); + } +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + else if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID && + client->state.external_client && client->state.external_client->add_game_hash) { + /* if an add_game_hash external handler exists, do the identification locally, then + * pass the resulting game_id/hash to the external client */ + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = hash; + + result = rc_api_init_resolve_hash_request_hosted(&request, &resolve_hash_request, &client->state.host); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return NULL; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1); + + rc_client_begin_async(client, &load_state->async_handle); + client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client); + + rc_api_destroy_request(&request); + } + else if (load_state->hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID && + client->state.external_client && client->state.external_client->begin_load_game) { + rc_client_begin_async(client, &load_state->async_handle); + client->state.external_client->begin_load_game(client, load_state->hash->hash, rc_client_external_load_state_callback, load_state); + } +#endif + else { + rc_client_begin_fetch_game_sets(load_state); + } + + return (client->state.load == load_state) ? &load_state->async_handle : NULL; +} + +static void rc_client_abort_load_in_progress(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + rc_mutex_lock(&client->state.mutex); + + load_state = client->state.load; + if (load_state) { + /* this mimics rc_client_abort_async without nesting the lock */ + load_state->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED; + + client->state.load = NULL; + } + + rc_mutex_unlock(&client->state.mutex); + + if (load_state && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + + rc_client_abort_load_in_progress(client); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_load_game) + return client->state.external_client->begin_load_game(client, hash, callback, callback_userdata); +#endif + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + return rc_client_load_game(load_state, hash, NULL); +} + +#ifdef RC_CLIENT_SUPPORTS_HASH + +rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) +{ + if (client && client->state.load) + return &client->state.load->hash_iterator; + + return NULL; +} + +static void rc_client_log_hash_message_verbose(const char* message, const rc_hash_iterator_t* iterator) +{ + const rc_client_load_state_t* load_state = (const rc_client_load_state_t*)iterator->userdata; + if (load_state->client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) + rc_client_log_message(load_state->client, message); +} + +static void rc_client_log_hash_message_error(const char* message, const rc_hash_iterator_t* iterator) +{ + const rc_client_load_state_t* load_state = (const rc_client_load_state_t*)iterator->userdata; + if (load_state->client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) + rc_client_log_message(load_state->client, message); +} + +void rc_client_set_hash_callbacks(rc_client_t* client, const struct rc_hash_callbacks* callbacks) +{ + memcpy(&client->callbacks.hash, callbacks, sizeof(*callbacks)); + + if (!callbacks->verbose_message) + client->callbacks.hash.verbose_message = rc_client_log_hash_message_verbose; + if (!callbacks->error_message) + client->callbacks.hash.error_message = rc_client_log_hash_message_error; +} + +rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, + uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + char hash[33]; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + rc_client_abort_load_in_progress(client); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + /* if a add_game_hash handler exists, do the identification locally, then pass the + * resulting game_id/hash to the external client */ + if (client->state.external_client && !client->state.external_client->add_game_hash) { + if (client->state.external_client->begin_identify_and_load_game) + return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata); + } +#endif + + if (data) { + if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data); + } + } + else if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path); + } + else { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + if (!file_path) + file_path = "?"; + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + /* initialize the iterator */ + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + rc_hash_merge_callbacks(&load_state->hash_iterator, &client->callbacks.hash); + load_state->hash_iterator.userdata = load_state; + + if (!load_state->hash_iterator.callbacks.verbose_message) + load_state->hash_iterator.callbacks.verbose_message = rc_client_log_hash_message_verbose; + if (!load_state->hash_iterator.callbacks.error_message) + load_state->hash_iterator.callbacks.error_message = rc_client_log_hash_message_error; + + /* calculate the hash */ + if (console_id == RC_CONSOLE_UNKNOWN) { + if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + } + else { + /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ + load_state->hash_console_id = console_id; + + /* prevent initializing the iterator so it won't try other consoles in rc_client_process_resolved_hash */ + load_state->hash_iterator.index = 0; + + if (!rc_hash_generate(hash, console_id, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +#endif /* RC_CLIENT_SUPPORTS_HASH */ + +int rc_client_get_load_game_state(const rc_client_t* client) +{ + int state = RC_CLIENT_LOAD_GAME_STATE_NONE; + if (client) { + const rc_client_load_state_t* load_state = client->state.load; + if (load_state) + state = load_state->progress; + else if (client->game) + state = RC_CLIENT_LOAD_GAME_STATE_DONE; + } + + return state; +} + +int rc_client_is_game_loaded(const rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + const rc_client_game_t* game = rc_client_get_game_info(client); + return (game && game->id != 0); + } +#endif + + return (client->game && client->game->public_.id != 0); +} + +static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + rc_client_subset_info_t* subset; + + for (subset = game->subsets; subset; subset = subset->next) { + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE && + achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } + + rc_client_hide_progress_tracker(client, game); +} + +void rc_client_unload_game(rc_client_t* client) +{ + rc_client_game_info_t* game; + + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->unload_game) { + client->state.external_client->unload_game(); + + /* a game object may have been allocated to manage hashes */ + game = client->game; + client->game = NULL; + if (game != NULL) + rc_client_free_game(game); + + return; + } +#endif + + rc_mutex_lock(&client->state.mutex); + + game = client->game; + client->game = NULL; + + if (client->state.load) { + /* this mimics rc_client_abort_async without nesting the lock */ + client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED; + + /* if the game is still being loaded, let the load process clean it up */ + if (client->state.load->game == game) + game = NULL; + + client->state.load = NULL; + } + + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) + client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; + + if (game != NULL) { + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_client_game_mark_ui_to_be_hidden(client, game); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; + + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && next->related_id == game->public_.id) { + *last = next->next; + continue; + } + + last = &next->next; + } while (1); + } + + rc_mutex_unlock(&client->state.mutex); + + if (game != NULL) { + rc_client_raise_pending_events(client, game); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id); + rc_client_free_game(game); + } +} + +static void rc_client_change_media_internal(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + client->game->public_.hash = game_hash->hash; + + if (game_hash->game_id == client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash); + } + else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + RC_CLIENT_LOG_INFO(client, "Switching to unknown media"); + } + else if (game_hash->game_id == 0) { + if (client->state.hardcore) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", game_hash->hash); + rc_client_set_hardcore_enabled(client, 0); + callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, callback_userdata); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash); + } + + callback(RC_OK, NULL, client, callback_userdata); +} + +static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + + int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + const int async_aborted = rc_client_end_async(client, &load_state->async_handle); + if (async_aborted) { + if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Media change aborted"); + /* if lookup succeeded, still capture the new hash */ + if (result == RC_OK) + load_state->hash->game_id = resolve_hash_response.game_id; + } + } + else if (client->game != load_state->game) { + /* loaded game changed. return success regardless of result */ + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (error_message) { + load_state->callback(result, error_message, client, load_state->callback_userdata); + } + else { + load_state->hash->game_id = resolve_hash_response.game_id; + + if (resolve_hash_response.game_id != 0) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + } + + rc_client_change_media_internal(client, load_state->hash, load_state->callback, load_state->callback_userdata); + } + + free(load_state); + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +static rc_client_async_handle_t* rc_client_begin_change_media_internal(rc_client_t* client, + rc_client_game_info_t* game, rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID || /* previously looked up */ + game_hash->hash[0] == '[') { /* internal use - don't try to look up */ + rc_client_change_media_internal(client, game_hash, callback, callback_userdata); + return NULL; + } + + /* call the server to make sure the hash is valid for the loaded game */ + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = game_hash->hash; + + result = rc_api_init_resolve_hash_request_hosted(&request, &resolve_hash_request, &client->state.host); + if (result != RC_OK) { + callback(result, rc_error_str(result), client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client = client; + callback_data->hash = game_hash; + callback_data->game = game; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + /* if handle is no longer valid, the async operation completed synchronously */ + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, const rc_client_pending_media_t* media) +{ + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (!game || game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) + rc_client_free_pending_media(pending_media); + + pending_media = (rc_client_pending_media_t*)malloc(sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata); + return NULL; + } + + memcpy(pending_media, media, sizeof(*pending_media)); + if (media->hash) + pending_media->hash = strdup(media->hash); + +#ifdef RC_CLIENT_SUPPORTS_HASH + if (media->file_path) + pending_media->file_path = strdup(media->file_path); + + if (media->data && media->data_size) { + pending_media->data = (uint8_t*)malloc(media->data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata); + return NULL; + } + memcpy(pending_media->data, media->data, media->data_size); + } else { + pending_media->data = NULL; + } +#endif + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + media->callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, media->callback_userdata); + return NULL; + } + + /* still waiting for game data - don't call callback - it's queued */ + if (pending_media) + return NULL; + + return game; +} + +#ifdef RC_CLIENT_SUPPORTS_HASH + +rc_client_async_handle_t* rc_client_begin_identify_and_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_pending_media_t media; + rc_client_game_hash_t* game_hash = NULL; + rc_client_game_info_t* game; + rc_client_media_hash_t* media_hash; + uint32_t path_djb2; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!data && !file_path) { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && !client->state.external_client->begin_change_media) { + if (client->state.external_client->begin_identify_and_change_media) + return client->state.external_client->begin_identify_and_change_media(client, file_path, data, data_size, callback, callback_userdata); + } +#endif + + memset(&media, 0, sizeof(media)); + media.file_path = file_path; + media.data = (uint8_t*)data; + media.data_size = data_size; + media.callback = callback; + media.callback_userdata = callback_userdata; + + game = rc_client_check_pending_media(client, &media); + if (game == NULL) + return NULL; + + /* check to see if we've already hashed this file */ + path_djb2 = rc_djb2(file_path); + rc_mutex_lock(&client->state.mutex); + for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) { + if (media_hash->path_djb2 == path_djb2) { + game_hash = media_hash->game_hash; + break; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!game_hash) { + char hash[33]; + int result; + + if (data != NULL) + result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); + else + result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); + + if (!result) { + /* when changing discs, if the disc is not supported by the system, allow it. this is + * primarily for games that support user-provided audio CDs, but does allow using discs + * from other systems for games that leverage user-provided discs. */ + strcpy_s(hash, sizeof(hash), "[NO HASH]"); + } + + game_hash = rc_client_find_game_hash(client, hash); + + media_hash = (rc_client_media_hash_t*)rc_buffer_alloc(&game->buffer, sizeof(*media_hash)); + media_hash->game_hash = game_hash; + media_hash->path_djb2 = path_djb2; + + rc_mutex_lock(&client->state.mutex); + media_hash->next = game->media_hash; + game->media_hash = media_hash; + rc_mutex_unlock(&client->state.mutex); + + if (!result) { +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, game_hash->hash, callback, callback_userdata); +#endif + + rc_client_change_media_internal(client, game_hash, callback, callback_userdata); + return NULL; + } + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(game_hash->hash, game_hash->game_id); + if (client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, game_hash->hash, callback, callback_userdata); + } +#endif + + return rc_client_begin_change_media_internal(client, game, game_hash, callback, callback_userdata); +} + +#endif /* RC_CLIENT_SUPPORTS_HASH */ + +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_pending_media_t media; + rc_client_game_hash_t* game_hash; + rc_client_game_info_t* game; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_change_media) { + return client->state.external_client->begin_change_media(client, hash, callback, callback_userdata); + } +#endif + + memset(&media, 0, sizeof(media)); + media.hash = hash; + media.callback = callback; + media.callback_userdata = callback_userdata; + + game = rc_client_check_pending_media(client, &media); + if (game == NULL) + return NULL; + + /* check to see if we've already hashed this file. */ + game_hash = rc_client_find_game_hash(client, hash); + return rc_client_begin_change_media_internal(client, game, game_hash, callback, callback_userdata); +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->get_game_info_v3) + return client->state.external_client->get_game_info_v3(); + + if (client->state.external_client->get_game_info) + return rc_client_external_convert_v1_game(client, client->state.external_client->get_game_info()); + } +#endif + + return client->game ? &client->game->public_ : NULL; +} + +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size) +{ + if (!game) + return RC_INVALID_STATE; + + if (game->badge_url) { + snprintf(buffer, buffer_size, "%s", game->badge_url); + return RC_OK; + } + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); +} + +/* ===== Subsets ===== */ + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) +{ + rc_client_subset_info_t* subset; + + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->get_subset_info_v3) + return client->state.external_client->get_subset_info_v3(subset_id); + + if (client->state.external_client->get_subset_info) + return rc_client_external_convert_v1_subset(client, client->state.external_client->get_subset_info(subset_id)); + } +#endif + + if (!client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->public_.id == subset_id) + return &subset->public_; + } + + return NULL; +} + +rc_client_subset_list_t* rc_client_create_subset_list(rc_client_t* client) +{ + rc_client_subset_list_info_t* list; + const rc_client_subset_info_t* subset; + const rc_client_subset_t** subset_ptr; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t num_subsets = 0; + + if (!client) + return (rc_client_subset_list_t*)calloc(1, list_size); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->create_subset_list) + return (rc_client_subset_list_t*)client->state.external_client->create_subset_list(); +#endif + + if (!client->game) + return (rc_client_subset_list_t*)calloc(1, list_size); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + num_subsets++; + } + + list = (rc_client_subset_list_info_t*)malloc(list_size + num_subsets * sizeof(rc_client_subset_t*)); + list->public_.subsets = subset_ptr = (const rc_client_subset_t**)((uint8_t*)list + list_size); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + *subset_ptr++ = &subset->public_; + } + + rc_mutex_unlock(&client->state.mutex); + + list->destroy_func = NULL; + list->public_.num_subsets = (uint32_t)(subset_ptr - list->public_.subsets); + return &list->public_; +} + +void rc_client_destroy_subset_list(rc_client_subset_list_t* list) +{ + rc_client_subset_list_info_t* info = (rc_client_subset_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + free(list); +} + +/* ===== Fetch Game Hashes ===== */ + +typedef struct rc_client_fetch_hash_library_callback_data_t { + rc_client_t* client; + rc_client_fetch_hash_library_callback_t callback; + void* callback_userdata; + uint32_t console_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_hash_library_callback_data_t; + +static void rc_client_fetch_hash_library_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_hash_library_callback_data_t* hashlib_callback_data = + (rc_client_fetch_hash_library_callback_data_t*)callback_data; + rc_client_t* client = hashlib_callback_data->client; + rc_api_fetch_hash_library_response_t hashlib_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &hashlib_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + RC_CLIENT_LOG_VERBOSE(client, "Fetch hash library aborted"); + + free(hashlib_callback_data); + return; + } + + result = rc_api_process_fetch_hash_library_server_response(&hashlib_response, server_response); + error_message = + rc_client_server_error_message(&result, server_response->http_status_code, &hashlib_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch hash library for console %u failed: %s", + hashlib_callback_data->console_id, error_message); + hashlib_callback_data->callback(result, error_message, NULL, client, hashlib_callback_data->callback_userdata); + } else { + rc_client_hash_library_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_hash_library_entry_t) * hashlib_response.num_entries; + list = (rc_client_hash_library_t*)malloc(list_size); + if (!list) { + hashlib_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, + hashlib_callback_data->callback_userdata); + } else { + rc_client_hash_library_entry_t* entry = list->entries = + (rc_client_hash_library_entry_t*)((uint8_t*)list + sizeof(*list)); + const rc_api_hash_library_entry_t* hlentry = hashlib_response.entries; + const rc_api_hash_library_entry_t* stop = hlentry + hashlib_response.num_entries; + + for (; hlentry < stop; ++hlentry, ++entry) { + snprintf(entry->hash, sizeof(entry->hash), "%s", hlentry->hash); + entry->game_id = hlentry->game_id; + } + + list->num_entries = hashlib_response.num_entries; + + hashlib_callback_data->callback(RC_OK, NULL, list, client, hashlib_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_hash_library_response(&hashlib_response); + free(hashlib_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_fetch_hash_library(rc_client_t* client, uint32_t console_id, + rc_client_fetch_hash_library_callback_t callback, + void* callback_userdata) +{ + rc_api_fetch_hash_library_request_t api_params; + rc_client_fetch_hash_library_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata); + return NULL; + } + + api_params.console_id = console_id; + result = rc_api_init_fetch_hash_library_request_hosted(&request, &api_params, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_hash_library_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->console_id = console_id; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_hash_library_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +void rc_client_destroy_hash_library(rc_client_hash_library_t* list) +{ + free(list); +} + +/* ===== Fetch Game Titles ===== */ + +typedef struct rc_client_fetch_game_titles_callback_data_t { + rc_client_t* client; + rc_client_fetch_game_titles_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_fetch_game_titles_callback_data_t; + +static void rc_client_fetch_game_titles_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_game_titles_callback_data_t* titles_callback_data = + (rc_client_fetch_game_titles_callback_data_t*)callback_data; + rc_client_t* client = titles_callback_data->client; + rc_api_fetch_game_titles_response_t titles_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &titles_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + RC_CLIENT_LOG_VERBOSE(client, "Fetch game titles aborted"); + + free(titles_callback_data); + return; + } + + result = rc_api_process_fetch_game_titles_server_response(&titles_response, server_response); + error_message = + rc_client_server_error_message(&result, server_response->http_status_code, &titles_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch game titles failed: %s", error_message); + titles_callback_data->callback(result, error_message, NULL, client, titles_callback_data->callback_userdata); + } else { + rc_client_game_title_list_t* list; + size_t strings_size = 0; + const rc_api_game_title_entry_t* src; + const rc_api_game_title_entry_t* stop; + size_t list_size; + + /* calculate string buffer size */ + for (src = titles_response.entries, stop = src + titles_response.num_entries; src < stop; ++src) { + if (src->title) + strings_size += strlen(src->title) + 1; + } + + list_size = sizeof(*list) + sizeof(rc_client_game_title_entry_t) * titles_response.num_entries + strings_size; + list = (rc_client_game_title_list_t*)malloc(list_size); + if (!list) { + titles_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, + titles_callback_data->callback_userdata); + } else { + rc_client_game_title_entry_t* entry = list->entries = + (rc_client_game_title_entry_t*)((uint8_t*)list + sizeof(*list)); + char* strings = (char*)((uint8_t*)list + sizeof(*list) + + sizeof(rc_client_game_title_entry_t) * titles_response.num_entries); + + for (src = titles_response.entries, stop = src + titles_response.num_entries; src < stop; ++src, ++entry) { + entry->game_id = src->id; + + if (src->title) { + const size_t len = strlen(src->title) + 1; + entry->title = strings; + memcpy(strings, src->title, len); + strings += len; + } else { + entry->title = NULL; + } + + if (src->image_name) + snprintf(entry->badge_name, sizeof(entry->badge_name), "%s", src->image_name); + else + entry->badge_name[0] = '\0'; + } + + list->num_entries = titles_response.num_entries; + + titles_callback_data->callback(RC_OK, NULL, list, client, titles_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_game_titles_response(&titles_response); + free(titles_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_fetch_game_titles(rc_client_t* client, const uint32_t* game_ids, + uint32_t num_game_ids, + rc_client_fetch_game_titles_callback_t callback, + void* callback_userdata) +{ + rc_api_fetch_game_titles_request_t api_params; + rc_client_fetch_game_titles_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata); + return NULL; + } + + if (!game_ids || num_game_ids == 0) { + callback(RC_INVALID_STATE, "game_ids is required", NULL, client, callback_userdata); + return NULL; + } + + api_params.game_ids = game_ids; + api_params.num_game_ids = num_game_ids; + result = rc_api_init_fetch_game_titles_request_hosted(&request, &api_params, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_game_titles_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_game_titles_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +void rc_client_destroy_game_title_list(rc_client_game_title_list_t* list) +{ + free(list); +} + +/* ===== Achievements ===== */ + +static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) +{ + uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN; + uint32_t new_measured_value = 0; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + return; + + achievement->public_.measured_progress[0] = '\0'; + + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { + /* achievement unlocked */ + if (achievement->public_.unlock_time >= recent_unlock_time) { + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + } else { + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + if (client->state.disconnect && rc_client_is_award_achievement_pending(client, achievement->public_.id)) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED; + } + } + else { + /* active achievement */ + new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + if (achievement->trigger) { + if (achievement->trigger->measured_target) { + if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) { + /* value hasn't been initialized yet, leave progress string empty */ + } + else if (achievement->trigger->measured_value == 0) { + /* value is 0, leave progress string empty. update progress to 0.0 */ + achievement->public_.measured_percent = 0.0; + } + else { + /* clamp measured value at target (can't get more than 100%) */ + new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ? + achievement->trigger->measured_target : achievement->trigger->measured_value; + + achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target; + + if (!achievement->trigger->measured_as_percent) { + char* ptr = achievement->public_.measured_progress; + const int buffer_size = (int)sizeof(achievement->public_.measured_progress); + const int chars = rc_format_value(ptr, buffer_size, (int32_t)new_measured_value, RC_FORMAT_UNSIGNED_VALUE); + ptr[chars] = '/'; + rc_format_value(ptr + chars + 1, buffer_size - chars - 1, (int32_t)achievement->trigger->measured_target, RC_FORMAT_UNSIGNED_VALUE); + } + else if (achievement->public_.measured_percent >= 1.0) { + snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), + "%lu%%", (unsigned long)achievement->public_.measured_percent); + } + } + } + + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE; + else if (achievement->public_.measured_percent >= 80.0) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE; + } + } + + achievement->public_.bucket = new_bucket; +} + +static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: return "Unlocks Not Synced to Server"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_achievement_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static int rc_client_compare_achievement_unlock_times(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + if (unlock_b->unlock_time == unlock_a->unlock_time) + return 0; + return (unlock_b->unlock_time < unlock_a->unlock_time) ? -1 : 1; +} + +static int rc_client_compare_achievement_progress(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + if (unlock_b->measured_percent == unlock_a->measured_percent) { + if (unlock_a->id == unlock_b->id) + return 0; + return (unlock_a->id < unlock_b->id) ? -1 : 1; + } + return (unlock_b->measured_percent < unlock_a->measured_percent) ? -1 : 1; +} + +static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) +{ + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { + switch (bucket) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: + return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + default: + return bucket; + } + } + + return bucket; +} + +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + const rc_client_achievement_t** bucket_achievements; + const rc_client_achievement_t** achievement_ptr; + rc_client_achievement_bucket_t* bucket_ptr; + rc_client_achievement_list_info_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS]; + uint32_t num_buckets; + uint32_t num_achievements; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED, + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED + }; + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + + if (!client) + return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t)); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->create_achievement_list_v3) + return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list_v3(category, grouping); + + if (client->state.external_client->create_achievement_list) + return rc_client_external_convert_v1_achievement_list(client, + (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping)); + } +#endif + + if (!client->game) + return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + } + } + } + + num_buckets = 0; + num_achievements = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_achievements += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category) { + if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) { + ++num_buckets; + break; + } + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); + + list = (rc_client_achievement_list_info_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + list->public_.buckets = bucket_ptr = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (const rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + + if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) + qsort((void*)bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE) + qsort((void*)bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); + + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + + achievement = subset->achievements; + stop = achievement + subset->public_.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public_.category & category && + rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public_; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->destroy_func = NULL; + list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets); + return &list->public_; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + rc_client_achievement_list_info_t* info = (rc_client_achievement_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + free(list); +} + +int rc_client_has_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->has_achievements) + return client->state.external_client->has_achievements(); +#endif + + if (!client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + result = 0; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + if (subset->public_.num_achievements > 0) { + result = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static const rc_client_achievement_t* rc_client_subset_get_achievement_info( + rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public_.id == id) { + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_mutex_lock((rc_mutex_t*)(&client->state.mutex)); + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex)); + return &achievement->public_; + } + } + + return NULL; +} + +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->get_achievement_info_v3) + return client->state.external_client->get_achievement_info_v3(id); + + if (client->state.external_client->get_achievement_info) + return rc_client_external_convert_v1_achievement(client, client->state.external_client->get_achievement_info(id)); + } +#endif + + if (!client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id); + if (achievement != NULL) + return achievement; + } + + return NULL; +} + +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) +{ + const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? + RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + if (!achievement || !achievement->badge_name[0]) + return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + + if (image_type == RC_IMAGE_TYPE_ACHIEVEMENT && achievement->badge_url) { + snprintf(buffer, buffer_size, "%s", achievement->badge_url); + return RC_OK; + } + + if (image_type == RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED && achievement->badge_locked_url) { + snprintf(buffer, buffer_size, "%s", achievement->badge_locked_url); + return RC_OK; + } + + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); +} + +typedef struct rc_client_award_achievement_callback_data_t +{ + uint32_t id; + uint32_t retry_count; + uint8_t hardcore; + const char* game_hash; + rc_clock_t unlock_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_award_achievement_callback_data_t; + +static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id) +{ + /* assume lock already held */ + rc_client_scheduled_callback_data_t* scheduled_callback = client->state.scheduled_callbacks; + for (; scheduled_callback; scheduled_callback = scheduled_callback->next) + { + if (scheduled_callback->callback == rc_client_award_achievement_retry) + { + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)scheduled_callback->data; + if (ach_data->id == achievement_id) + return 1; + } + } + + return 0; +} + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); + +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data->data; + + (void)client; + (void)now; + + rc_client_award_achievement_server_call(ach_data); +} + +static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data; + rc_api_award_achievement_response_t award_achievement_response; + + int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response); + + if (error_message) { + if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, result, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message); + rc_client_award_achievement_server_call(ach_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (ach_data->retry_count > 8) ? 120 : (1 << (ach_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay); + + if (!ach_data->scheduled_callback_data) { + ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data)); + if (!ach_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry; + ach_data->scheduled_callback_data->data = ach_data; + ach_data->scheduled_callback_data->related_id = ach_data->id; + } + + ach_data->scheduled_callback_data->when = + ach_data->client->callbacks.get_time_millisecs(ach_data->client) + delay * 1000; + + rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data); + + rc_client_update_disconnect_state(ach_data->client); + return; + } + } + else { + ach_data->client->user.score = award_achievement_response.new_player_score; + ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore; + + if (award_achievement_response.awarded_achievement_id != ach_data->id) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message); + } + else { + if (award_achievement_response.response.error_message) { + /* previously unlocked achievements are returned as a success with an error message */ + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u", + ach_data->id, ach_data->retry_count + 1, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u", + ach_data->id, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + + if (award_achievement_response.achievements_remaining == 0) { + rc_client_subset_info_t* subset; + for (subset = ach_data->client->game->subsets; subset; subset = subset->next) { + if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE && + rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) { + if (subset->public_.id == ach_data->client->game->public_.id) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + } + } + } + } + } + + if (ach_data->retry_count) + rc_client_update_disconnect_state(ach_data->client); + + if (ach_data->scheduled_callback_data) + free(ach_data->scheduled_callback_data); + free(ach_data); +} + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data) +{ + rc_api_award_achievement_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = ach_data->client->user.username; + api_params.api_token = ach_data->client->user.token; + api_params.achievement_id = ach_data->id; + api_params.hardcore = ach_data->hardcore; + api_params.game_hash = ach_data->game_hash; + + if (ach_data->retry_count) { + const rc_clock_t now = ach_data->client->callbacks.get_time_millisecs(ach_data->client); + api_params.seconds_since_unlock = (uint32_t)((now - ach_data->unlock_time) / 1000); + } + + result = rc_api_init_award_achievement_request_hosted(&request, &api_params, &ach_data->client->state.host); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); + free(ach_data); + return; + } + + ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement) +{ + rc_client_award_achievement_callback_data_t* callback_data; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.hardcore) { + achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL); + if (achievement->unlock_time_softcore == 0) + achievement->unlock_time_softcore = achievement->unlock_time_hardcore; + + /* adjust score now - will get accurate score back from server */ + client->user.score += achievement->public_.points; + } + else { + achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL); + + /* adjust score now - will get accurate score back from server */ + client->user.score_softcore += achievement->public_.points; + } + + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public_.unlocked |= (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_unlock(&client->state.mutex); + + if (client->callbacks.can_submit_achievement_unlock && + !client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id); + return; + } + + /* can't unlock unofficial achievements on the server */ + if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + /* don't actually unlock achievements when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + + callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id); + rc_client_raise_server_error_event(client, "award_achievement", achievement->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = achievement->public_.id; + callback_data->hardcore = client->state.hardcore; + callback_data->game_hash = client->game->public_.hash; + callback_data->unlock_time = client->callbacks.get_time_millisecs(client); + + if (client->game) /* may be NULL if this gets called while unloading the game (from another thread - events are raised outside the lock) */ + callback_data->game_hash = client->game->public_.hash; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); + rc_client_award_achievement_server_call(callback_data); +} + +static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + + rc_reset_trigger(trigger); + } +} + +static void rc_client_reset_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_achievements(subset); +} + +/* ===== Leaderboards ===== */ + +static rc_client_leaderboard_info_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public_.id == id) + return leaderboard; + } + + return NULL; +} + +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_leaderboard_info) + return client->state.external_client->get_leaderboard_info(id); +#endif + + if (!client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_leaderboard_info_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id); + if (leaderboard != NULL) + return &leaderboard->public_; + } + + return NULL; +} + +static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive"; + case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active"; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_leaderboard_bucket_label(bucket_type); + new_label_len = strlen(subset->public_.title) + strlen(label) + 4; + new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label); + + *ptr = new_label; + return new_label; +} + +static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping) +{ + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED; + + default: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + } +} + +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + const rc_client_leaderboard_t** bucket_leaderboards; + const rc_client_leaderboard_t** leaderboard_ptr; + rc_client_leaderboard_bucket_t* bucket_ptr; + rc_client_leaderboard_list_info_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[8]; + uint32_t num_buckets; + uint32_t num_leaderboards; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ALL, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED + }; + + if (!client) + return (rc_client_leaderboard_list_t*)calloc(1, list_size); + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->create_leaderboard_list) + return (rc_client_leaderboard_list_t*)client->state.external_client->create_leaderboard_list(grouping); +#endif + + if (!client->game) + return (rc_client_leaderboard_list_t*)calloc(1, list_size); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->hidden) + continue; + + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + bucket_counts[leaderboard->bucket]++; + } + } + + num_buckets = 0; + num_leaderboards = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_leaderboards += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); + + list = (rc_client_leaderboard_list_info_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + list->public_.buckets = bucket_ptr = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (const rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + *leaderboard_ptr++ = &leaderboard->public_; + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->destroy_func = NULL; + list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets); + return &list->public_; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + rc_client_leaderboard_list_info_t* info = (rc_client_leaderboard_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + free(list); +} + +int rc_client_has_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->has_leaderboards) + return client->state.external_client->has_leaderboards(); +#endif + + if (!client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + result = 0; + for (; subset; subset = subset->next) + { + if (!subset->active) + continue; + + if (subset->public_.num_leaderboards > 0) { + result = 1; + break; + } + } + + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker; + rc_client_leaderboard_tracker_info_t* available_tracker = NULL; + + for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) { + if (tracker->reference_count == 0) { + if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + available_tracker = tracker; + + continue; + } + + if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format) + continue; + + if (tracker->raw_value != leaderboard->value) { + /* if the value comes from tracking hits, we can't assume the trackers started in the + * same frame, so we can't share the tracker */ + if (tracker->value_from_hits) + continue; + + /* value has changed. prepare an update event */ + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } + + /* attach to the existing tracker */ + ++tracker->reference_count; + tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + leaderboard->tracker = tracker; + leaderboard->public_.tracker_value = tracker->public_.display; + return; + } + + if (!available_tracker) { + rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers; + + available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buffer_alloc(&game->buffer, sizeof(*available_tracker)); + memset(available_tracker, 0, sizeof(*available_tracker)); + available_tracker->public_.id = 1; + + for (tracker = *next; tracker; next = &tracker->next, tracker = *next) + available_tracker->public_.id++; + + *next = available_tracker; + } + + /* update the claimed tracker */ + available_tracker->reference_count = 1; + available_tracker->value_djb2 = leaderboard->value_djb2; + available_tracker->format = leaderboard->format; + available_tracker->raw_value = leaderboard->value; + available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW; + available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value); + leaderboard->tracker = available_tracker; + leaderboard->public_.tracker_value = available_tracker->public_.display; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; +} + +void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + leaderboard->tracker = NULL; + + if (tracker && --tracker->reference_count == 0) { + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + if (tracker && tracker->raw_value != leaderboard->value) { + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +typedef struct rc_client_submit_leaderboard_entry_callback_data_t +{ + uint32_t id; + int32_t score; + uint32_t retry_count; + const char* game_hash; + rc_clock_t submit_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_submit_leaderboard_entry_callback_data_t; + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data); + +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; + + (void)client; + (void)now; + + rc_client_submit_leaderboard_entry_server_call(lboard_data); +} + +static void rc_client_raise_scoreboard_event(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data, + const rc_api_submit_lboard_entry_response_t* response) +{ + rc_client_leaderboard_scoreboard_t sboard; + rc_client_event_t client_event; + rc_client_subset_info_t* subset; + rc_client_t* client = lboard_data->client; + rc_client_leaderboard_info_t* leaderboard = NULL; + + if (!client || !client->game) + return; + + for (subset = client->game->subsets; subset; subset = subset->next) { + leaderboard = rc_client_subset_get_leaderboard_info(subset, lboard_data->id); + if (leaderboard != NULL) + break; + } + if (leaderboard == NULL) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Trying to raise scoreboard for unknown leaderboard %u", lboard_data->id); + return; + } + + memset(&sboard, 0, sizeof(sboard)); + sboard.leaderboard_id = lboard_data->id; + rc_format_value(sboard.submitted_score, sizeof(sboard.submitted_score), response->submitted_score, leaderboard->format); + rc_format_value(sboard.best_score, sizeof(sboard.best_score), response->best_score, leaderboard->format); + sboard.new_rank = response->new_rank; + sboard.num_entries = response->num_entries; + sboard.num_top_entries = response->num_top_entries; + if (sboard.num_top_entries > 0) { + sboard.top_entries = (rc_client_leaderboard_scoreboard_entry_t*)calloc( + response->num_top_entries, sizeof(rc_client_leaderboard_scoreboard_entry_t)); + if (sboard.top_entries != NULL) { + uint32_t i; + for (i = 0; i < response->num_top_entries; i++) { + sboard.top_entries[i].username = response->top_entries[i].username; + sboard.top_entries[i].rank = response->top_entries[i].rank; + rc_format_value(sboard.top_entries[i].score, sizeof(sboard.top_entries[i].score), response->top_entries[i].score, + leaderboard->format); + } + } + } + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD; + client_event.leaderboard = &leaderboard->public_; + client_event.leaderboard_scoreboard = &sboard; + + lboard_data->client->callbacks.event_handler(&client_event, lboard_data->client); + + if (sboard.top_entries != NULL) { + free(sboard.top_entries); + } +} + +static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data; + rc_api_submit_lboard_entry_response_t submit_lboard_entry_response; + + int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response); + + if (error_message) { + if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, result, submit_lboard_entry_response.response.error_message); + } + else if (lboard_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message); + rc_client_submit_leaderboard_entry_server_call(lboard_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (lboard_data->retry_count > 8) ? 120 : (1 << (lboard_data->retry_count - 2)); + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay); + + if (!lboard_data->scheduled_callback_data) { + lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data)); + if (!lboard_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry; + lboard_data->scheduled_callback_data->data = lboard_data; + lboard_data->scheduled_callback_data->related_id = lboard_data->id; + } + + lboard_data->scheduled_callback_data->when = + lboard_data->client->callbacks.get_time_millisecs(lboard_data->client) + delay * 1000; + + rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data); + + rc_client_update_disconnect_state(lboard_data->client); + return; + } + } + else { + /* raise event for scoreboard */ + if (lboard_data->retry_count < 2) { + rc_client_raise_scoreboard_event(lboard_data, &submit_lboard_entry_response); + } + + /* not currently doing anything with the response */ + if (lboard_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts", + lboard_data->id, lboard_data->score, lboard_data->retry_count); + } + } + + if (lboard_data->retry_count) + rc_client_update_disconnect_state(lboard_data->client); + + if (lboard_data->scheduled_callback_data) + free(lboard_data->scheduled_callback_data); + free(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data) +{ + rc_api_submit_lboard_entry_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = lboard_data->client->user.username; + api_params.api_token = lboard_data->client->user.token; + api_params.leaderboard_id = lboard_data->id; + api_params.score = lboard_data->score; + api_params.game_hash = lboard_data->game_hash; + + if (lboard_data->retry_count) { + const rc_clock_t now = lboard_data->client->callbacks.get_time_millisecs(lboard_data->client); + api_params.seconds_since_completion = (uint32_t)((now - lboard_data->submit_time) / 1000); + } + + result = rc_api_init_submit_lboard_entry_request_hosted(&request, &api_params, &lboard_data->client->state.host); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); + return; + } + + lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_submit_leaderboard_entry_callback_data_t* callback_data; + + if (!client->state.hardcore) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission not allowed in softcore", leaderboard->public_.id); + return; + } + + if (client->callbacks.can_submit_leaderboard_entry && + !client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id); + return; + } + + /* don't actually submit leaderboard entries when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + return; + } + + callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id); + rc_client_raise_server_error_event(client, "submit_lboard_entry", leaderboard->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = leaderboard->public_.id; + callback_data->score = leaderboard->value; + callback_data->game_hash = client->game->public_.hash; + callback_data->submit_time = client->callbacks.get_time_millisecs(client); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", + leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); + rc_client_submit_leaderboard_entry_server_call(callback_data); +} + +static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard) + continue; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(game, leaderboard); + /* fallthrough */ /* to default */ + default: + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_reset_lboard(lboard); + break; + } + } +} + +static void rc_client_reset_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_leaderboards(client->game, subset); +} + +typedef struct rc_client_fetch_leaderboard_entries_callback_data_t { + rc_client_t* client; + rc_client_fetch_leaderboard_entries_callback_t callback; + void* callback_userdata; + uint32_t leaderboard_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_leaderboard_entries_callback_data_t; + +static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data; + rc_client_t* client = lbinfo_callback_data->client; + rc_api_fetch_leaderboard_info_response_t lbinfo_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &lbinfo_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted"); + } + free(lbinfo_callback_data); + return; + } + + result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message); + lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_info_t* info; + const size_t list_size = sizeof(*info) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries; + size_t needed_size = list_size; + uint32_t i; + + for (i = 0; i < lbinfo_response.num_entries; i++) + needed_size += strlen(lbinfo_response.entries[i].username) + 1; + + info = (rc_client_leaderboard_entry_list_info_t*)malloc(needed_size); + if (!info) { + lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_t* list = &info->public_; + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)info + sizeof(*info)); + char* user = (char*)((uint8_t*)list + list_size); + const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries; + const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries; + const size_t logged_in_user_len = strlen(client->user.display_name) + 1; + info->destroy_func = NULL; + list->user_index = -1; + + for (; lbentry < stop; ++lbentry, ++entry) { + const size_t len = strlen(lbentry->username) + 1; + entry->user = user; + memcpy(user, lbentry->username, len); + user += len; + + if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0) + list->user_index = (int)(entry - list->entries); + + entry->index = lbentry->index; + entry->rank = lbentry->rank; + entry->submitted = lbentry->submitted; + + rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format); + } + + list->num_entries = lbinfo_response.num_entries; + list->total_entries = lbinfo_response.total_entries; + + lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response); + free(lbinfo_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client, + const rc_api_fetch_leaderboard_info_request_t* lbinfo_request, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + result = rc_api_init_fetch_leaderboard_info_request_hosted(&request, lbinfo_request, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->leaderboard_id = lbinfo_request->leaderboard_id; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries) + return client->state.external_client->begin_fetch_leaderboard_entries(client, leaderboard_id, first_entry, count, callback, callback_userdata); +#endif + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.first_entry = first_entry; + lbinfo_request.count = count; + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries_around_user) + return client->state.external_client->begin_fetch_leaderboard_entries_around_user(client, leaderboard_id, count, callback, callback_userdata); +#endif + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.username = client->user.username; + lbinfo_request.count = count; + + if (!lbinfo_request.username) { + callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata); + return NULL; + } + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list) +{ + rc_client_leaderboard_entry_list_info_t* info = (rc_client_leaderboard_entry_list_info_t*)list; + if (info->destroy_func) + info->destroy_func(info); + else + free(list); +} + +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size) +{ + if (!entry) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user); +} + +/* ===== Rich Presence ===== */ + +static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_t* client = (rc_client_t*)callback_data; + rc_api_ping_response_t response; + + int result = rc_api_process_ping_server_response(&response, server_response); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response); + if (error_message) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message); + } + + rc_api_destroy_ping_response(&response); +} + +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_api_ping_request_t api_params; + rc_api_request_t request; + char buffer[256]; + int result; + + /* if no frames have been processed since the last ping, the emulator is idle. let the + * server session expire. it will be resumed/restarted once frames start getting + * processed again. */ + if (client->state.frames_processed != client->state.frames_at_last_ping) { + client->state.frames_at_last_ping = client->state.frames_processed; + + if (!client->callbacks.rich_presence_override || + !client->callbacks.rich_presence_override(client, buffer, sizeof(buffer))) { + rc_mutex_lock(&client->state.mutex); + + rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer), + client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + } + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public_.id; + api_params.rich_presence = buffer; + api_params.game_hash = client->game->public_.hash; + api_params.hardcore = client->state.hardcore; + + result = rc_api_init_ping_request_hosted(&request, &api_params, &client->state.host); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); + } + else { + client->callbacks.server_call(&request, rc_client_ping_callback, client, client); + } + } + + callback_data->when = now + 120 * 1000; + rc_client_schedule_callback(client, callback_data); +} + +int rc_client_has_rich_presence(rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->has_rich_presence) + return client->state.external_client->has_rich_presence(); +#endif + + if (!client->game || !client->game->runtime.richpresence || !client->game->runtime.richpresence->richpresence) + return 0; + + return 1; +} + +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size) +{ + int result; + + if (!client || !buffer) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_rich_presence_message) + return client->state.external_client->get_rich_presence_message(buffer, buffer_size); +#endif + + if (!client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + + result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size, + client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + if (result == 0) { + result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title); + /* snprintf will return the amount of space needed, we want to return the number of chars written */ + if ((size_t)result >= buffer_size) + return (buffer_size - 1); + } + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_event_handler) + client->state.external_client->set_event_handler(client, handler); +#endif + + client->callbacks.event_handler = handler; +} + +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_read_memory) + client->state.external_client->set_read_memory(client, handler); +#endif + + client->callbacks.read_memory = handler; +} + +void rc_client_set_allow_background_memory_reads(rc_client_t* client, int allowed) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_allow_background_memory_reads) + client->state.external_client->set_allow_background_memory_reads(allowed); +#endif + + client->state.allow_background_memory_reads = allowed; +} + +static void rc_client_invalidate_processing_memref(rc_client_t* client) +{ + /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ + if (!client->state.processing_memref) + return; + + client->state.processing_memref->value.type = RC_VALUE_TYPE_NONE; + + rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); + rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); + + client->state.processing_memref = NULL; +} + +static uint32_t rc_client_peek_le(uint32_t address, uint32_t num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint32_t value = 0; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + if (num_bytes <= sizeof(value)) { + num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client); + if (num_read == num_bytes) + return value; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +static uint32_t rc_client_peek(uint32_t address, uint32_t num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint8_t buffer[4]; + uint32_t num_read = 0; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 0; + + switch (num_bytes) { + case 1: + num_read = client->callbacks.read_memory(address, buffer, 1, client); + if (num_read == 1) + return buffer[0]; + break; + case 2: + num_read = client->callbacks.read_memory(address, buffer, 2, client); + if (num_read == 2) + return buffer[0] | (buffer[1] << 8); + break; + case 3: + num_read = client->callbacks.read_memory(address, buffer, 3, client); + if (num_read == 3) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16); + break; + case 4: + num_read = client->callbacks.read_memory(address, buffer, 4, client); + if (num_read == 4) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + break; + default: + break; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +void rc_client_set_legacy_peek(rc_client_t* client, int method) +{ + if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { + union { + uint32_t whole; + uint8_t parts[4]; + } u; + u.whole = 1; + method = (u.parts[0] == 1) ? + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED; + } + + client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ? + rc_client_peek_le : rc_client_peek; +} + +int rc_client_is_processing_required(rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->is_processing_required) + return client->state.external_client->is_processing_required(); +#endif + + if (!client->game) + return 0; + + if (client->game->runtime.trigger_count || client->game->runtime.lboard_count) + return 1; + + return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence); +} + +static void rc_client_update_memref_values(rc_client_t* client) { + rc_memrefs_t* memrefs = client->game->runtime.memrefs; + rc_memref_list_t* memref_list; + rc_modified_memref_list_t* modified_memref_list; + int invalidated_memref = 0; + + memref_list = &memrefs->memrefs; + do { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + uint32_t value; + + for (; memref < memref_stop; ++memref) { + if (memref->value.type == RC_VALUE_TYPE_NONE) + continue; + + /* if processing_memref is set, and the memory read fails, all dependent achievements will be disabled */ + client->state.processing_memref = memref; + + value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + + if (client->state.processing_memref) { + rc_update_memref_value(&memref->value, value); + } + else { + /* if the peek function cleared the processing_memref, the memref was invalidated */ + invalidated_memref = 1; + } + } + + memref_list = memref_list->next; + } while (memref_list); + + client->state.processing_memref = NULL; + + modified_memref_list = &memrefs->modified_memrefs; + if (modified_memref_list->count) { + do { + rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_stop = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_stop; ++modified_memref) + rc_update_memref_value(&modified_memref->memref.value, rc_get_modified_memref_value(modified_memref, client->state.legacy_peek, client)); + + modified_memref_list = modified_memref_list->next; + } while (modified_memref_list); + } + + if (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence) + rc_update_values(client->game->runtime.richpresence->richpresence->values, client->state.legacy_peek, client); + + if (invalidated_memref) + rc_client_update_active_achievements(client->game); +} + +static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + int old_state, new_state; + uint32_t old_measured_value; + + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL); + + /* trigger->state doesn't actually change to RESET - RESET just serves as a notification. + * we don't care about that particular notification, so look at the actual state. */ + if (new_state == RC_TRIGGER_STATE_RESET) + new_state = trigger->state; + + /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_value <= trigger->measured_target && + rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { + + /* only show a popup for the achievement closest to triggering */ + float progress = (float)trigger->measured_value / (float)trigger->measured_target; + + if (trigger->measured_as_percent) { + /* if reporting the measured value as a percentage, only show the popup if the percentage changes */ + const uint32_t old_percent = (uint32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const uint32_t new_percent = (uint32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent == new_percent) + progress = -1.0; + } + + if (progress > client->game->progress_tracker.progress) { + client->game->progress_tracker.progress = progress; + client->game->progress_tracker.achievement = achievement; + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE; + } + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + + /* raise events for each of the possible new states */ + if (new_state == RC_TRIGGER_STATE_TRIGGERED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED; + else if (new_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } +} + +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (game->progress_tracker.hide_callback && + game->progress_tracker.hide_callback->when && + game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0); + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + } +} + +static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + + (void)callback_data; + (void)now; + + rc_mutex_lock(&client->state.mutex); + if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + client->game->progress_tracker.hide_callback->when = 0; + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + } + rc_mutex_unlock(&client->state.mutex); + + if (client_event.type) + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (!game->progress_tracker.hide_callback) { + game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) + rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t)); + game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed; + } + + if (game->progress_tracker.hide_callback->when == 0) + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW; + else + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; + + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, + client->callbacks.get_time_millisecs(client) + 2 * 1000); +} + +static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + switch (game->progress_tracker.action) { + case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + break; + case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + break; + default: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + break; + } + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE; + + client_event.achievement = &game->progress_tracker.achievement->public_; + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + rc_client_event_t client_event; + time_t recent_unlock_time = 0; + + memset(&client_event, 0, sizeof(client_event)); + + for (; achievement < stop; ++achievement) { + if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE) + continue; + + /* kick off award achievement request first */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + rc_client_award_achievement(client, achievement); + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS; + } + + /* update display state */ + if (recent_unlock_time == 0) + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + + /* raise events */ + client_event.achievement = &achievement->public_; + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + client->callbacks.event_handler(&client_event, client); + } + + /* clear pending flags */ + achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + client_event.subset = &subset->public_; + + if (subset == client->game->subsets) + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + else + client_event.type = RC_CLIENT_EVENT_SUBSET_COMPLETED; + + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + int old_state, new_state; + + switch (leaderboard->public_.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + default: + if (!lboard) + continue; + + break; + } + + old_state = lboard->state; + new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL); + + switch (new_state) { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (old_state != RC_LBOARD_STATE_STARTED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED; + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + } + else { + rc_client_update_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (old_state != RC_LBOARD_STATE_CANCELED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED; + + if (old_state != RC_LBOARD_STATE_STARTED) + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + else + rc_client_update_leaderboard_tracker(client->game, leaderboard); + + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + } + + if (leaderboard->pending_events) + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } +} + +static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = game->leaderboard_trackers; + for (; tracker; tracker = tracker->next) { + if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard_tracker = &tracker->public_; + + /* update display text for new trackers or updated trackers */ + if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE)) + rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format); + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) { + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + /* request to show and hide in the same frame - ignore the event */ + } + else { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + client->callbacks.event_handler(&client_event, client); + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + client->callbacks.event_handler(&client_event, client); + } + + tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard = &leaderboard->public_; + + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) { + /* kick off submission request before raising event */ + rc_client_submit_leaderboard_entry(client, leaderboard); + + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + client->callbacks.event_handler(&client_event, client); + } + + leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE; + } +} + +static void rc_client_reset_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; + + for (subset = client->game->subsets; subset; subset = subset->next) + subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE; +} + +static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + /* raise any pending achievement events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT) + rc_client_raise_achievement_events(client, subset); + + /* raise any pending leaderboard events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD) + rc_client_raise_leaderboard_events(client, subset); + + /* raise mastery event if pending */ + if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING) + rc_client_raise_mastery_event(client, subset); +} + +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client, game); + + for (subset = game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* raise progress tracker events after achievement events so formatted values are updated for tracker event */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_raise_progress_tracker_events(client, game); + + /* if any achievements were unlocked, resync the active achievements list */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(game); + rc_mutex_unlock(&client->state.mutex); + } + + game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; +} + +void rc_client_do_frame(rc_client_t* client) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->do_frame) { + client->state.external_client->do_frame(); + return; + } +#endif + + if (client->game && !client->game->waiting_for_reset) { + rc_runtime_richpresence_t* richpresence; + rc_client_subset_info_t* subset; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + rc_client_update_memref_values(client); + + client->game->progress_tracker.progress = 0.0; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_do_frame_update_progress_tracker(client, client->game); + + if (client->state.hardcore || client->state.allow_leaderboards_in_softcore) { + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_leaderboards(client, subset); + } + } + + richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_update_richpresence_internal(richpresence->richpresence, client->state.legacy_peek, client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + + ++client->state.frames_processed; + } + + /* we've processed a frame. if there's a pause delay in effect, process it */ + if (client->state.unpaused_frame_decay > 0) { + client->state.unpaused_frame_decay--; + + if (client->state.unpaused_frame_decay == 0 && + client->state.required_unpaused_frames > RC_MINIMUM_UNPAUSED_FRAMES) { + /* the full decay has elapsed and a penalty still exists. + * lower the penalty and reset the decay counter */ + client->state.required_unpaused_frames >>= 1; + + if (client->state.required_unpaused_frames <= RC_MINIMUM_UNPAUSED_FRAMES) + client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + + client->state.unpaused_frame_decay = + client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1) - 1; + } + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->idle) { + client->state.external_client->idle(); + return; + } +#endif + + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + const rc_clock_t now = client->callbacks.get_time_millisecs(client); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (scheduled_callback->when > now) { + /* not time for next callback yet, ignore it */ + scheduled_callback = NULL; + } + else { + /* remove the callback from the queue while we process it. callback can requeue if desired */ + client->state.scheduled_callbacks = scheduled_callback->next; + scheduled_callback->next = NULL; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!scheduled_callback) + break; + + scheduled_callback->callback(scheduled_callback, client, now); + } while (1); + } + + if (client->state.disconnect & ~RC_CLIENT_DISCONNECT_VISIBLE) + rc_client_raise_disconnect_events(client); +} + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_mutex_lock(&client->state.mutex); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next || scheduled_callback->when < next->when) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reschedule_callback(rc_client_t* client, + rc_client_scheduled_callback_data_t* callback, rc_clock_t when) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + /* ASSERT: this should only be called if the mutex is held */ + + callback->when = when; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + + if (next == callback) { + if (when == 0) { + /* request to unschedule the callback */ + *last = next->next; + next->next = NULL; + break; + } + + if (!next->next) { + /* end of list, just append it */ + break; + } + + if (when < next->next->when) { + /* already in the correct place */ + break; + } + + /* remove from current position - will insert later */ + *last = next->next; + next->next = NULL; + continue; + } + + if (!next || (when < next->when && when != 0)) { + /* insert here */ + callback->next = next; + *last = callback; + break; + } + + last = &next->next; + } while (1); +} + +static void rc_client_reset_richpresence(rc_client_t* client) +{ + rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_reset_richpresence(richpresence->richpresence); +} + +static void rc_client_reset_variables(rc_client_t* client) +{ + if (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence) + rc_reset_values(client->game->runtime.richpresence->richpresence->values); +} + +static void rc_client_reset_all(rc_client_t* client) +{ + rc_client_reset_achievements(client); + rc_client_reset_leaderboards(client); + rc_client_reset_richpresence(client); + rc_client_reset_variables(client); +} + +void rc_client_reset(rc_client_t* client) +{ + rc_client_game_hash_t* game_hash; + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->reset) { + client->state.external_client->reset(); + return; + } +#endif + + if (!client->game) + return; + + game_hash = rc_client_find_game_hash(client, client->game->public_.hash); + if (game_hash && game_hash->game_id != client->game->public_.id) { + /* current media is not for loaded game. unload game */ + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)", + (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash); + rc_client_unload_game(client); + return; + } + + RC_CLIENT_LOG_INFO(client, "Resetting runtime"); + + rc_mutex_lock(&client->state.mutex); + + client->game->waiting_for_reset = 0; + rc_client_reset_pending_events(client); + + rc_client_hide_progress_tracker(client, client->game); + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); +} + +int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining) +{ + if (!client) + return 1; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->can_pause) + return client->state.external_client->can_pause(frames_remaining); +#endif + + if (frames_remaining) + *frames_remaining = 0; + + /* pause is always allowed in softcore */ + if (!rc_client_get_hardcore_enabled(client)) + return 1; + + /* a full decay means we haven't processed any frames since the last time this was called. */ + if (client->state.unpaused_frame_decay == client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER) + return 1; + + /* if less than RC_MINIMUM_UNPAUSED_FRAMES have been processed, don't allow the pause */ + if (client->state.unpaused_frame_decay > client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1)) { + if (frames_remaining) { + *frames_remaining = client->state.unpaused_frame_decay - + client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1); + } + return 0; + } + + /* we're going to allow the emulator to pause. calculate how many frames are needed before the next + * pause will be allowed. */ + + if (client->state.unpaused_frame_decay > 0) { + /* The user has paused within the decay window. Require a longer + * run of unpaused frames before allowing the next pause */ + if (client->state.required_unpaused_frames < 5 * 60) /* don't make delay longer then 5 seconds */ + client->state.required_unpaused_frames += RC_MINIMUM_UNPAUSED_FRAMES; + } + + /* require multiple unpaused_frames windows to decay the penalty */ + client->state.unpaused_frame_decay = client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER; + + return 1; +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->progress_size) + return client->state.external_client->progress_size(); +#endif + + if (!rc_client_is_game_loaded(client)) + return 0; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_progress_size(&client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer) +{ + return rc_client_serialize_progress_sized(client, buffer, 0xFFFFFFFF); +} + +int rc_client_serialize_progress_sized(rc_client_t* client, uint8_t* buffer, size_t buffer_size) +{ + int result; + + if (!client) + return RC_NO_GAME_LOADED; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->serialize_progress) + return client->state.external_client->serialize_progress(buffer, buffer_size); +#endif + + if (!rc_client_is_game_loaded(client)) + return RC_NO_GAME_LOADED; + + if (!buffer) + return RC_INVALID_STATE; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_serialize_progress_sized(buffer, (uint32_t)buffer_size, &client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any visible challenge indicators to be hidden */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED && + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + /* flag any visible trackers to be hidden */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (lboard && lboard->state == RC_LBOARD_STATE_STARTED && + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } + } +} + +static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any challenge indicators that should be shown */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + /* if it's already shown, just keep it. otherwise flag it to be shown */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + } + else { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + /* ASSERT: only active achievements are serialized, so we don't have to worry about + * deserialization deactiving them. */ + } + + /* flag any trackers that need to be shown */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE || + leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (lboard->state == RC_LBOARD_STATE_STARTED) { + leaderboard->value = (int)lboard->value.value.value; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + + /* if it's already being tracked, just update tracker. otherwise, allocate one */ + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_update_leaderboard_tracker(game, leaderboard); + } + else { + rc_client_allocate_leaderboard_tracker(game, leaderboard); + } + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + /* deallocate the tracker (don't actually raise the failed event) */ + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } +} + +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized) +{ + return rc_client_deserialize_progress_sized(client, serialized, 0xFFFFFFFF); +} + +int rc_client_deserialize_progress_sized(rc_client_t* client, const uint8_t* serialized, size_t serialized_size) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client) + return RC_NO_GAME_LOADED; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->deserialize_progress) + return client->state.external_client->deserialize_progress(serialized, serialized_size); +#endif + + if (!rc_client_is_game_loaded(client)) + return RC_NO_GAME_LOADED; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_before_deserialize_progress(subset); + + rc_client_hide_progress_tracker(client, client->game); + + if (!serialized) { + rc_client_reset_all(client); + result = RC_OK; + } + else { + result = rc_runtime_deserialize_progress_sized(&client->game->runtime, serialized, (uint32_t)serialized_size, NULL); + } + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_after_deserialize_progress(client->game, subset); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); + + return result; +} + +/* ===== Toggles ===== */ + +static void rc_client_enable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 1; + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); + rc_client_activate_leaderboards(client->game, client); + + /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */ + RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset"); + client->game->waiting_for_reset = 1; + } + else { + RC_CLIENT_LOG_INFO(client, "Hardcore enabled"); + } +} + +static void rc_client_disable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 0; + RC_CLIENT_LOG_INFO(client, "Hardcore disabled"); + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + if (!client->state.allow_leaderboards_in_softcore) + rc_client_deactivate_leaderboards(client->game, client); + } +} + +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled) +{ + int changed = 0; + + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_hardcore_enabled) { + client->state.external_client->set_hardcore_enabled(enabled); + return; + } +#endif + + rc_mutex_lock(&client->state.mutex); + + enabled = enabled ? 1 : 0; + if (client->state.hardcore != enabled) { + if (enabled) + rc_client_enable_hardcore(client); + else + rc_client_disable_hardcore(client); + + changed = 1; + } + + rc_mutex_unlock(&client->state.mutex); + + /* events must be raised outside of lock */ + if (changed && client->game) { + if (enabled) { + /* if enabling hardcore, notify client that a reset is requested */ + if (client->game->waiting_for_reset) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + client->callbacks.event_handler(&client_event, client); + } + } + else { + /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */ + rc_client_raise_pending_events(client, client->game); + } + } +} + +int rc_client_get_hardcore_enabled(const rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_hardcore_enabled) + return client->state.external_client->get_hardcore_enabled(); +#endif + + return client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_unofficial_enabled) { + client->state.external_client->set_unofficial_enabled(enabled); + return; + } +#endif + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled"); + client->state.unofficial_enabled = enabled ? 1 : 0; +} + +int rc_client_get_unofficial_enabled(const rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_unofficial_enabled) + return client->state.external_client->get_unofficial_enabled(); +#endif + + return client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_encore_mode_enabled) { + client->state.external_client->set_encore_mode_enabled(enabled); + return; + } +#endif + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled"); + client->state.encore_mode = enabled ? 1 : 0; +} + +int rc_client_get_encore_mode_enabled(const rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_encore_mode_enabled) + return client->state.external_client->get_encore_mode_enabled(); +#endif + + return client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_spectator_mode_enabled) { + client->state.external_client->set_spectator_mode_enabled(enabled); + return; + } +#endif + + if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) { + RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game."); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled"); + client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF; +} + +int rc_client_get_spectator_mode_enabled(const rc_client_t* client) +{ + if (!client) + return 0; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_spectator_mode_enabled) + return client->state.external_client->get_spectator_mode_enabled(); +#endif + + return (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1; +} + +void rc_client_set_userdata(rc_client_t* client, void* userdata) +{ + if (client) + client->callbacks.client_data = userdata; +} + +void* rc_client_get_userdata(const rc_client_t* client) +{ + return client ? client->callbacks.client_data : NULL; +} + +void rc_client_set_host(rc_client_t* client, const char* hostname) +{ + if (!client) + return; + + if (client->state.host.host && hostname && strcmp(hostname, client->state.host.host) == 0) + return; + + /* clear out any previously specified host information */ + memset(&client->state.host, 0, sizeof(client->state.host)); + + if (hostname && (!hostname[0] || strcmp(hostname, rc_api_default_host()) == 0)) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", rc_api_default_host()); + hostname = rc_api_default_host(); + } + else { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + client->state.host.host = rc_buffer_strcpy(&client->state.buffer, hostname); + } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_host) + client->state.external_client->set_host(hostname); +#endif +} + +size_t rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size) +{ + size_t result; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client && client->state.external_client && client->state.external_client->get_user_agent_clause) { + result = client->state.external_client->get_user_agent_clause(buffer, buffer_size); + if (result > 0) { + result += snprintf(buffer + result, buffer_size - result, " rc_client/" RCHEEVOS_VERSION_STRING); + buffer[buffer_size - 1] = '\0'; + return result; + } + } +#else + (void)client; +#endif + + result = snprintf(buffer, buffer_size, "rcheevos/" RCHEEVOS_VERSION_STRING); + + /* some implementations of snprintf will fill the buffer without null terminating. + * make sure the buffer is null terminated */ + buffer[buffer_size - 1] = '\0'; + return result; +} diff --git a/src/rcheevos/src/rc_client_external.c b/src/rcheevos/src/rc_client_external.c new file mode 100644 index 0000000000..0cf90dbb02 --- /dev/null +++ b/src/rcheevos/src/rc_client_external.c @@ -0,0 +1,277 @@ +#include "rc_client_external.h" + +#include "rc_client_external_versions.h" +#include "rc_client_internal.h" + +#include "rc_api_runtime.h" + +#define RC_CONVERSION_FILL(obj, obj_type, src_type) memset((uint8_t*)obj + sizeof(src_type), 0, sizeof(obj_type) - sizeof(src_type)) + +/* https://media.retroachievements.org/Badge/123456_lock.png is 58 with null terminator */ +#define RC_CLIENT_IMAGE_URL_BUFFER_SIZE 64 +/* https://media.retroachievements.org/UserPic/TwentyCharUserNameXX.png is 69 with null terminator */ +#define RC_CLIENT_USER_IMAGE_URL_BUFFER_SIZE 80 + +typedef struct rc_client_external_conversions_t { + rc_client_user_t user; + rc_client_game_t game; + rc_client_subset_t subsets[4]; + rc_client_achievement_t achievements[16]; + char user_avatar_url[RC_CLIENT_USER_IMAGE_URL_BUFFER_SIZE]; + char game_badge_url[RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char subset_badge_url[4][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char achievement_badge_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char achievement_badge_locked_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + uint32_t next_subset_index; + uint32_t next_achievement_index; +} rc_client_external_conversions_t; + +static const char* rc_client_external_build_avatar_url(char buffer[], size_t buffer_size, uint32_t image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result != RC_OK) + return NULL; + + snprintf(buffer, buffer_size, "%s", request.url); + return buffer; +} + +static void rc_client_external_conversions_init(const rc_client_t* client) +{ + if (!client->state.external_client_conversions) { + rc_client_t* mutable_client = (rc_client_t*)client; + rc_client_external_conversions_t* conversions = (rc_client_external_conversions_t*) + rc_buffer_alloc(&mutable_client->state.buffer, sizeof(rc_client_external_conversions_t)); + + memset(conversions, 0, sizeof(*conversions)); + + mutable_client->state.external_client_conversions = conversions; + } +} + +const rc_client_user_t* rc_client_external_convert_v1_user(const rc_client_t* client, const rc_client_user_t* v1_user) +{ + rc_client_user_t* converted; + + if (!v1_user) + return NULL; + + rc_client_external_conversions_init(client); + + converted = &client->state.external_client_conversions->user; + memcpy(converted, v1_user, sizeof(v1_rc_client_user_t)); + RC_CONVERSION_FILL(converted, rc_client_user_t, v1_rc_client_user_t); + + converted->avatar_url = rc_client_external_build_avatar_url( + client->state.external_client_conversions->user_avatar_url, + sizeof(client->state.external_client_conversions->user_avatar_url), + RC_IMAGE_TYPE_USER, v1_user->username); + + return converted; +} + +const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* client, const rc_client_game_t* v1_game) +{ + rc_client_game_t* converted; + + if (!v1_game) + return NULL; + + rc_client_external_conversions_init(client); + + converted = &client->state.external_client_conversions->game; + memcpy(converted, v1_game, sizeof(v1_rc_client_game_t)); + RC_CONVERSION_FILL(converted, rc_client_game_t, v1_rc_client_game_t); + + converted->badge_url = rc_client_external_build_avatar_url( + client->state.external_client_conversions->game_badge_url, + sizeof(client->state.external_client_conversions->game_badge_url), + RC_IMAGE_TYPE_GAME, v1_game->badge_name); + + return converted; +} + +const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset) +{ + rc_client_subset_t* converted = NULL; + char* badge_url = NULL; + const uint32_t num_subsets = sizeof(client->state.external_client_conversions->subsets) / sizeof(client->state.external_client_conversions->subsets[0]); + uint32_t index; + + if (!v1_subset) + return NULL; + + rc_client_external_conversions_init(client); + + for (index = 0; index < num_subsets; ++index) { + if (client->state.external_client_conversions->subsets[index].id == v1_subset->id) { + converted = &client->state.external_client_conversions->subsets[index]; + badge_url = client->state.external_client_conversions->subset_badge_url[index]; + break; + } + } + + if (!converted) { + index = client->state.external_client_conversions->next_subset_index; + converted = &client->state.external_client_conversions->subsets[index]; + badge_url = client->state.external_client_conversions->subset_badge_url[client->state.external_client_conversions->next_subset_index]; + client->state.external_client_conversions->next_subset_index = (index + 1) % num_subsets; + } + + memcpy(converted, v1_subset, sizeof(v1_rc_client_subset_t)); + RC_CONVERSION_FILL(converted, rc_client_subset_t, v1_rc_client_subset_t); + + converted->badge_url = rc_client_external_build_avatar_url(badge_url, + sizeof(client->state.external_client_conversions->subset_badge_url[0]), + RC_IMAGE_TYPE_GAME, v1_subset->badge_name); + + return converted; +} + +const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement) +{ + rc_client_achievement_t* converted = NULL; + char* badge_url = NULL; + char* badge_locked_url = NULL; + const uint32_t num_achievements = sizeof(client->state.external_client_conversions->achievements) / sizeof(client->state.external_client_conversions->achievements[0]); + uint32_t index; + + if (!v1_achievement) + return NULL; + + rc_client_external_conversions_init(client); + + for (index = 0; index < num_achievements; ++index) { + if (client->state.external_client_conversions->achievements[index].id == v1_achievement->id) { + converted = &client->state.external_client_conversions->achievements[index]; + badge_url = client->state.external_client_conversions->achievement_badge_url[index]; + badge_locked_url = client->state.external_client_conversions->achievement_badge_locked_url[index]; + break; + } + } + + if (!converted) { + index = client->state.external_client_conversions->next_achievement_index; + converted = &client->state.external_client_conversions->achievements[index]; + badge_url = client->state.external_client_conversions->achievement_badge_url[index]; + badge_locked_url = client->state.external_client_conversions->achievement_badge_locked_url[index]; + client->state.external_client_conversions->next_achievement_index = (index + 1) % num_achievements; + } + + memcpy(converted, v1_achievement, sizeof(v1_rc_client_achievement_t)); + RC_CONVERSION_FILL(converted, rc_client_achievement_t, v1_rc_client_achievement_t); + + converted->badge_url = rc_client_external_build_avatar_url(badge_url, + sizeof(client->state.external_client_conversions->achievement_badge_url[0]), + RC_IMAGE_TYPE_ACHIEVEMENT, v1_achievement->badge_name); + converted->badge_locked_url = rc_client_external_build_avatar_url(badge_locked_url, + sizeof(client->state.external_client_conversions->achievement_badge_locked_url[0]), + RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, v1_achievement->badge_name); + + return converted; +} + +typedef struct rc_client_achievement_list_wrapper_t { + rc_client_achievement_list_info_t info; + rc_client_achievement_list_t* source_list; + rc_client_achievement_t* achievements; + rc_client_achievement_t** achievements_pointers; + char* badge_url_buffer; +} rc_client_achievement_list_wrapper_t; + +static void rc_client_destroy_achievement_list_wrapper(rc_client_achievement_list_info_t* info) +{ + rc_client_achievement_list_wrapper_t* wrapper = (rc_client_achievement_list_wrapper_t*)info; + + if (wrapper->achievements) + free(wrapper->achievements); + if (wrapper->achievements_pointers) + free(wrapper->achievements_pointers); + if (wrapper->info.public_.buckets) + free((void*)wrapper->info.public_.buckets); + if (wrapper->badge_url_buffer) + free(wrapper->badge_url_buffer); + + rc_client_destroy_achievement_list(wrapper->source_list); + + free(wrapper); +} + +rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list) +{ + rc_client_achievement_list_wrapper_t* new_list; + (void)client; + + if (!v1_achievement_list) + return NULL; + + new_list = (rc_client_achievement_list_wrapper_t*)calloc(1, sizeof(*new_list)); + if (!new_list) + return NULL; + + new_list->source_list = v1_achievement_list; + new_list->info.destroy_func = rc_client_destroy_achievement_list_wrapper; + + if (v1_achievement_list->num_buckets) { + const v1_rc_client_achievement_bucket_t* src_bucket = (const v1_rc_client_achievement_bucket_t*)&v1_achievement_list->buckets[0]; + const v1_rc_client_achievement_bucket_t* stop_bucket = src_bucket + v1_achievement_list->num_buckets; + rc_client_achievement_bucket_t* bucket; + uint32_t num_achievements = 0; + char* badge_url = NULL; + + new_list->info.public_.buckets = bucket = (rc_client_achievement_bucket_t*)calloc(v1_achievement_list->num_buckets, sizeof(*new_list->info.public_.buckets)); + if (!new_list->info.public_.buckets) + return (rc_client_achievement_list_t*)new_list; + + for (; src_bucket < stop_bucket; src_bucket++) + num_achievements += src_bucket->num_achievements; + + if (num_achievements) { + new_list->achievements = (rc_client_achievement_t*)calloc(num_achievements, sizeof(*new_list->achievements)); + new_list->achievements_pointers = (rc_client_achievement_t**)malloc(num_achievements * sizeof(rc_client_achievement_t*)); + new_list->badge_url_buffer = badge_url = (char*)malloc(num_achievements * 2 * RC_CLIENT_IMAGE_URL_BUFFER_SIZE); + if (!new_list->achievements || !new_list->achievements_pointers || !new_list->badge_url_buffer) + return (rc_client_achievement_list_t*)new_list; + } + + num_achievements = 0; + src_bucket = (const v1_rc_client_achievement_bucket_t*)&v1_achievement_list->buckets[0]; + for (; src_bucket < stop_bucket; src_bucket++, bucket++) { + memcpy(bucket, src_bucket, sizeof(*src_bucket)); + + if (src_bucket->num_achievements) { + const v1_rc_client_achievement_t** src_achievement = (const v1_rc_client_achievement_t**)src_bucket->achievements; + const v1_rc_client_achievement_t** stop_achievement = src_achievement + src_bucket->num_achievements; + rc_client_achievement_t** achievement = &new_list->achievements_pointers[num_achievements]; + + bucket->achievements = (const rc_client_achievement_t**)achievement; + + for (; src_achievement < stop_achievement; ++src_achievement, ++achievement) { + *achievement = &new_list->achievements[num_achievements++]; + memcpy(*achievement, *src_achievement, sizeof(**src_achievement)); + + (*achievement)->badge_url = rc_client_external_build_avatar_url(badge_url, + sizeof(client->state.external_client_conversions->achievement_badge_url[0]), + RC_IMAGE_TYPE_ACHIEVEMENT, (*achievement)->badge_name); + badge_url += RC_CLIENT_IMAGE_URL_BUFFER_SIZE; + (*achievement)->badge_locked_url = rc_client_external_build_avatar_url(badge_url, + sizeof(client->state.external_client_conversions->achievement_badge_locked_url[0]), + RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, (*achievement)->badge_name); + badge_url += RC_CLIENT_IMAGE_URL_BUFFER_SIZE; + } + } + } + + new_list->info.public_.num_buckets = v1_achievement_list->num_buckets; + } + + return (rc_client_achievement_list_t*)new_list; +} diff --git a/src/rcheevos/src/rc_client_external.h b/src/rcheevos/src/rc_client_external.h new file mode 100644 index 0000000000..d73e581b8d --- /dev/null +++ b/src/rcheevos/src/rc_client_external.h @@ -0,0 +1,173 @@ +#ifndef RC_CLIENT_EXTERNAL_H +#define RC_CLIENT_EXTERNAL_H + +#include "rc_client.h" + +RC_BEGIN_C_DECLS + +/* NOTE: any function that is passed a callback also needs to be passed a client instance to pass + * to the callback, and the external interface has to capture both. */ + +typedef void (RC_CCONV *rc_client_external_enable_logging_func_t)(rc_client_t* client, int level, rc_client_message_callback_t callback); +typedef void (RC_CCONV *rc_client_external_set_event_handler_func_t)(rc_client_t* client, rc_client_event_handler_t handler); +typedef void (RC_CCONV *rc_client_external_set_read_memory_func_t)(rc_client_t* client, rc_client_read_memory_func_t handler); +typedef void (RC_CCONV *rc_client_external_set_get_time_millisecs_func_t)(rc_client_t* client, rc_get_time_millisecs_func_t handler); +typedef int (RC_CCONV *rc_client_external_can_pause_func_t)(uint32_t* frames_remaining); + +typedef void (RC_CCONV *rc_client_external_set_int_func_t)(int value); +typedef int (RC_CCONV *rc_client_external_get_int_func_t)(void); +typedef void (RC_CCONV *rc_client_external_set_string_func_t)(const char* value); +typedef size_t (RC_CCONV *rc_client_external_copy_string_func_t)(char buffer[], size_t buffer_size); +typedef void (RC_CCONV *rc_client_external_action_func_t)(void); + +typedef void (RC_CCONV *rc_client_external_async_handle_func_t)(rc_client_async_handle_t* handle); + +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_login_func_t)(rc_client_t* client, + const char* username, const char* pass_token, rc_client_callback_t callback, void* callback_userdata); +typedef const rc_client_user_t* (RC_CCONV *rc_client_external_get_user_info_func_t)(void); + +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_identify_and_load_game_func_t)( + rc_client_t* client, uint32_t console_id, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_game_func_t)(rc_client_t* client, + const char* hash, rc_client_callback_t callback, void* callback_userdata); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_subset_t)(rc_client_t* client, + uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); +typedef const rc_client_game_t* (RC_CCONV *rc_client_external_get_game_info_func_t)(void); +typedef const rc_client_subset_t* (RC_CCONV *rc_client_external_get_subset_info_func_t)(uint32_t subset_id); +typedef void (RC_CCONV* rc_client_external_get_user_subset_summary_func_t)(uint32_t subset_id, rc_client_user_game_summary_t* summary); +typedef void (RC_CCONV *rc_client_external_get_user_game_summary_func_t)(rc_client_user_game_summary_t* summary); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_change_media_func_t)(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); +typedef void (RC_CCONV* rc_client_external_add_game_hash_func_t)(const char* hash, uint32_t game_id); + +/* NOTE: rc_client_external_create_achievement_list_func_t returns an internal wrapper structure which contains the public list + * and a destructor function. */ +struct rc_client_achievement_list_info_t; +typedef struct rc_client_achievement_list_info_t* (RC_CCONV *rc_client_external_create_achievement_list_func_t)(int category, int grouping); +typedef const rc_client_achievement_t* (RC_CCONV *rc_client_external_get_achievement_info_func_t)(uint32_t id); + +/* NOTE: rc_client_external_create_leaderboard_list_func_t returns an internal wrapper structure which contains the public list + * and a destructor function. */ +struct rc_client_leaderboard_list_info_t; +typedef struct rc_client_leaderboard_list_info_t* (RC_CCONV *rc_client_external_create_leaderboard_list_func_t)(int grouping); +typedef const rc_client_leaderboard_t* (RC_CCONV *rc_client_external_get_leaderboard_info_func_t)(uint32_t id); + +/* NOTE: rc_client_external_begin_fetch_leaderboard_entries_func_t and rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t + * pass an internal wrapper structure around the list, which contains the public list and a destructor function. */ +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_fetch_leaderboard_entries_func_t)(rc_client_t* client, + uint32_t leaderboard_id, uint32_t first_entry, uint32_t count, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); +typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t)(rc_client_t* client, + uint32_t leaderboard_id, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/* NOTE: rc_client_external_create_subset_list_func_t returns an internal wrapper structure which contains the public list + * and a destructor function. */ +struct rc_client_subset_list_info_t; +typedef struct rc_client_subset_list_info_t* (RC_CCONV* rc_client_external_create_subset_list_func_t)(); + + +typedef size_t (RC_CCONV *rc_client_external_progress_size_func_t)(void); +typedef int (RC_CCONV *rc_client_external_serialize_progress_func_t)(uint8_t* buffer, size_t buffer_size); +typedef int (RC_CCONV *rc_client_external_deserialize_progress_func_t)(const uint8_t* buffer, size_t buffer_size); + +typedef struct rc_client_external_t +{ + rc_client_external_action_func_t destroy; + + rc_client_external_enable_logging_func_t enable_logging; + rc_client_external_set_event_handler_func_t set_event_handler; + rc_client_external_set_read_memory_func_t set_read_memory; + rc_client_external_set_get_time_millisecs_func_t set_get_time_millisecs; + rc_client_external_set_string_func_t set_host; + rc_client_external_copy_string_func_t get_user_agent_clause; + + rc_client_external_set_int_func_t set_hardcore_enabled; + rc_client_external_get_int_func_t get_hardcore_enabled; + rc_client_external_set_int_func_t set_unofficial_enabled; + rc_client_external_get_int_func_t get_unofficial_enabled; + rc_client_external_set_int_func_t set_encore_mode_enabled; + rc_client_external_get_int_func_t get_encore_mode_enabled; + rc_client_external_set_int_func_t set_spectator_mode_enabled; + rc_client_external_get_int_func_t get_spectator_mode_enabled; + + rc_client_external_async_handle_func_t abort_async; + + rc_client_external_begin_login_func_t begin_login_with_password; + rc_client_external_begin_login_func_t begin_login_with_token; + rc_client_external_action_func_t logout; + rc_client_external_get_user_info_func_t get_user_info; + + rc_client_external_begin_identify_and_load_game_func_t begin_identify_and_load_game; + rc_client_external_begin_load_game_func_t begin_load_game; + rc_client_external_get_game_info_func_t get_game_info; + rc_client_external_begin_load_subset_t begin_load_subset; /* DEPRECATED */ + rc_client_external_get_subset_info_func_t get_subset_info; + rc_client_external_action_func_t unload_game; + rc_client_external_get_user_game_summary_func_t get_user_game_summary; + rc_client_external_begin_change_media_func_t begin_identify_and_change_media; + rc_client_external_begin_load_game_func_t begin_change_media; + + rc_client_external_create_achievement_list_func_t create_achievement_list; + rc_client_external_get_int_func_t has_achievements; + rc_client_external_get_achievement_info_func_t get_achievement_info; + + rc_client_external_create_leaderboard_list_func_t create_leaderboard_list; + rc_client_external_get_int_func_t has_leaderboards; + rc_client_external_get_leaderboard_info_func_t get_leaderboard_info; + rc_client_external_begin_fetch_leaderboard_entries_func_t begin_fetch_leaderboard_entries; + rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t begin_fetch_leaderboard_entries_around_user; + + rc_client_external_copy_string_func_t get_rich_presence_message; + rc_client_external_get_int_func_t has_rich_presence; + + rc_client_external_action_func_t do_frame; + rc_client_external_action_func_t idle; + rc_client_external_get_int_func_t is_processing_required; + rc_client_external_can_pause_func_t can_pause; + rc_client_external_action_func_t reset; + + rc_client_external_progress_size_func_t progress_size; + rc_client_external_serialize_progress_func_t serialize_progress; + rc_client_external_deserialize_progress_func_t deserialize_progress; + + /* VERSION 2 */ + rc_client_external_add_game_hash_func_t add_game_hash; + rc_client_external_set_string_func_t load_unknown_game; + + /* VERSION 3 */ + rc_client_external_get_user_info_func_t get_user_info_v3; + rc_client_external_get_game_info_func_t get_game_info_v3; + rc_client_external_get_subset_info_func_t get_subset_info_v3; + rc_client_external_get_achievement_info_func_t get_achievement_info_v3; + rc_client_external_create_achievement_list_func_t create_achievement_list_v3; + + /* VERSION 4 */ + rc_client_external_set_int_func_t set_allow_background_memory_reads; + + /* VERSION 5 */ + rc_client_external_get_user_game_summary_func_t get_user_game_summary_v5; + rc_client_external_get_user_subset_summary_func_t get_user_subset_summary; + + /* VERSION 6 */ + rc_client_external_create_subset_list_func_t create_subset_list; + +} rc_client_external_t; + +#define RC_CLIENT_EXTERNAL_VERSION 5 + +void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); +void rc_client_load_unknown_game(rc_client_t* client, const char* hash); + +/* conversion support */ + +struct rc_client_external_conversions_t; +const rc_client_user_t* rc_client_external_convert_v1_user(const rc_client_t* client, const rc_client_user_t* v1_user); +const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* client, const rc_client_game_t* v1_game); +const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset); +const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement); +rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list); + +RC_END_C_DECLS + +#endif /* RC_CLIENT_EXTERNAL_H */ diff --git a/src/rcheevos/src/rc_client_external_versions.h b/src/rcheevos/src/rc_client_external_versions.h new file mode 100644 index 0000000000..90f6f68960 --- /dev/null +++ b/src/rcheevos/src/rc_client_external_versions.h @@ -0,0 +1,171 @@ +#ifndef RC_CLIENT_EXTERNAL_CONVERSIONS_H +#define RC_CLIENT_EXTERNAL_CONVERSIONS_H + +#include "rc_client_internal.h" + +RC_BEGIN_C_DECLS + +/* user */ + +typedef struct v1_rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; +} v1_rc_client_user_t; + +typedef struct v3_rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; + const char* avatar_url; +} v3_rc_client_user_t; + +/* game */ + +typedef struct v1_rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} v1_rc_client_game_t; + +typedef struct v3_rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; + const char* badge_url; +} v3_rc_client_game_t; + +/* subset */ + +typedef struct v1_rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + uint32_t num_achievements; + uint32_t num_leaderboards; +} v1_rc_client_subset_t; + +typedef struct v3_rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + uint32_t num_achievements; + uint32_t num_leaderboards; + const char* badge_url; +} v3_rc_client_subset_t; + +/* achievement */ + +typedef struct v1_rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; + float rarity; + float rarity_hardcore; + uint8_t type; +} v1_rc_client_achievement_t; + +typedef struct v3_rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; + float rarity; + float rarity_hardcore; + uint8_t type; + const char* badge_url; + const char* badge_locked_url; +} v3_rc_client_achievement_t; + +/* achievement list */ + +typedef struct v1_rc_client_achievement_bucket_t { + v1_rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v1_rc_client_achievement_bucket_t; + +typedef struct v1_rc_client_achievement_list_t { + v1_rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} v1_rc_client_achievement_list_t; + +typedef struct v1_rc_client_achievement_list_info_t { + v1_rc_client_achievement_list_t public_; + rc_client_destroy_achievement_list_func_t destroy_func; +} v1_rc_client_achievement_list_info_t; + +typedef struct v3_rc_client_achievement_bucket_t { + const v3_rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v3_rc_client_achievement_bucket_t; + +typedef struct v3_rc_client_achievement_list_t { + const v3_rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} v3_rc_client_achievement_list_t; + +typedef struct v3_rc_client_achievement_list_info_t { + v3_rc_client_achievement_list_t public_; + rc_client_destroy_achievement_list_func_t destroy_func; +} v3_rc_client_achievement_list_info_t; + +/* user_game_summary */ + +typedef struct v1_rc_client_user_game_summary_t { + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + uint32_t points_core; + uint32_t points_unlocked; +} v1_rc_client_user_game_summary_t; + +typedef struct v5_rc_client_user_game_summary_t { + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + uint32_t points_core; + uint32_t points_unlocked; + time_t beaten_time; + time_t completed_time; +} v5_rc_client_user_game_summary_t; + +RC_END_C_DECLS + +#endif /* RC_CLIENT_EXTERNAL_CONVERSIONS_H */ diff --git a/src/rcheevos/src/rc_client_internal.h b/src/rcheevos/src/rc_client_internal.h new file mode 100644 index 0000000000..fad288d62a --- /dev/null +++ b/src/rcheevos/src/rc_client_internal.h @@ -0,0 +1,409 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#include "rc_client.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + #include "rc_client_raintegration_internal.h" +#endif +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + #include "rc_client_external.h" +#endif +#ifdef RC_CLIENT_SUPPORTS_HASH + #include "rhash/rc_hash_internal.h" +#endif + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +struct rc_api_fetch_game_sets_response_t; +typedef void (RC_CCONV *rc_client_post_process_game_sets_response_t)(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_sets_response_t* game_sets_response, rc_client_t* client, void* userdata); +typedef int (RC_CCONV *rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); +typedef int (RC_CCONV *rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); +typedef int (RC_CCONV *rc_client_rich_presence_override_t)(rc_client_t* client, char buffer[], size_t buffersize); +typedef uint32_t (RC_CCONV* rc_client_identify_hash_func_t)(uint32_t console_id, const char* hash, + rc_client_t* client, void* callback_userdata); + +typedef struct rc_client_callbacks_t { + rc_client_read_memory_func_t read_memory; + rc_client_event_handler_t event_handler; + rc_client_server_call_t server_call; + rc_client_message_callback_t log_call; + rc_get_time_millisecs_func_t get_time_millisecs; + rc_client_identify_hash_func_t identify_unknown_hash; + rc_client_post_process_game_sets_response_t post_process_game_sets_response; + rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock; + rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry; + rc_client_rich_presence_override_t rich_presence_override; + +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_callbacks_t hash; +#endif + + void* client_data; +} rc_client_callbacks_t; + +struct rc_client_scheduled_callback_data_t; +typedef void (RC_CCONV *rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + rc_clock_t when; + uint32_t related_id; + rc_client_scheduled_callback_t callback; + void* data; + struct rc_client_scheduled_callback_data_t* next; +} rc_client_scheduled_callback_data_t; + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback); + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE = 0, + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED = (1 << 1), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW = (1 << 2), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE = (1 << 3), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE = (1 << 4) /* not a real event, just triggers update */ +}; + +typedef struct rc_client_achievement_info_t { + rc_client_achievement_t public_; + + rc_trigger_t* trigger; + uint8_t md5[16]; + + time_t unlock_time_hardcore; + time_t unlock_time_softcore; + + uint8_t pending_events; + + const char* author; + time_t created_time; + time_t updated_time; +} rc_client_achievement_info_t; + +struct rc_client_achievement_list_info_t; +typedef void (RC_CCONV *rc_client_destroy_achievement_list_func_t)(struct rc_client_achievement_list_info_t* list); + +typedef struct rc_client_achievement_list_info_t { + rc_client_achievement_list_t public_; + rc_client_destroy_achievement_list_func_t destroy_func; +} rc_client_achievement_list_info_t; + +enum { + RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW, + RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE +}; + +typedef struct rc_client_progress_tracker_t { + rc_client_achievement_info_t* achievement; + float progress; + + rc_client_scheduled_callback_data_t* hide_callback; + uint8_t action; +} rc_client_progress_tracker_t; + +/*****************************************************************************\ +| Leaderboard Trackers | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE = (1 << 1), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW = (1 << 2), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE = (1 << 3) +}; + +typedef struct rc_client_leaderboard_tracker_info_t { + rc_client_leaderboard_tracker_t public_; + struct rc_client_leaderboard_tracker_info_t* next; + int32_t raw_value; + + uint32_t value_djb2; + + uint8_t format; + uint8_t pending_events; + uint8_t reference_count; + uint8_t value_from_hits; +} rc_client_leaderboard_tracker_info_t; + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED = (1 << 1), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED = (1 << 2), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED = (1 << 3) +}; + +typedef struct rc_client_leaderboard_info_t { + rc_client_leaderboard_t public_; + + rc_lboard_t* lboard; + uint8_t md5[16]; + + rc_client_leaderboard_tracker_info_t* tracker; + + uint32_t value_djb2; + int32_t value; + + uint8_t format; + uint8_t pending_events; + uint8_t bucket; + uint8_t hidden; +} rc_client_leaderboard_info_t; + +struct rc_client_leaderboard_list_info_t; +typedef void (RC_CCONV *rc_client_destroy_leaderboard_list_func_t)(struct rc_client_leaderboard_list_info_t* list); + +typedef struct rc_client_leaderboard_list_info_t { + rc_client_leaderboard_list_t public_; + rc_client_destroy_leaderboard_list_func_t destroy_func; +} rc_client_leaderboard_list_info_t; + +struct rc_client_leaderboard_entry_list_info_t; +typedef void (RC_CCONV *rc_client_destroy_leaderboard_entry_list_func_t)(struct rc_client_leaderboard_entry_list_info_t* list); + +typedef struct rc_client_leaderboard_entry_list_info_t { + rc_client_leaderboard_entry_list_t public_; + rc_client_destroy_leaderboard_entry_list_func_t destroy_func; +} rc_client_leaderboard_entry_list_info_t; + +uint8_t rc_client_map_leaderboard_format(int format); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +enum { + RC_CLIENT_SUBSET_PENDING_EVENT_NONE = 0, + RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT = (1 << 1), + RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD = (1 << 2) +}; + +typedef struct rc_client_subset_info_t { + rc_client_subset_t public_; + + rc_client_achievement_info_t* achievements; + rc_client_leaderboard_info_t* leaderboards; + + struct rc_client_subset_info_t* next; + + const char* all_label; + const char* inactive_label; + const char* locked_label; + const char* unlocked_label; + const char* unofficial_label; + const char* unsupported_label; + + uint8_t active; + uint8_t mastery; + uint8_t pending_events; +} rc_client_subset_info_t; + +struct rc_client_subset_list_info_t; +typedef void (RC_CCONV* rc_client_destroy_subset_list_func_t)(struct rc_client_subset_list_info_t* list); + +typedef struct rc_client_subset_list_info_t { + rc_client_subset_list_t public_; + rc_client_destroy_subset_list_func_t destroy_func; +} rc_client_subset_list_info_t; + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +typedef struct rc_client_game_hash_t { + char hash[33]; + uint8_t is_unknown; + uint32_t game_id; + struct rc_client_game_hash_t* next; +} rc_client_game_hash_t; + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash); + +typedef struct rc_client_media_hash_t { + rc_client_game_hash_t* game_hash; + struct rc_client_media_hash_t* next; + uint32_t path_djb2; +} rc_client_media_hash_t; + +enum { + RC_CLIENT_GAME_PENDING_EVENT_NONE = 0, + RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER = (1 << 1), + RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS = (1 << 2), + RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER = (1 << 3) +}; + +typedef struct rc_client_game_info_t { + rc_client_game_t public_; + rc_client_leaderboard_tracker_info_t* leaderboard_trackers; + rc_client_progress_tracker_t progress_tracker; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + + uint32_t max_valid_address; + + uint8_t waiting_for_reset; + uint8_t pending_events; + + rc_buffer_t buffer; +} rc_client_game_info_t; + +void rc_client_update_active_achievements(rc_client_game_info_t* game); +void rc_client_update_active_leaderboards(rc_client_game_info_t* game); + +/*****************************************************************************\ +| Client | +\*****************************************************************************/ + +enum { + RC_CLIENT_USER_STATE_NONE, + RC_CLIENT_USER_STATE_LOGIN_REQUESTED, + RC_CLIENT_USER_STATE_LOGGED_IN +}; + +enum { + RC_CLIENT_MASTERY_STATE_NONE, + RC_CLIENT_MASTERY_STATE_PENDING, + RC_CLIENT_MASTERY_STATE_SHOWN +}; + +enum { + RC_CLIENT_SPECTATOR_MODE_OFF, + RC_CLIENT_SPECTATOR_MODE_ON, + RC_CLIENT_SPECTATOR_MODE_LOCKED +}; + +enum { + RC_CLIENT_DISCONNECT_HIDDEN = 0, + RC_CLIENT_DISCONNECT_VISIBLE = (1 << 0), + RC_CLIENT_DISCONNECT_SHOW_PENDING = (1 << 1), + RC_CLIENT_DISCONNECT_HIDE_PENDING = (1 << 2) +}; + +struct rc_client_load_state_t; + +typedef struct rc_client_state_t { + rc_mutex_t mutex; + rc_buffer_t buffer; + + rc_client_scheduled_callback_data_t* scheduled_callbacks; + rc_api_host_t host; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + rc_client_external_t* external_client; + struct rc_client_external_conversions_t* external_client_conversions; +#endif +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + rc_client_raintegration_t* raintegration; +#endif + + uint32_t frames_processed; + uint32_t frames_at_last_ping; + uint16_t unpaused_frame_decay; + uint16_t required_unpaused_frames; + + uint8_t hardcore; + uint8_t encore_mode; + uint8_t spectator_mode; + uint8_t unofficial_enabled; + uint8_t log_level; + uint8_t user; + uint8_t disconnect; + uint8_t allow_leaderboards_in_softcore; + uint8_t allow_background_memory_reads; + + struct rc_client_load_state_t* load; + struct rc_client_async_handle_t* async_handles[4]; + rc_memref_t* processing_memref; + + rc_peek_t legacy_peek; +} rc_client_state_t; + +struct rc_client_t { + rc_client_game_info_t* game; + rc_client_game_hash_t* hashes; + + rc_client_user_t user; + + rc_client_callbacks_t callbacks; + + rc_client_state_t state; +}; + +/*****************************************************************************\ +| Helpers | +\*****************************************************************************/ + +#ifdef RC_NO_VARIADIC_MACROS + void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...); +#else + void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...); + #define RC_CLIENT_LOG_ERR_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_WARN_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_INFO_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_VERBOSE_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message_formatted(client, format, __VA_ARGS__); } +#endif + +void rc_client_log_message(const rc_client_t* client, const char* message); +#define RC_CLIENT_LOG_ERR(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_WARN(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_INFO(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_VERBOSE(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message(client, message); } + +/* internals pulled from runtime.c */ +void rc_runtime_checksum(const char* memaddr, uint8_t* md5); +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref); +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref); +/* end runtime.c internals */ + +/* helper functions for unit tests */ +#ifdef RC_CLIENT_SUPPORTS_HASH +struct rc_hash_iterator; +struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +#endif +/* end helper functions for unit tests */ + +enum { + RC_CLIENT_LEGACY_PEEK_AUTO, + RC_CLIENT_LEGACY_PEEK_CONSTRUCTED, + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS +}; + +void rc_client_set_legacy_peek(rc_client_t* client, int method); + +void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); +void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); + +RC_END_C_DECLS + +#endif /* RC_CLIENT_INTERNAL_H */ diff --git a/src/rcheevos/src/rc_client_raintegration.c b/src/rcheevos/src/rc_client_raintegration.c new file mode 100644 index 0000000000..e89d3de168 --- /dev/null +++ b/src/rcheevos/src/rc_client_raintegration.c @@ -0,0 +1,566 @@ +#include "rc_client_raintegration_internal.h" + +#include "rc_client_internal.h" + +#include "rapi/rc_api_common.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +/* ===== natvis extensions ===== */ + +typedef struct __rc_client_raintegration_event_enum_t { uint8_t value; } __rc_client_raintegration_event_enum_t; +static void rc_client_raintegration_natvis_helper(void) +{ + struct natvis_extensions { + __rc_client_raintegration_event_enum_t raintegration_event_type; + } natvis; + + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_PAUSE; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED; +} + +/* ============================= */ + +static void rc_client_raintegration_load_dll(rc_client_t* client, + const wchar_t* search_directory, rc_client_callback_t callback, void* callback_userdata) +{ + wchar_t sPath[_MAX_PATH]; + const int nPathSize = sizeof(sPath) / sizeof(sPath[0]); + rc_client_raintegration_t* raintegration; + int sPathIndex = 0; + DWORD dwAttrib; + HINSTANCE hDLL; + + if (search_directory) { + sPathIndex = swprintf_s(sPath, nPathSize, L"%s\\", search_directory); + if (sPathIndex > nPathSize - 22) { + callback(RC_INVALID_STATE, "search_directory too long", client, callback_userdata); + return; + } + } + +#if defined(_M_X64) || defined(__amd64__) + wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration-x64.dll"); + dwAttrib = GetFileAttributesW(sPath); + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); + dwAttrib = GetFileAttributesW(sPath); + } +#else + wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); + dwAttrib = GetFileAttributesW(sPath); +#endif + + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + callback(RC_MISSING_VALUE, "RA_Integration.dll not found in search directory", client, callback_userdata); + return; + } + + hDLL = LoadLibraryW(sPath); + if (hDLL == NULL) { + char error_message[512]; + const DWORD last_error = GetLastError(); + int offset = snprintf(error_message, sizeof(error_message), "Failed to load RA_Integration.dll (%u)", last_error); + + if (last_error != 0) { + LPSTR messageBuffer = NULL; + const DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); + + snprintf(&error_message[offset], sizeof(error_message) - offset, ": %.*s", size, messageBuffer); + + LocalFree(messageBuffer); + } + + callback(RC_ABORTED, error_message, client, callback_userdata); + return; + } + + raintegration = (rc_client_raintegration_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_raintegration_t)); + memset(raintegration, 0, sizeof(*raintegration)); + raintegration->hDLL = hDLL; + + raintegration->get_version = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_IntegrationVersion"); + raintegration->get_host_url = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_HostUrl"); + raintegration->init_client = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitClient"); + raintegration->init_client_offline = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitOffline"); + raintegration->set_console_id = (rc_client_raintegration_set_int_func_t)GetProcAddress(hDLL, "_RA_SetConsoleID"); + raintegration->shutdown = (rc_client_raintegration_action_func_t)GetProcAddress(hDLL, "_RA_Shutdown"); + + raintegration->update_main_window_handle = (rc_client_raintegration_hwnd_action_func_t)GetProcAddress(hDLL, "_RA_UpdateHWnd"); + + raintegration->get_external_client = (rc_client_raintegration_get_external_client_func_t)GetProcAddress(hDLL, "_Rcheevos_GetExternalClient"); + raintegration->get_menu = (rc_client_raintegration_get_menu_func_t)GetProcAddress(hDLL, "_Rcheevos_RAIntegrationGetMenu"); + raintegration->activate_menu_item = (rc_client_raintegration_activate_menuitem_func_t)GetProcAddress(hDLL, "_Rcheevos_ActivateRAIntegrationMenuItem"); + raintegration->set_write_memory_function = (rc_client_raintegration_set_write_memory_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationWriteMemoryFunction"); + raintegration->set_get_game_name_function = (rc_client_raintegration_set_get_game_name_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationGetGameNameFunction"); + raintegration->set_event_handler = (rc_client_raintegration_set_event_handler_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationEventHandler"); + raintegration->has_modifications = (rc_client_raintegration_get_int_func_t)GetProcAddress(hDLL, "_Rcheevos_HasModifications"); + raintegration->get_achievement_state = (rc_client_raintegration_get_achievement_state_func_t)GetProcAddress(hDLL, "_Rcheevos_GetAchievementState"); + + if (!raintegration->get_version || + !raintegration->init_client || + !raintegration->get_external_client) { + FreeLibrary(hDLL); + + callback(RC_ABORTED, "One or more required exports was not found in RA_Integration.dll", client, callback_userdata); + + /* dummy reference to natvis helper to ensure extensions get compiled in. */ + raintegration->shutdown = rc_client_raintegration_natvis_helper; + } + else { + rc_mutex_lock(&client->state.mutex); + client->state.raintegration = raintegration; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "RA_Integration.dll %s loaded", client->state.raintegration->get_version()); + } +} + +typedef struct rc_client_version_validation_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + HWND main_window_handle; + char* client_name; + char* client_version; + rc_client_async_handle_t async_handle; +} rc_client_version_validation_callback_data_t; + +int rc_client_version_less(const char* left, const char* right) +{ + do { + int left_len = 0; + int right_len = 0; + while (*left && *left == '0') + ++left; + while (left[left_len] && left[left_len] != '.') + ++left_len; + while (*right && *right == '0') + ++right; + while (right[right_len] && right[right_len] != '.') + ++right_len; + + if (left_len != right_len) + return (left_len < right_len); + + while (left_len--) { + if (*left != *right) + return (*left < *right); + ++left; + ++right; + } + + if (*left == '.') + ++left; + if (*right == '.') + ++right; + } while (*left || *right); + + return 0; +} + +static void rc_client_init_raintegration(rc_client_t* client, + rc_client_version_validation_callback_data_t* version_validation_callback_data) +{ + rc_client_raintegration_init_client_func_t init_func = client->state.raintegration->init_client; + + if (client->state.raintegration->get_host_url) { + const char* host_url = client->state.raintegration->get_host_url(); + if (host_url) { + if (strcmp(host_url, "OFFLINE") != 0) { + if (strcmp(host_url, rc_api_default_host()) != 0) + rc_client_set_host(client, host_url); + } + else if (client->state.raintegration->init_client_offline) { + init_func = client->state.raintegration->init_client_offline; + RC_CLIENT_LOG_INFO(client, "Initializing in offline mode"); + } + } + } + + if (!init_func || !init_func(version_validation_callback_data->main_window_handle, + version_validation_callback_data->client_name, + version_validation_callback_data->client_version)) { + const char* error_message = "RA_Integration initialization failed"; + + rc_client_unload_raintegration(client); + + RC_CLIENT_LOG_ERR(client, error_message); + version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); + } + else { + rc_client_external_t* external_client = (rc_client_external_t*) + rc_buffer_alloc(&client->state.buffer, sizeof(*external_client)); + memset(external_client, 0, sizeof(*external_client)); + + if (!client->state.raintegration->get_external_client(external_client, RC_CLIENT_EXTERNAL_VERSION)) { + const char* error_message = "RA_Integration external client export failed"; + + rc_client_unload_raintegration(client); + + RC_CLIENT_LOG_ERR(client, error_message); + version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); + } + else { + /* copy state to the external client */ + if (external_client->enable_logging) + external_client->enable_logging(client, client->state.log_level, client->callbacks.log_call); + + if (external_client->set_event_handler) + external_client->set_event_handler(client, client->callbacks.event_handler); + if (external_client->set_read_memory) + external_client->set_read_memory(client, client->callbacks.read_memory); + + if (external_client->set_hardcore_enabled) + external_client->set_hardcore_enabled(rc_client_get_hardcore_enabled(client)); + if (external_client->set_unofficial_enabled) + external_client->set_unofficial_enabled(rc_client_get_unofficial_enabled(client)); + if (external_client->set_encore_mode_enabled) + external_client->set_encore_mode_enabled(rc_client_get_encore_mode_enabled(client)); + if (external_client->set_spectator_mode_enabled) + external_client->set_spectator_mode_enabled(rc_client_get_spectator_mode_enabled(client)); + if (external_client->set_allow_background_memory_reads) + external_client->set_allow_background_memory_reads(client->state.allow_background_memory_reads); + + /* attach the external client and call the callback */ + client->state.external_client = external_client; + + client->state.raintegration->hMainWindow = version_validation_callback_data->main_window_handle; + client->state.raintegration->bIsInited = 1; + + version_validation_callback_data->callback(RC_OK, NULL, + client, version_validation_callback_data->callback_userdata); + } + } +} + +static void rc_client_version_validation_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_version_validation_callback_data_t* version_validation_callback_data = + (rc_client_version_validation_callback_data_t*)callback_data; + rc_client_t* client = version_validation_callback_data->client; + + if (rc_client_async_handle_aborted(client, &version_validation_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Version validation aborted"); + } + else { + rc_api_response_t response; + int result; + const char* current_version; + const char* minimum_version = ""; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("MinimumVersion"), + }; + + memset(&response, 0, sizeof(response)); + rc_buffer_init(&response.buffer); + + result = rc_json_parse_server_response(&response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result == RC_OK) { + if (!rc_json_get_required_string(&minimum_version, &response, &fields[2], "MinimumVersion")) + result = RC_MISSING_VALUE; + } + + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to fetch latest integration version: %.*s", server_response->body_length, server_response->body); + + rc_client_unload_raintegration(client); + + version_validation_callback_data->callback(result, rc_error_str(result), + client, version_validation_callback_data->callback_userdata); + } + else { + current_version = client->state.raintegration->get_version(); + + if (rc_client_version_less(current_version, minimum_version)) { + char error_message[256]; + + rc_client_unload_raintegration(client); + + snprintf(error_message, sizeof(error_message), + "RA_Integration version %s is lower than minimum version %s", current_version, minimum_version); + RC_CLIENT_LOG_WARN(client, error_message); + version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Validated RA_Integration version %s (minimum %s)", current_version, minimum_version); + + rc_client_init_raintegration(client, version_validation_callback_data); + } + } + + rc_buffer_destroy(&response.buffer); + } + + free(version_validation_callback_data->client_name); + free(version_validation_callback_data->client_version); + free(version_validation_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client, + const wchar_t* search_directory, HWND main_window_handle, + const char* client_name, const char* client_version, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_version_validation_callback_data_t* callback_data; + rc_api_url_builder_t builder; + rc_api_request_t request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!client_name) { + callback(RC_INVALID_STATE, "client_name is required", client, callback_userdata); + return NULL; + } + + if (!client_version) { + callback(RC_INVALID_STATE, "client_version is required", client, callback_userdata); + return NULL; + } + + if (client->state.user != RC_CLIENT_USER_STATE_NONE) { + callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata); + return NULL; + } + + if (!client->state.raintegration) { + if (!main_window_handle) { + callback(RC_INVALID_STATE, "main_window_handle is required", client, callback_userdata); + return NULL; + } + + rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata); + if (!client->state.raintegration) + return NULL; + } + + if (client->state.raintegration->get_host_url) { + const char* host_url = client->state.raintegration->get_host_url(); + if (host_url && strcmp(host_url, rc_api_default_host()) != 0 && + strcmp(host_url, "OFFLINE") != 0) { + /* if the DLL specifies a custom host, use it */ + rc_client_set_host(client, host_url); + } + } + + memset(&request, 0, sizeof(request)); + rc_api_url_build_dorequest_url(&request, &client->state.host); + rc_url_builder_init(&builder, &request.buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "latestintegration"); + request.post_data = rc_url_builder_finalize(&builder); + + callback_data = calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client_name = strdup(client_name); + callback_data->client_version = strdup(client_version); + callback_data->main_window_handle = main_window_handle; + + client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client); + return &callback_data->async_handle; +} + +void rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle) +{ + if (client && client->state.raintegration) { + client->state.raintegration->hMainWindow = main_window_handle; + + if (client->state.raintegration->bIsInited && + client->state.raintegration->update_main_window_handle) { + client->state.raintegration->update_main_window_handle(main_window_handle); + } + } +} + +void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler) +{ + if (client && client->state.raintegration && client->state.raintegration->set_write_memory_function) + client->state.raintegration->set_write_memory_function(client, handler); +} + +void rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler) +{ + if (client && client->state.raintegration && client->state.raintegration->set_get_game_name_function) + client->state.raintegration->set_get_game_name_function(client, handler); +} + +void rc_client_raintegration_set_event_handler(rc_client_t* client, + rc_client_raintegration_event_handler_t handler) +{ + if (client && client->state.raintegration && client->state.raintegration->set_event_handler) + client->state.raintegration->set_event_handler(client, handler); +} + +const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_client_t* client) +{ + if (!client || !client->state.raintegration || + !client->state.raintegration->bIsInited || + !client->state.raintegration->get_menu) { + return NULL; + } + + return client->state.raintegration->get_menu(); +} + +void rc_client_raintegration_set_console_id(rc_client_t* client, uint32_t console_id) +{ + if (client && client->state.raintegration && client->state.raintegration->set_console_id) + client->state.raintegration->set_console_id(console_id); +} + +int rc_client_raintegration_has_modifications(const rc_client_t* client) +{ + if (!client || !client->state.raintegration || + !client->state.raintegration->bIsInited || + !client->state.raintegration->has_modifications) { + return 0; + } + + return client->state.raintegration->has_modifications(); +} + +int rc_client_raintegration_get_achievement_state(const rc_client_t* client, uint32_t achievement_id) +{ + if (!client || !client->state.raintegration || + !client->state.raintegration->bIsInited || + !client->state.raintegration->get_achievement_state) { + return RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_NONE; + } + + return client->state.raintegration->get_achievement_state(achievement_id); +} + +void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu) +{ + HMENU hPopupMenu = NULL; + const rc_client_raintegration_menu_t* menu; + + if (!client || !client->state.raintegration) + return; + + /* destroy the existing menu */ + if (client->state.raintegration->hPopupMenu) + DestroyMenu(client->state.raintegration->hPopupMenu); + + /* create the popup menu */ + hPopupMenu = CreatePopupMenu(); + + menu = rc_client_raintegration_get_menu(client); + if (menu && menu->num_items) + { + const rc_client_raintegration_menu_item_t* menuitem = menu->items; + const rc_client_raintegration_menu_item_t* stop = menu->items + menu->num_items; + + for (; menuitem < stop; ++menuitem) + { + if (menuitem->id == 0) + AppendMenuA(hPopupMenu, MF_SEPARATOR, 0U, NULL); + else + { + UINT flags = MF_STRING; + if (menuitem->checked) + flags |= MF_CHECKED; + if (!menuitem->enabled) + flags |= MF_GRAYED; + + AppendMenuA(hPopupMenu, flags, menuitem->id, menuitem->label); + } + } + } + + /* add/update the item containing the popup menu */ + { + int nIndex = GetMenuItemCount(hMenu); + const char* menuText = "&RetroAchievements"; + char buffer[64]; + + UINT flags = MF_POPUP | MF_STRING; + if (!menu || !menu->num_items) + flags |= MF_GRAYED; + + while (--nIndex >= 0) + { + if (GetMenuStringA(hMenu, nIndex, buffer, sizeof(buffer) - 1, MF_BYPOSITION)) + { + if (strcmp(buffer, menuText) == 0) + break; + } + } + + if (nIndex == -1) + AppendMenuA(hMenu, flags, (UINT_PTR)hPopupMenu, menuText); + else + ModifyMenuA(hMenu, nIndex, flags | MF_BYPOSITION, (UINT_PTR)hPopupMenu, menuText); + + if (client->state.raintegration->hMainWindow && GetMenu(client->state.raintegration->hMainWindow) == hMenu) + DrawMenuBar(client->state.raintegration->hMainWindow); + } + + client->state.raintegration->hPopupMenu = hPopupMenu; +} + +void rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menuitem) +{ + if (client && client->state.raintegration && client->state.raintegration->hPopupMenu) + { + UINT flags = MF_STRING; + if (menuitem->checked) + flags |= MF_CHECKED; + + CheckMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); + + flags = (menuitem->enabled) ? MF_ENABLED : MF_GRAYED; + EnableMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); + } +} + +int rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t menu_item_id) +{ + if (!client || !client->state.raintegration || !client->state.raintegration->activate_menu_item) + return 0; + + return client->state.raintegration->activate_menu_item(menu_item_id); +} + +void rc_client_unload_raintegration(rc_client_t* client) +{ + HINSTANCE hDLL; + + if (!client || !client->state.raintegration) + return; + + RC_CLIENT_LOG_INFO(client, "Unloading RA_Integration") + + if (client->state.external_client && client->state.external_client->destroy) + client->state.external_client->destroy(); + + if (client->state.raintegration->shutdown) + client->state.raintegration->shutdown(); + + rc_mutex_lock(&client->state.mutex); + hDLL = client->state.raintegration->hDLL; + client->state.raintegration = NULL; + client->state.external_client = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (hDLL) + FreeLibrary(hDLL); +} + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ diff --git a/src/rcheevos/src/rc_client_raintegration_internal.h b/src/rcheevos/src/rc_client_raintegration_internal.h new file mode 100644 index 0000000000..ce3a99dda5 --- /dev/null +++ b/src/rcheevos/src/rc_client_raintegration_internal.h @@ -0,0 +1,61 @@ +#ifndef RC_CLIENT_RAINTEGRATION_INTERNAL_H +#define RC_CLIENT_RAINTEGRATION_INTERNAL_H + +#include "rc_client_raintegration.h" + +#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + +#include "rc_client_external.h" +#include "rc_compat.h" + +RC_BEGIN_C_DECLS + +/* RAIntegration follows the same calling convention as rcheevos */ + +typedef void (RC_CCONV* rc_client_raintegration_action_func_t)(void); +typedef const char* (RC_CCONV* rc_client_raintegration_get_string_func_t)(void); +typedef int (RC_CCONV* rc_client_raintegration_init_client_func_t)(HWND hMainWnd, const char* sClientName, const char* sClientVersion); +typedef int (RC_CCONV* rc_client_raintegration_get_external_client_func_t)(rc_client_external_t* pClient, int nVersion); +typedef void (RC_CCONV* rc_client_raintegration_hwnd_action_func_t)(HWND hWnd); +typedef int (RC_CCONV* rc_client_raintegration_get_achievement_state_func_t)(uint32_t nMenuItemId); +typedef const rc_client_raintegration_menu_t* (RC_CCONV* rc_client_raintegration_get_menu_func_t)(void); +typedef int (RC_CCONV* rc_client_raintegration_activate_menuitem_func_t)(uint32_t nMenuItemId); +typedef void (RC_CCONV* rc_client_raintegration_set_write_memory_func_t)(rc_client_t* pClient, rc_client_raintegration_write_memory_func_t handler); +typedef void (RC_CCONV* rc_client_raintegration_set_get_game_name_func_t)(rc_client_t* pClient, rc_client_raintegration_get_game_name_func_t handler); +typedef void (RC_CCONV* rc_client_raintegration_set_event_handler_func_t)(rc_client_t* pClient, rc_client_raintegration_event_handler_t handler); +typedef void (RC_CCONV* rc_client_raintegration_set_int_func_t)(int); +typedef int (RC_CCONV* rc_client_raintegration_get_int_func_t)(void); + +typedef struct rc_client_raintegration_t +{ + HINSTANCE hDLL; + HWND hMainWindow; + HMENU hPopupMenu; + uint8_t bIsInited; + + rc_client_raintegration_get_string_func_t get_version; + rc_client_raintegration_get_string_func_t get_host_url; + rc_client_raintegration_init_client_func_t init_client; + rc_client_raintegration_init_client_func_t init_client_offline; + rc_client_raintegration_set_int_func_t set_console_id; + rc_client_raintegration_action_func_t shutdown; + + rc_client_raintegration_hwnd_action_func_t update_main_window_handle; + + rc_client_raintegration_set_write_memory_func_t set_write_memory_function; + rc_client_raintegration_set_get_game_name_func_t set_get_game_name_function; + rc_client_raintegration_set_event_handler_func_t set_event_handler; + rc_client_raintegration_get_menu_func_t get_menu; + rc_client_raintegration_activate_menuitem_func_t activate_menu_item; + rc_client_raintegration_get_int_func_t has_modifications; + rc_client_raintegration_get_achievement_state_func_t get_achievement_state; + + rc_client_raintegration_get_external_client_func_t get_external_client; + +} rc_client_raintegration_t; + +RC_END_C_DECLS + +#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ + +#endif /* RC_CLIENT_RAINTEGRATION_INTERNAL_H */ diff --git a/src/rcheevos/src/rc_client_types.natvis b/src/rcheevos/src/rc_client_types.natvis new file mode 100644 index 0000000000..397d400c35 --- /dev/null +++ b/src/rcheevos/src/rc_client_types.natvis @@ -0,0 +1,396 @@ + + + + + + {{display_name={display_name,s} score={score}}} + + + {{title={title,s} id={id}}} + + + {{title={title,s} id={id}}} + + + {RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE} + {RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE} + {RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED} + {RC_CLIENT_ACHIEVEMENT_STATE_DISABLED} + unknown ({value}) + + + {RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE} + {RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE} + {RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL} + {RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL} + unknown ({value}) + + + {RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD} + {RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE} + {RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION} + {RC_CLIENT_ACHIEVEMENT_TYPE_WIN} + unknown ({value}) + + + {RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN} + {RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED} + {RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED} + {RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED} + {RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL} + {RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED} + {RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE} + {RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE} + {RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED} + unknown ({value}) + + + {RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE} + {RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE} + {RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE} + {RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH} + unknown ({value}) + + + {{title={title,s} id={id}}} + + title + description + points + id + *((__rc_client_achievement_state_enum_t*)&state) + *((__rc_client_achievement_type_enum_t*)&state) + *((__rc_client_achievement_category_enum_t*)&category) + *((__rc_client_achievement_state_enum_t*)&bucket) + *((__rc_client_achievement_unlocked_enum_t*)&unlocked) + + + + {{label={label,s} count={num_achievements}}} + + + num_achievements + achievements[$i] + + + + + {{count={num_buckets}}} + + + num_buckets + buckets[$i] + + + + + {RC_CLIENT_LEADERBOARD_STATE_INACTIVE} + {RC_CLIENT_LEADERBOARD_STATE_ACTIVE} + {RC_CLIENT_LEADERBOARD_STATE_TRACKING} + {RC_CLIENT_LEADERBOARD_STATE_DISABLED} + unknown ({value}) + + + {RC_CLIENT_LEADERBOARD_FORMAT_TIME} + {RC_CLIENT_LEADERBOARD_FORMAT_SCORE} + {RC_CLIENT_LEADERBOARD_FORMAT_VALUE} + unknown ({value}) + + + {{title={title,s} id={id}}} + + title + description + tracker_value + id + *((__rc_client_leaderboard_state_enum_t*)&state) + *((__rc_client_leaderboard_format_enum_t*)&format) + *((__rc_bool_enum_t*)&lower_is_better) + + + + {{label={label,s} count={num_leaderboards}}} + + + num_leaderboards + leaderboards[$i] + + + + + {{count={num_buckets}}} + + + num_buckets + buckets[$i] + + + + + {{rank={rank} score={score,s} username={username}}} + + + {{leaderboard_id={leaderboard_id} num_entries={num_entries}}} + + leaderboard_id + submitted_score + best_score + new_rank + num_entries + + num_top_entries + top_entries[$i] + + + + + {RC_CLIENT_EVENT_TYPE_NONE} + {RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED} + {RC_CLIENT_EVENT_LEADERBOARD_STARTED} + {RC_CLIENT_EVENT_LEADERBOARD_FAILED} + {RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED} + {RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW} + {RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE} + {RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW} + {RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE} + {RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE} + {RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW} + {RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE} + {RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE} + {RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD} + {RC_CLIENT_EVENT_RESET} + {RC_CLIENT_EVENT_GAME_COMPLETED} + {RC_CLIENT_EVENT_SERVER_ERROR} + {RC_CLIENT_EVENT_DISCONNECTED} + {RC_CLIENT_EVENT_RECONNECTED} + {RC_CLIENT_EVENT_SUBSET_COMPLETED} + unknown ({value}) + + + {{type={*((__rc_client_event_type_enum_t*)&type)}}} + + *((__rc_client_event_type_enum_t*)&type) + *achievement + *leaderboard + *leaderboard + *leaderboard + *achievement + *achievement + *achievement + *achievement + *achievement + *leaderboard_tracker + *leaderboard_tracker + *leaderboard_tracker + *leaderboard_scoreboard + *leaderboard + *server_error + *subset + + + + {{count={info.public_.num_achievements}}} + + + info.public_.num_achievements + info.achievements[$i] + + + + + {{count={info.public_.num_leaderboards}}} + + + info.public_.num_leaderboards + info.leaderboards[$i] + + + + + {RC_CLIENT_MASTERY_STATE_NONE} + {RC_CLIENT_MASTERY_STATE_PENDING} + {RC_CLIENT_MASTERY_STATE_SHOWN} + unknown ({value}) + + + {{title={public_.title,s} id={public_.id}}} + + public_ + *((__rc_bool_enum_t*)&active) + *((__rc_client_mastery_state_enum_t*)&mastery) + *((__rc_client_subset_info_achievements_list_t*)this) + *((__rc_client_subset_info_leaderboards_list_t*)this) + + + + {{NULL}} + {(void**)&first,na} + + + first + next + this + + + + + {{NULL}} + {(void**)&first,na} + + + first + next + this + + + + + {{NULL}} + {(void**)&first,na} + + + first + next + this + + + + + {{title={public_.title,s} id={public_.id}}} + + + {{title={public_.title,s} id={public_.id}}} + + + {{title={public_.title,s} id={public_.id}}} + + public_ + *((__rc_client_subset_info_list_t*)&subsets) + *((__rc_client_media_hash_list_t*)&media_hash) + *((__rc_client_leaderboard_tracker_list_t*)&leaderboard_trackers) + progress_tracker + runtime + + + + {{hash={hash,s} game_id={game_id}}} + + + {client.hashes} + + + client.hashes + next + *this + + + + + {RC_CLIENT_LOAD_GAME_STATE_NONE} + {RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME} + {RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN} + {RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION} + {RC_CLIENT_LOAD_GAME_STATE_DONE} + {RC_CLIENT_LOAD_GAME_STATE_ABORTED} + unknown ({value}) + + + + *((__rc_client_load_game_state_enum_t*)&progress) + *game + subset + *hash + pending_media + start_session_response + (int)outstanding_requests + + + + {{when={when} callback={callback,na}}} + + + {state.scheduled_callbacks} + + + state.scheduled_callbacks + next + *this + + + + + {RC_CLIENT_LOG_LEVEL_NONE} + {RC_CLIENT_LOG_LEVEL_ERROR} + {RC_CLIENT_LOG_LEVEL_WARN} + {RC_CLIENT_LOG_LEVEL_INFO} + {RC_CLIENT_LOG_LEVEL_VERBOSE} + unknown ({value}) + + + {RC_CLIENT_USER_STATE_NONE} + {RC_CLIENT_USER_STATE_LOGIN_REQUESTED} + {RC_CLIENT_USER_STATE_LOGGED_IN} + unknown ({value}) + + + {RC_CLIENT_SPECTATOR_MODE_OFF} + {RC_CLIENT_SPECTATOR_MODE_ON} + {RC_CLIENT_SPECTATOR_MODE_LOCKED} + unknown ({value}) + + + {RC_CLIENT_DISCONNECT_HIDDEN} + {RC_CLIENT_DISCONNECT_VISIBLE} + {RC_CLIENT_DISCONNECT_SHOW_PENDING} + {RC_CLIENT_DISCONNECT_HIDE_PENDING} + {RC_CLIENT_DISCONNECT_VISIBLE|RC_CLIENT_DISCONNECT_HIDE_PENDING} + unknown ({value}) + + + + *((__rc_bool_enum_t*)&hardcore) + *((__rc_bool_enum_t*)&unofficial_enabled) + *((__rc_bool_enum_t*)&encore_mode) + *((__rc_client_spectator_mode_enum_t*)&spectator_mode) + *((__rc_client_disconnect_enum_t*)&disconnect) + *((__rc_client_log_level_enum_t*)&log_level) + *((__rc_client_user_state_enum_t*)&user) + *((__rc_client_scheduled_callback_list_t*)this) + host + load + + + + + game + *((__rc_client_game_hash_list_t*)this) + user + callbacks + state + + + + {{count={num_items}}} + + + num_items + items[$i] + + + + + {RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE} + {RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED} + {RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED} + {RC_CLIENT_RAINTEGRATION_EVENT_PAUSE} + {RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED} + unknown ({value}) + + + {{type={*((__rc_client_raintegration_event_enum_t*)&type)}}} + + *((__rc_client_raintegration_event_enum_t*)&type) + menu_item + + + diff --git a/src/rcheevos/src/rc_compat.c b/src/rcheevos/src/rc_compat.c new file mode 100644 index 0000000000..0ab9e4dda5 --- /dev/null +++ b/src/rcheevos/src/rc_compat.c @@ -0,0 +1,251 @@ +#if !defined(RC_NO_THREADS) && !defined(_WIN32) && !defined(GEKKO) && !defined(_3DS) && (!defined(_XOPEN_SOURCE) || (_XOPEN_SOURCE - 0) < 500) +/* We'll want to use pthread_mutexattr_settype/PTHREAD_MUTEX_RECURSIVE, but glibc only conditionally exposes pthread_mutexattr_settype and PTHREAD_MUTEX_RECURSIVE depending on feature flags + * Defining _XOPEN_SOURCE must be done at the top of the source file, before including any headers + * pthread_mutexattr_settype/PTHREAD_MUTEX_RECURSIVE are specified the Single UNIX Specification (Version 2, 1997), along with POSIX later on (IEEE Standard 1003.1-2008), so should cover practically any pthread implementation + */ +#undef _XOPEN_SOURCE +#define _XOPEN_SOURCE 500 +#endif + +#include "rc_compat.h" + +#include +#include +#include + +#ifdef RC_C89_HELPERS + +int rc_strncasecmp(const char* left, const char* right, size_t length) +{ + while (length) + { + if (*left != *right) + { + const int diff = tolower(*left) - tolower(*right); + if (diff != 0) + return diff; + } + + ++left; + ++right; + --length; + } + + return 0; +} + +int rc_strcasecmp(const char* left, const char* right) +{ + while (*left || *right) + { + if (*left != *right) + { + const int diff = tolower(*left) - tolower(*right); + if (diff != 0) + return diff; + } + + ++left; + ++right; + } + + return 0; +} + +char* rc_strdup(const char* str) +{ + const size_t length = strlen(str); + char* buffer = (char*)malloc(length + 1); + if (buffer) + memcpy(buffer, str, length + 1); + return buffer; +} + +int rc_snprintf(char* buffer, size_t size, const char* format, ...) +{ + int result; + va_list args; + + va_start(args, format); + +#ifdef __STDC_SECURE_LIB__ + result = vsprintf_s(buffer, size, format, args); +#else + /* assume buffer is large enough and ignore size */ + (void)size; + result = vsprintf(buffer, format, args); +#endif + + va_end(args); + + return result; +} + +#endif + +#ifndef __STDC_SECURE_LIB__ + +struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) +{ + struct tm* tm = gmtime(timer); + memcpy(buf, tm, sizeof(*tm)); + return buf; +} + +#endif + +#ifndef RC_NO_THREADS + +#if defined(_WIN32) + +/* https://learn.microsoft.com/en-us/archive/msdn-magazine/2012/november/windows-with-c-the-evolution-of-synchronization-in-windows-and-c */ +/* implementation largely taken from https://github.com/libsdl-org/SDL/blob/0fc3574/src/thread/windows/SDL_sysmutex.c */ + +#if defined(WINVER) && WINVER >= 0x0600 + +void rc_mutex_init(rc_mutex_t* mutex) +{ + InitializeSRWLock(&mutex->srw_lock); + /* https://learn.microsoft.com/en-us/windows/win32/procthread/thread-handles-and-identifiers */ + /* thread ids are never 0 */ + mutex->owner = 0; + mutex->count = 0; +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + /* Nothing to do here */ + (void)mutex; +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + DWORD current_thread = GetCurrentThreadId(); + if (mutex->owner == current_thread) { + ++mutex->count; + assert(mutex->count > 0); + } + else { + AcquireSRWLockExclusive(&mutex->srw_lock); + assert(mutex->owner == 0 && mutex->count == 0); + mutex->owner = current_thread; + mutex->count = 1; + } +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + if (mutex->owner == GetCurrentThreadId()) { + assert(mutex->count > 0); + if (--mutex->count == 0) { + mutex->owner = 0; + ReleaseSRWLockExclusive(&mutex->srw_lock); + } + } + else { + assert(!"Tried to unlock unowned mutex"); + } +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + InitializeCriticalSection(&mutex->critical_section); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + DeleteCriticalSection(&mutex->critical_section); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + EnterCriticalSection(&mutex->critical_section); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + LeaveCriticalSection(&mutex->critical_section); +} + +#endif + +#elif defined(GEKKO) + +/* https://github.com/libretro/RetroArch/pull/16116 */ + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* LWP_MutexInit has the handle passed by reference */ + /* Other LWP_Mutex* calls have the handle passed by value */ + LWP_MutexInit(&mutex->handle, 1); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + LWP_MutexDestroy(mutex->handle); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + LWP_MutexLock(mutex->handle); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + LWP_MutexUnlock(mutex->handle); +} + +#elif defined(_3DS) + +void rc_mutex_init(rc_mutex_t* mutex) +{ + RecursiveLock_Init(mutex); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + /* Nothing to do here */ + (void)mutex; +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + RecursiveLock_Lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + RecursiveLock_Unlock(mutex); +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* Define the mutex as recursive, for consistent semantics against other rc_mutex_t implementations */ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(mutex, &attr); + pthread_mutexattr_destroy(&attr); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + pthread_mutex_destroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + pthread_mutex_lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + pthread_mutex_unlock(mutex); +} + +#endif +#endif /* RC_NO_THREADS */ diff --git a/src/rcheevos/src/rc_compat.h b/src/rcheevos/src/rc_compat.h new file mode 100644 index 0000000000..3f24df2efd --- /dev/null +++ b/src/rcheevos/src/rc_compat.h @@ -0,0 +1,121 @@ +#ifndef RC_COMPAT_H +#define RC_COMPAT_H + +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#endif + +#include "rc_export.h" + +#include +#include +#include + +RC_BEGIN_C_DECLS + +#if defined(MINGW) || defined(__MINGW32__) || defined(__MINGW64__) + +/* MinGW redefinitions */ + +#define RC_NO_VARIADIC_MACROS 1 + +#elif defined(_MSC_VER) + +/* Visual Studio redefinitions */ + +#ifndef strcasecmp + #define strcasecmp _stricmp +#endif +#ifndef strncasecmp + #define strncasecmp _strnicmp +#endif +#ifndef strdup + #define strdup _strdup +#endif + +#elif __STDC_VERSION__ < 199901L + +/* C89 redefinitions */ +#define RC_C89_HELPERS 1 + +#define RC_NO_VARIADIC_MACROS 1 + +#ifndef snprintf + extern int rc_snprintf(char* buffer, size_t size, const char* format, ...); + #define snprintf rc_snprintf +#endif + +#ifndef strncasecmp + extern int rc_strncasecmp(const char* left, const char* right, size_t length); + #define strncasecmp rc_strncasecmp +#endif + +#ifndef strcasecmp + extern int rc_strcasecmp(const char* left, const char* right); + #define strcasecmp rc_strcasecmp +#endif + +#ifndef strdup + extern char* rc_strdup(const char* str); + #define strdup rc_strdup +#endif + +#endif /* __STDC_VERSION__ < 199901L */ + +#ifndef __STDC_SECURE_LIB__ + /* _CRT_SECURE_NO_WARNINGS redefinitions */ + #define strcpy_s(dest, sz, src) strcpy(dest, src) + #define sscanf_s sscanf + + /* NOTE: Microsoft secure gmtime_s parameter order differs from C11 standard */ + #include + extern struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer); + #define gmtime_s rc_gmtime_s +#endif + +#ifdef RC_NO_THREADS + typedef int rc_mutex_t; + + #define rc_mutex_init(mutex) + #define rc_mutex_destroy(mutex) + #define rc_mutex_lock(mutex) + #define rc_mutex_unlock(mutex) +#else + #if defined(_WIN32) + typedef struct rc_mutex_t { + #if defined(WINVER) && WINVER >= 0x0600 + /* Windows Vista and later can use a slim reader/writer (SRW) lock */ + SRWLOCK srw_lock; + /* Current thread owner needs to be tracked (for recursive mutex usage) */ + DWORD owner; + DWORD count; + #else + /* Pre-Vista must use a critical section */ + CRITICAL_SECTION critical_section; + #endif + } rc_mutex_t; + #elif defined(GEKKO) + #include + typedef struct rc_mutex_t { + mutex_t handle; + } rc_mutex_t; + #elif defined(_3DS) + #include <3ds/synchronization.h> + typedef RecursiveLock rc_mutex_t; + #else + #include + typedef pthread_mutex_t rc_mutex_t; + #endif + + void rc_mutex_init(rc_mutex_t* mutex); + void rc_mutex_destroy(rc_mutex_t* mutex); + void rc_mutex_lock(rc_mutex_t* mutex); + void rc_mutex_unlock(rc_mutex_t* mutex); +#endif + +RC_END_C_DECLS + +#endif /* RC_COMPAT_H */ diff --git a/src/rcheevos/src/rc_libretro.c b/src/rcheevos/src/rc_libretro.c new file mode 100644 index 0000000000..e04c8d2f4f --- /dev/null +++ b/src/rcheevos/src/rc_libretro.c @@ -0,0 +1,915 @@ +/* This file provides a series of functions for integrating RetroAchievements with libretro. + * These functions will be called by a libretro frontend to validate certain expected behaviors + * and simplify mapping core data to the RAIntegration DLL. + * + * Originally designed to be shared between RALibretro and RetroArch, but will simplify + * integrating with any other frontends. + */ + +#include "rc_libretro.h" + +#include "rc_consoles.h" +#include "rc_compat.h" +#include "rhash/rc_hash_internal.h" + +#include +#include + +static rc_libretro_message_callback rc_libretro_verbose_message_callback = NULL; + +/* a value that starts with a comma is a CSV. + * if it starts with an exclamation point, it's everything but the provided value. + * if it starts with an exclamntion point followed by a comma, it's everything but the CSV values. + * values are case-insensitive */ +typedef struct rc_disallowed_core_settings_t +{ + const char* library_name; + const rc_disallowed_setting_t* disallowed_settings; +} rc_disallowed_core_settings_t; + + +static const rc_disallowed_setting_t _rc_disallowed_beetle_psx_settings[] = { + { "beetle_psx_cpu_freq_scale", "<100" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_beetle_psx_hw_settings[] = { + { "beetle_psx_hw_cpu_freq_scale", "<100" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { + { "bsnes_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_cap32_settings[] = { + { "cap32_autorun", "disabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = { + { "dolphin_cheats_enabled", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dosbox_pure_settings[] = { + { "dosbox_pure_strict_mode", "false" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = { + { "duckstation_CDROM.LoadImagePatches", "true" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { + { "ecwolf-invulnerability", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { + { "fbneo-allow-patched-romsets", "enabled" }, + { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, + { "fbneo-cpu-speed-adjust", "??%" }, /* disallow speeds under 100% */ + { "fbneo-dipswitch-*", "Universe BIOS*" }, + { "fbneo-neogeo-mode", "UNIBIOS" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { + { "fceumm_game_genie", "!disabled" }, + { "fceumm_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_flycast_settings[] = { + { "reicast_sh4clock", "<200" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { + { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_wide_settings[] = { + { "genesis_plus_gx_wide_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_wide_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_settings[] = { + { "mesen_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_s_settings[] = { + { "mesen-s_region", "PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_neocd_settings[] = { + { "neocd_bios", "uni-bios*" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { + { "pcsx_rearmed_psxclock", ",!auto,<55" }, + { "pcsx_rearmed_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_picodrive_settings[] = { + { "picodrive_region", ",Europe,Japan PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = { + { "ppsspp_cheats", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_quasi88_settings[] = { + { "q88_cpu_clock", ",1,2" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_smsplus_settings[] = { + { "smsplus_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { + { "snes9x_gfx_clip", "disabled" }, + { "snes9x_gfx_transp", "disabled" }, + { "snes9x_layer_*", "disabled" }, + { "snes9x_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_swanstation_settings[] = { + { "swanstation_CPU_Overclock", "<100" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { + { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ + { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = { + { "virtualjaguar_pal", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { + { "Beetle PSX", _rc_disallowed_beetle_psx_settings }, + { "Beetle PSX HW", _rc_disallowed_beetle_psx_hw_settings }, + { "bsnes-mercury", _rc_disallowed_bsnes_settings }, + { "cap32", _rc_disallowed_cap32_settings }, + { "dolphin-emu", _rc_disallowed_dolphin_settings }, + { "DOSBox-pure", _rc_disallowed_dosbox_pure_settings }, + { "DuckStation", _rc_disallowed_duckstation_settings }, + { "ecwolf", _rc_disallowed_ecwolf_settings }, + { "FCEUmm", _rc_disallowed_fceumm_settings }, + { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, + { "Flycast", _rc_disallowed_flycast_settings }, + { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, + { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, + { "Mesen", _rc_disallowed_mesen_settings }, + { "Mesen-S", _rc_disallowed_mesen_s_settings }, + { "NeoCD", _rc_disallowed_neocd_settings }, + { "PPSSPP", _rc_disallowed_ppsspp_settings }, + { "PCSX-ReARMed", _rc_disallowed_pcsx_rearmed_settings }, + { "PicoDrive", _rc_disallowed_picodrive_settings }, + { "QUASI88", _rc_disallowed_quasi88_settings }, + { "SMS Plus GX", _rc_disallowed_smsplus_settings }, + { "Snes9x", _rc_disallowed_snes9x_settings }, + { "SwanStation", _rc_disallowed_swanstation_settings }, + { "VICE x64", _rc_disallowed_vice_settings }, + { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, + { NULL, NULL } +}; + +static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* match) { + char c1, c2; + while ((c1 = *test++)) { + if (tolower(c1) != tolower(c2 = *match++) && c2 != '?') + return (c2 == '*'); + } + + return (*match == '\0'); +} + +static int rc_libretro_numeric_less_than(const char* test, const char* value) { + int test_num = atoi(test); + int value_num = atoi(value); + return (test_num < value_num); +} + +static int rc_libretro_match_token(const char* val, const char* token, size_t size, int* result) { + if (*token == '!') { + /* !X => if X is a match, it's explicitly allowed. match with result = false */ + if (rc_libretro_match_token(val, token + 1, size - 1, result)) { + *result = 0; + return 1; + } + } + + if (*token == '<') { + /* if val < token, match with result = true */ + char buffer[128]; + memcpy(buffer, token + 1, size - 1); + buffer[size - 1] = '\0'; + if (rc_libretro_numeric_less_than(val, buffer)) { + *result = 1; + return 1; + } + } + + if (memcmp(token, val, size) == 0 && val[size] == 0) { + /* exact match, match with result = true */ + *result = 1; + return 1; + } + else { + /* check for case insensitive match */ + char buffer[128]; + memcpy(buffer, token, size); + buffer[size] = '\0'; + if (rc_libretro_string_equal_nocase_wildcard(val, buffer)) { + /* case insensitive match, match with result = true */ + *result = 1; + return 1; + } + } + + /* no match */ + return 0; +} + +static int rc_libretro_match_value(const char* val, const char* match) { + int result = 0; + + /* if value starts with a comma, it's a CSV list of potential matches */ + if (*match == ',') { + do { + const char* ptr = ++match; + size_t size; + + while (*match && *match != ',') + ++match; + + size = match - ptr; + if (rc_libretro_match_token(val, ptr, size, &result)) + return result; + + } while (*match == ','); + } + else { + /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ + if (*match == '!') + return !rc_libretro_match_value(val, &match[1]); + + /* just a single value, attempt to match it */ + if (rc_libretro_match_token(val, match, strlen(match), &result)) + return result; + } + + /* value did not match filters, assume it's allowed */ + return 0; +} + +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { + const char* key; + size_t key_len; + + for (; disallowed_settings->setting; ++disallowed_settings) { + key = disallowed_settings->setting; + key_len = strlen(key); + + if (key[key_len - 1] == '*') { + if (memcmp(setting, key, key_len - 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + else { + if (memcmp(setting, key, key_len + 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + } + + return 1; +} + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name) { + const rc_disallowed_core_settings_t* core_filter = rc_disallowed_core_settings; + size_t library_name_length; + + if (!library_name || !library_name[0]) + return NULL; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) + return core_filter->disallowed_settings; + + ++core_filter; + } + + return NULL; +} + +typedef struct rc_disallowed_core_systems_t +{ + const char* library_name; + const uint32_t disallowed_consoles[4]; +} rc_disallowed_core_systems_t; + +static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { + /* https://github.com/libretro/Mesen-S/issues/8 */ + { "Mesen-S", { RC_CONSOLE_GAMEBOY, RC_CONSOLE_GAMEBOY_COLOR, 0 }}, + { NULL, { 0 } } +}; + +int rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id) { + const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; + size_t library_name_length; + size_t i; + + if (!library_name || !library_name[0]) + return 1; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) { + for (i = 0; i < sizeof(core_filter->disallowed_consoles) / sizeof(core_filter->disallowed_consoles[0]); ++i) { + if (core_filter->disallowed_consoles[i] == console_id) + return 0; + } + break; + } + + ++core_filter; + } + + return 1; +} + +uint8_t* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail) { + uint32_t i; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + if (avail) + *avail = (uint32_t)(size - address); + + return ®ions->data[i][address]; + } + + address -= (uint32_t)size; + } + + if (avail) + *avail = 0; + + return NULL; +} + +uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address) { + return rc_libretro_memory_find_avail(regions, address, NULL); +} + +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, + uint8_t* buffer, uint32_t num_bytes) { + uint32_t bytes_read = 0; + uint32_t avail; + uint32_t i; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address >= size) { + /* address is not in this block, adjust and look at next block */ + address -= (unsigned)size; + continue; + } + + if (regions->data[i] == NULL) /* no memory associated to this block. abort */ + break; + + avail = (unsigned)(size - address); + if (avail >= num_bytes) { + /* requested memory is fully within this block, copy and return it */ + memcpy(buffer, ®ions->data[i][address], num_bytes); + bytes_read += num_bytes; + return bytes_read; + } + + /* copy whatever is available in this block, and adjust for the next block */ + memcpy(buffer, ®ions->data[i][address], avail); + buffer += avail; + bytes_read += avail; + num_bytes -= avail; + address = 0; + } + + return bytes_read; +} + +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { + rc_libretro_verbose_message_callback = callback; +} + +static void rc_libretro_verbose(const char* message) { + if (rc_libretro_verbose_message_callback) + rc_libretro_verbose_message_callback(message); +} + +static const char* rc_memory_type_str(int type) { + switch (type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return "SRAM"; + case RC_MEMORY_TYPE_VIDEO_RAM: + return "VRAM"; + case RC_MEMORY_TYPE_UNUSED: + return "UNUSED"; + default: + break; + } + + return "SYSTEM RAM"; +} + +static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, + uint8_t* data, size_t size, const char* description) { + if (size == 0) + return; + + if (regions->count == (sizeof(regions->size) / sizeof(regions->size[0]))) { + rc_libretro_verbose("Too many memory memory regions to register"); + return; + } + + if (!data && regions->count > 0 && !regions->data[regions->count - 1]) { + /* extend null region */ + regions->size[regions->count - 1] += size; + } + else if (data && regions->count > 0 && + data == (regions->data[regions->count - 1] + regions->size[regions->count - 1])) { + /* extend non-null region */ + regions->size[regions->count - 1] += size; + } + else { + /* create new region */ + regions->data[regions->count] = data; + regions->size[regions->count] = size; + ++regions->count; + } + + regions->total_size += size; + + if (rc_libretro_verbose_message_callback) { + char message[128]; + snprintf(message, sizeof(message), "Registered 0x%04X bytes of %s at $%06X (%s)", (unsigned)size, + rc_memory_type_str(type), (unsigned)(regions->total_size - size), description); + rc_libretro_verbose_message_callback(message); + } +} + +static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info) { + /* no regions specified, assume system RAM followed by save RAM */ + char description[64]; + rc_libretro_core_memory_info_t info; + + snprintf(description, sizeof(description), "offset 0x%06x", 0); + + get_core_memory_info(RETRO_MEMORY_SYSTEM_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SYSTEM_RAM, info.data, info.size, description); + + get_core_memory_info(RETRO_MEMORY_SAVE_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); +} + +static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, uint32_t real_address, size_t* offset) +{ + const struct retro_memory_descriptor* desc = mmap->descriptors; + const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; + + for (; desc < end; desc++) { + if (desc->select == 0) { + /* if select is 0, attempt to explcitly match the address */ + if (real_address >= desc->start && real_address < desc->start + desc->len) { + *offset = real_address - desc->start; + return desc; + } + } + else { + /* otherwise, attempt to match the address by matching the select bits */ + /* address is in the block if (addr & select) == (start & select) */ + if (((desc->start ^ real_address) & desc->select) == 0) { + /* get the relative offset of the address from the start of the memory block */ + uint32_t reduced_address = real_address - (unsigned)desc->start; + + /* remove any bits from the reduced_address that correspond to the bits in the disconnect + * mask and collapse the remaining bits. this code was copied from the mmap_reduce function + * in RetroArch. i'm not exactly sure how it works, but it does. */ + uint32_t disconnect_mask = (unsigned)desc->disconnect; + while (disconnect_mask) { + const uint32_t tmp = (disconnect_mask - 1) & ~disconnect_mask; + reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); + disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; + } + + /* calculate the offset within the descriptor */ + *offset = reduced_address; + + /* sanity check - make sure the descriptor is large enough to hold the target address */ + if (reduced_address < desc->len) + return desc; + } + } + } + + *offset = 0; + return NULL; +} + +static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + const rc_memory_regions_t* console_regions) { + char description[64]; + uint32_t i; + uint8_t* region_start; + uint8_t* desc_start; + size_t desc_size; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + size_t console_region_size = console_region->end_address - console_region->start_address + 1; + uint32_t real_address = console_region->real_address; + uint32_t disconnect_size = 0; + + while (console_region_size > 0) { + const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); + if (!desc) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + (unsigned)(real_address - console_region->real_address + console_region->start_address)); + rc_libretro_verbose(description); + } + + if (disconnect_size && console_region_size > disconnect_size) { + rc_libretro_memory_register_region(regions, console_region->type, NULL, disconnect_size, "null filler"); + console_region_size -= disconnect_size; + real_address += disconnect_size; + disconnect_size = 0; + continue; + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + break; + } + + snprintf(description, sizeof(description), "descriptor %u, offset 0x%06X%s", + (unsigned)(desc - mmap->descriptors) + 1, (int)offset, desc->ptr ? "" : " [no pointer]"); + + if (desc->ptr) { + desc_start = (uint8_t*)desc->ptr + desc->offset; + region_start = desc_start + offset; + } + else { + region_start = NULL; + } + + desc_size = desc->len - offset; + if (desc->disconnect && desc_size > desc->disconnect) { + /* if we need to extract a disconnect bit, the largest block we can read is up to + * the next time that bit flips */ + /* https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit */ + disconnect_size = (desc->disconnect & -((int)desc->disconnect)); + desc_size = disconnect_size - (real_address & (disconnect_size - 1)); + } + + if (console_region_size > desc_size) { + if (desc_size == 0) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + (unsigned)(real_address - console_region->real_address + console_region->start_address)); + rc_libretro_verbose(description); + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + console_region_size = 0; + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, desc_size, description); + console_region_size -= desc_size; + real_address += (unsigned)desc_size; + } + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, console_region_size, description); + console_region_size = 0; + } + } + } +} + +static uint32_t rc_libretro_memory_console_region_to_ram_type(uint8_t region_type) { + switch (region_type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return RETRO_MEMORY_SAVE_RAM; + case RC_MEMORY_TYPE_VIDEO_RAM: + return RETRO_MEMORY_VIDEO_RAM; + default: + break; + } + + return RETRO_MEMORY_SYSTEM_RAM; +} + +static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { + char description[64]; + uint32_t i, j; + rc_libretro_core_memory_info_t info; + size_t offset; + int found_aligning_padding = 0; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + const size_t console_region_size = console_region->end_address - console_region->start_address + 1; + const uint32_t type = rc_libretro_memory_console_region_to_ram_type(console_region->type); + uint32_t base_address = 0; + + if (console_region->type == RC_MEMORY_TYPE_UNUSED && console_region_size >= 0x10000 && !found_aligning_padding) { + if (console_regions->region[console_regions->num_regions - 1].end_address > 0x01000000) { + /* assume anything exposing more than 16MB of regions with at least one 64KB+ UNUSED region + * is padding so things align with real addresses. this indicates the memory is disjoint + * in the system, so we cannot expect it to be contiguous in the RETRO_SYSTEM_RAM. + * stop processing regions now, and just fill the remaining memory map with null filler. */ + found_aligning_padding = 1; + } + } + + for (j = 0; j <= i; ++j) { + const rc_memory_region_t* console_region2 = &console_regions->region[j]; + if (rc_libretro_memory_console_region_to_ram_type(console_region2->type) == type) { + base_address = console_region2->start_address; + break; + } + } + offset = console_region->start_address - base_address; + + if (!found_aligning_padding) { + get_core_memory_info(type, &info); + } + else { + info.data = NULL; + info.size = console_region_size; + } + + if (offset < info.size) { + info.size -= offset; + + if (info.data) { + snprintf(description, sizeof(description), "offset 0x%06X", (int)offset); + info.data += offset; + } + else { + snprintf(description, sizeof(description), "null filler"); + } + } + else { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", (unsigned)console_region->start_address); + rc_libretro_verbose(description); + } + + info.data = NULL; + info.size = 0; + } + + if (console_region_size > info.size) { + /* want more than what is available, take what we can and null fill the rest */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, info.size, description); + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size - info.size, "null filler"); + } + else { + /* only take as much as we need */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, console_region_size, description); + } + } +} + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, uint32_t console_id) { + const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); + rc_libretro_memory_regions_t new_regions; + int has_valid_region = 0; + uint32_t i; + + if (!regions) + return 0; + + memset(&new_regions, 0, sizeof(new_regions)); + + if (console_regions == NULL || console_regions->num_regions == 0) + rc_libretro_memory_init_without_regions(&new_regions, get_core_memory_info); + else if (mmap && mmap->num_descriptors != 0) + rc_libretro_memory_init_from_memory_map(&new_regions, mmap, console_regions); + else + rc_libretro_memory_init_from_unmapped_memory(&new_regions, get_core_memory_info, console_regions); + + /* determine if any valid regions were found */ + for (i = 0; i < new_regions.count; i++) { + if (new_regions.data[i]) { + has_valid_region = 1; + break; + } + } + + memcpy(regions, &new_regions, sizeof(*regions)); + return has_valid_region; +} + +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions) { + memset(regions, 0, sizeof(*regions)); +} + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path, + const rc_hash_filereader_t* file_reader) { + char image_path[1024]; + char* m3u_contents; + char* ptr; + int64_t file_len; + void* file_handle; + int index = 0; + + memset(hash_set, 0, sizeof(*hash_set)); + + if (!rc_path_compare_extension(m3u_path, "m3u")) + return; + + file_handle = file_reader->open(m3u_path); + if (!file_handle) { + rc_hash_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + memcpy(&iterator.callbacks, &hash_set->callbacks, sizeof(hash_set->callbacks)); + rc_hash_iterator_error(&iterator, "Could not open playlist"); + return; + } + + file_reader->seek(file_handle, 0, SEEK_END); + file_len = file_reader->tell(file_handle); + file_reader->seek(file_handle, 0, SEEK_SET); + + m3u_contents = (char*)malloc((size_t)file_len + 1); + if (m3u_contents) { + file_reader->read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + ptr = m3u_contents; + do + { + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') { + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) { + /* get the path to the save disk from the frontend, assign it a bogus hash so + * it doesn't get hashed later */ + if (get_image_path(index, image_path, sizeof(image_path))) { + const char save_disk_hash[33] = "[SAVE DISK]"; + rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); + ++index; + } + } + } + else { + /* non-empty line, tally a file */ + ++index; + } + + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; + + } while (*ptr); + + free(m3u_contents); + } + + if (file_reader->close) + file_reader->close(file_handle); + + if (hash_set->entries_count > 0) { + /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by + * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */ + if (!get_image_path(index - 1, image_path, sizeof(image_path))) + hash_set->entries_count = 0; + } +} + +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { + if (hash_set->entries) + free(hash_set->entries); + memset(hash_set, 0, sizeof(*hash_set)); +} + +static uint32_t rc_libretro_djb2(const char* input) +{ + uint32_t result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, uint32_t game_id, const char hash[33]) { + const uint32_t path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; + struct rc_libretro_hash_entry_t* entry = NULL; + struct rc_libretro_hash_entry_t* scan; + struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; + + if (path_djb2) { + /* attempt to match the path */ + for (scan = hash_set->entries; scan < stop; ++scan) { + if (scan->path_djb2 == path_djb2) { + entry = scan; + break; + } + } + } + + if (!entry) + { + /* entry not found, allocate a new one */ + if (hash_set->entries_size == 0) { + hash_set->entries_size = 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*) + malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + else if (hash_set->entries_count == hash_set->entries_size) { + hash_set->entries_size += 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries, + hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + + if (hash_set->entries == NULL) /* unexpected, but better than crashing */ + return; + + entry = hash_set->entries + hash_set->entries_count++; + } + + /* update the entry */ + entry->path_djb2 = path_djb2; + entry->game_id = game_id; + memcpy(entry->hash, hash, sizeof(entry->hash)); +} + +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) +{ + const uint32_t path_djb2 = rc_libretro_djb2(path); + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) { + if (scan->path_djb2 == path_djb2) + return scan->hash; + } + + return NULL; +} + +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash) +{ + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) { + if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0) + return scan->game_id; + } + + return 0; +} diff --git a/src/rcheevos/src/rc_libretro.h b/src/rcheevos/src/rc_libretro.h new file mode 100644 index 0000000000..8821303218 --- /dev/null +++ b/src/rcheevos/src/rc_libretro.h @@ -0,0 +1,98 @@ +#ifndef RC_LIBRETRO_H +#define RC_LIBRETRO_H + +#include "rc_export.h" + +#include "rc_hash.h" + +/* this file comes from the libretro repository, which is not an explicit submodule. + * the integration must set up paths appropriately to find it. */ +#include + +#include +#include + +RC_BEGIN_C_DECLS + +/*****************************************************************************\ +| Disallowed Settings | +\*****************************************************************************/ + +typedef struct rc_disallowed_setting_t +{ + const char* setting; + const char* value; +} rc_disallowed_setting_t; + +RC_EXPORT const rc_disallowed_setting_t* RC_CCONV rc_libretro_get_disallowed_settings(const char* library_name); +RC_EXPORT int RC_CCONV rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); +RC_EXPORT int RC_CCONV rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id); + +/*****************************************************************************\ +| Memory Mapping | +\*****************************************************************************/ + +/* specifies a function to call for verbose logging */ +typedef void (RC_CCONV *rc_libretro_message_callback)(const char*); +RC_EXPORT void RC_CCONV rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback); + +#define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 +typedef struct rc_libretro_memory_regions_t +{ + uint8_t* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t total_size; + uint32_t count; +} rc_libretro_memory_regions_t; + +typedef struct rc_libretro_core_memory_info_t +{ + uint8_t* data; + size_t size; +} rc_libretro_core_memory_info_t; + +typedef void (RC_CCONV *rc_libretro_get_core_memory_info_func)(uint32_t id, rc_libretro_core_memory_info_t* info); + +RC_EXPORT int RC_CCONV rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, uint32_t console_id); +RC_EXPORT void RC_CCONV rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); + +RC_EXPORT uint8_t* RC_CCONV rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address); +RC_EXPORT uint8_t* RC_CCONV rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail); +RC_EXPORT uint32_t RC_CCONV rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, uint8_t* buffer, uint32_t num_bytes); + +/*****************************************************************************\ +| Disk Identification | +\*****************************************************************************/ + +typedef struct rc_libretro_hash_entry_t +{ + uint32_t path_djb2; + uint32_t game_id; + char hash[33]; +} rc_libretro_hash_entry_t; + +typedef struct rc_libretro_hash_set_t +{ + struct rc_libretro_hash_entry_t* entries; + uint16_t entries_count; + uint16_t entries_size; + + rc_hash_callbacks_t callbacks; +} rc_libretro_hash_set_t; + +typedef int (RC_CCONV *rc_libretro_get_image_path_func)(uint32_t index, char* buffer, size_t buffer_size); + +RC_EXPORT void RC_CCONV rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path, + const rc_hash_filereader_t* file_reader); +RC_EXPORT void RC_CCONV rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); + +RC_EXPORT void RC_CCONV rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, uint32_t game_id, const char hash[33]); +RC_EXPORT const char* RC_CCONV rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); +RC_EXPORT int RC_CCONV rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); + +RC_END_C_DECLS + +#endif /* RC_LIBRETRO_H */ diff --git a/src/rcheevos/src/rc_util.c b/src/rcheevos/src/rc_util.c new file mode 100644 index 0000000000..cd750d2c3c --- /dev/null +++ b/src/rcheevos/src/rc_util.c @@ -0,0 +1,199 @@ +#include "rc_util.h" + +#include "rc_compat.h" +#include "rc_error.h" + +#include +#include + +#undef DEBUG_BUFFERS + +/* --- rc_buffer --- */ + +void rc_buffer_init(rc_buffer_t* buffer) +{ + buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; + buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; + buffer->chunk.next = NULL; + /* leave buffer->data uninitialized */ +} + +void rc_buffer_destroy(rc_buffer_t* buffer) +{ + rc_buffer_chunk_t* chunk; +#ifdef DEBUG_BUFFERS + int count = 0; + int wasted = 0; + int total = 0; +#endif + + /* first chunk is not allocated. skip it. */ + chunk = buffer->chunk.next; + + /* deallocate any additional buffers */ + while (chunk) + { + rc_buffer_chunk_t* next = chunk->next; +#ifdef DEBUG_BUFFERS + total += (int)(chunk->end - chunk->start); + wasted += (int)(chunk->end - chunk->write); + ++count; +#endif +#ifdef DEBUG_BUFFERS + printf("< free %p.%p\n", (void*)buffer, (void*)chunk); +#endif + free(chunk); + chunk = next; + } + +#ifdef DEBUG_BUFFERS + printf("-- %d allocated buffers (%d/%d used, %d wasted, %0.2f%% efficiency)\n", count, + total - wasted, total, wasted, (float)(100.0 - (wasted * 100.0) / total)); +#endif +} + +uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount) +{ + rc_buffer_chunk_t* chunk = &buffer->chunk; + size_t remaining; + while (chunk) + { + remaining = chunk->end - chunk->write; + if (remaining >= amount) + return chunk->write; + + if (!chunk->next) + { + /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated + * to the chunk header, and the remaining will be used for data. + */ + const size_t chunk_header_size = sizeof(rc_buffer_chunk_t); + const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; + chunk->next = (rc_buffer_chunk_t*)malloc(alloc_size); + if (!chunk->next) + break; + +#ifdef DEBUG_BUFFERS + printf("> alloc %p.%p\n", (void*)buffer, (void*)chunk->next); +#endif + + chunk->next->start = (uint8_t*)chunk->next + chunk_header_size; + chunk->next->write = chunk->next->start; + chunk->next->end = (uint8_t*)chunk->next + alloc_size; + chunk->next->next = NULL; + } + + chunk = chunk->next; + } + + return NULL; +} + +void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end) +{ + rc_buffer_chunk_t* chunk = &buffer->chunk; + do + { + if (chunk->write == start) + { + size_t offset = (end - chunk->start); + offset = (offset + 7) & ~7; + chunk->write = &chunk->start[offset]; + + if (chunk->write > chunk->end) + chunk->write = chunk->end; + break; + } + + chunk = chunk->next; + } while (chunk); +} + +void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount) +{ + uint8_t* ptr = rc_buffer_reserve(buffer, amount); + rc_buffer_consume(buffer, ptr, ptr + amount); + return (void*)ptr; +} + +char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len) +{ + uint8_t* dst = rc_buffer_reserve(buffer, len + 1); + memcpy(dst, src, len); + dst[len] = '\0'; + rc_buffer_consume(buffer, dst, dst + len + 2); + return (char*)dst; +} + +char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src) +{ + return rc_buffer_strncpy(buffer, src, strlen(src)); +} + +/* --- other --- */ + +void rc_format_md5(char checksum[33], const uint8_t digest[16]) +{ + snprintf(checksum, 33, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] + ); +} + +uint32_t rc_djb2(const char* input) +{ + uint32_t result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +const char* rc_error_str(int ret) +{ + switch (ret) { + case RC_OK: return "OK"; + case RC_INVALID_FUNC_OPERAND: return "Invalid function operand"; + case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; + case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; + case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; + case RC_INVALID_CONDITION_TYPE: return "Invalid condition type"; + case RC_INVALID_OPERATOR: return "Invalid operator"; + case RC_INVALID_REQUIRED_HITS: return "Invalid required hits"; + case RC_DUPLICATED_START: return "Duplicated start condition"; + case RC_DUPLICATED_CANCEL: return "Duplicated cancel condition"; + case RC_DUPLICATED_SUBMIT: return "Duplicated submit condition"; + case RC_DUPLICATED_VALUE: return "Duplicated value expression"; + case RC_DUPLICATED_PROGRESS: return "Duplicated progress expression"; + case RC_MISSING_START: return "Missing start condition"; + case RC_MISSING_CANCEL: return "Missing cancel condition"; + case RC_MISSING_SUBMIT: return "Missing submit condition"; + case RC_MISSING_VALUE: return "Missing value expression"; + case RC_INVALID_LBOARD_FIELD: return "Invalid field in leaderboard"; + case RC_MISSING_DISPLAY_STRING: return "Missing display string"; + case RC_OUT_OF_MEMORY: return "Out of memory"; + case RC_INVALID_VALUE_FLAG: return "Invalid flag in value expression"; + case RC_MISSING_VALUE_MEASURED: return "Missing measured flag in value expression"; + case RC_MULTIPLE_MEASURED: return "Multiple measured targets"; + case RC_INVALID_MEASURED_TARGET: return "Invalid measured target"; + case RC_INVALID_COMPARISON: return "Invalid comparison"; + case RC_INVALID_STATE: return "Invalid state"; + case RC_INVALID_JSON: return "Invalid JSON"; + case RC_API_FAILURE: return "API call failed"; + case RC_LOGIN_REQUIRED: return "Login required"; + case RC_NO_GAME_LOADED: return "No game loaded"; + case RC_HARDCORE_DISABLED: return "Hardcore disabled"; + case RC_ABORTED: return "Aborted"; + case RC_NO_RESPONSE: return "No response"; + case RC_ACCESS_DENIED: return "Access denied"; + case RC_INVALID_CREDENTIALS: return "Invalid credentials"; + case RC_EXPIRED_TOKEN: return "Expired token"; + case RC_INSUFFICIENT_BUFFER: return "Buffer not large enough"; + case RC_INVALID_VARIABLE_NAME: return "Invalid variable name"; + case RC_UNKNOWN_VARIABLE_NAME: return "Unknown variable name"; + case RC_NOT_FOUND: return "Not found"; + default: return "Unknown error"; + } +} diff --git a/src/rcheevos/src/rc_version.c b/src/rcheevos/src/rc_version.c new file mode 100644 index 0000000000..a29d0a5f52 --- /dev/null +++ b/src/rcheevos/src/rc_version.c @@ -0,0 +1,11 @@ +#include "rc_version.h" + +uint32_t rc_version(void) +{ + return RCHEEVOS_VERSION; +} + +const char* rc_version_string(void) +{ + return RCHEEVOS_VERSION_STRING; +} diff --git a/src/rcheevos/src/rc_version.h b/src/rcheevos/src/rc_version.h new file mode 100644 index 0000000000..18b91284de --- /dev/null +++ b/src/rcheevos/src/rc_version.h @@ -0,0 +1,32 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#include "rc_export.h" + +#include + +RC_BEGIN_C_DECLS + +#define RCHEEVOS_VERSION_MAJOR 12 +#define RCHEEVOS_VERSION_MINOR 1 +#define RCHEEVOS_VERSION_PATCH 0 + +#define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) +#define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) + +#define RCHEEVOS_MAKE_STRING(num) #num +#define RCHEEVOS_MAKE_VERSION_STRING(major, minor, patch) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) "." RCHEEVOS_MAKE_STRING(patch) +#define RCHEEVOS_MAKE_VERSION_STRING_SHORT(major, minor) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) + +#if RCHEEVOS_VERSION_PATCH > 0 + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) +#else + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING_SHORT(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR) +#endif + +RC_EXPORT uint32_t rc_version(void); +RC_EXPORT const char* rc_version_string(void); + +RC_END_C_DECLS + +#endif /* RC_VERSION_H */ From 9469e461d61abafed5a1afbd3f9f82c212aa6f70 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:17:42 +0100 Subject: [PATCH 31/41] rcheevos v4 --- src/rcheevos/include/module.modulemap | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/rcheevos/include/module.modulemap diff --git a/src/rcheevos/include/module.modulemap b/src/rcheevos/include/module.modulemap new file mode 100644 index 0000000000..2a53853df3 --- /dev/null +++ b/src/rcheevos/include/module.modulemap @@ -0,0 +1,70 @@ +module rcheevos { + umbrella header "rcheevos.h" + + module rapi { + module rc_api_editor { + header "rc_api_editor.h" + export * + } + + module rc_api_info { + header "rc_api_info.h" + export * + } + + module rc_api_request { + header "rc_api_request.h" + export * + } + + module rc_api_runtime { + header "rc_api_runtime.h" + export * + } + + module rc_api_user { + header "rc_api_user.h" + export * + } + + export * + } + + module rc_client { + header "rc_client.h" + export * + } + + explicit module rc_client_raintegration { + header "rc_client_raintegration.h" + export * + } + + module rc_consoles { + header "rc_consoles.h" + export * + } + + module rc_error { + header "rc_error.h" + export * + } + + module rc_runtime { + header "rc_runtime.h" + export * + } + + module rc_runtime_types { + header "rc_runtime_types.h" + export * + } + + module rc_hash { + header "rc_hash.h" + export * + } + + export * + module * { export * } +} From 7281d902a67554e03bb7d8abee98f3058dc551c5 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:22:47 +0100 Subject: [PATCH 32/41] somehow - snuck in --- src/frontend/qt_sdl/Window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 6da0d4a1ae..48a40eba7a 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -1814,7 +1814,7 @@ void MainWindow::onEnableCheats(bool checked) emuInstance->osdAddMessage(0xFFA0A0, "Cheats are disabled in Hardcore Mode"); actEnableCheats->setChecked(false); return; - }- + } localCfg.SetBool("EnableCheats", checked); emuThread->enableCheats(checked); From 4112671b71bff2f93d3801d261867c45b82027b2 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:28:45 +0100 Subject: [PATCH 33/41] core fixes and changes --- src/CMakeLists.txt | 123 +++++++++-------- src/NDS.cpp | 44 ++++--- src/NDS.h | 11 +- src/NDSCart.cpp | 37 +++--- src/NDSCart.h | 7 + src/RetroAchievements/RAClient.cpp | 205 ++++------------------------- src/RetroAchievements/RAClient.h | 64 ++------- 7 files changed, 159 insertions(+), 332 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a5cc1f1e67..60d3b2cef6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -226,71 +226,80 @@ if (ENABLE_JIT_PROFILING) endif() # --- RetroAchievements --- -message(STATUS "Adding RetroAchievements integration sources") +option(ENABLE_RETROACHIEVEMENTS "Enable RetroAchievements support" OFF) -target_sources(core PRIVATE - RetroAchievements/RAClient.cpp - RetroAchievements/RAClient.h - RetroAchievements/RAFunctions.h -) +if (ENABLE_RETROACHIEVEMENTS) + message(STATUS "RetroAchievements: ENABLED") -target_include_directories(core PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/RetroAchievements" - "${CMAKE_CURRENT_SOURCE_DIR}" -) + target_compile_definitions(core PUBLIC RETROACHIEVEMENTS_ENABLED HAVE_CURL CURL_STATICLIB) + + set(CURL_DEPENDENCY_NAMES + curl + nghttp2 + nghttp3 + ngtcp2 + ssh2 + psl + ssl + crypto + brotlidec + brotlicommon + brotlienc + zstd + idn2 + unistring + iconv + z + ) + + set(CURL_FOUND_LIBS "") + + foreach(LIB_NAME ${CURL_DEPENDENCY_NAMES}) + find_library(LIB_PATH_${LIB_NAME} NAMES ${LIB_NAME} REQUIRED) + + if(LIB_PATH_${LIB_NAME}) + message(STATUS "Found ${LIB_NAME}: ${LIB_PATH_${LIB_NAME}}") + list(APPEND CURL_FOUND_LIBS ${LIB_PATH_${LIB_NAME}}) + else() + message(FATAL_ERROR "Could not find library: ${LIB_NAME}") + endif() + endforeach() + + find_library(LIB_NGTCP2_CRYPTO NAMES ngtcp2_crypto_ossl ngtcp2_crypto_openssl) + if(LIB_NGTCP2_CRYPTO) + message(STATUS "Found ngtcp2 crypto: ${LIB_NGTCP2_CRYPTO}") + list(INSERT CURL_FOUND_LIBS 4 ${LIB_NGTCP2_CRYPTO}) + else() + message(FATAL_ERROR "Could not find ngtcp2_crypto (checked ossl and openssl variants)") + endif() + + target_link_libraries(core PRIVATE + ${CURL_FOUND_LIBS} + ws2_32 wldap32 crypt32 normaliz advapi32 secur32 bcrypt + ) -message(STATUS "RetroAchievements support enabled") + target_include_directories(core PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/RetroAchievements" + "${CMAKE_CURRENT_SOURCE_DIR}" + ) -# --- CURL & SSL Configuration --- -target_compile_definitions(core PRIVATE HAVE_CURL CURL_STATICLIB) + target_sources(core PRIVATE + RetroAchievements/RAClient.cpp + RetroAchievements/RAClient.h + RetroAchievements/RAFunctions.h + ) -if(DEFINED ENV{MSYSTEM_PREFIX}) - set(CURL_LDIR "$ENV{MSYSTEM_PREFIX}/lib") + file(GLOB_RECURSE RCH_ALL "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src/*.c") + list(FILTER RCH_ALL EXCLUDE REGEX "rc_libretro.c|rc_client_external.c|rhash/aes.c") + target_sources(core PRIVATE ${RCH_ALL}) + target_include_directories(core PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include" + "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src" + ) else() - set(CURL_LDIR "C:/msys64/ucrt64/lib") + message(STATUS "RetroAchievements: DISABLED (default)") endif() -set(NGTCP2_CRYPTO "${CURL_LDIR}/libngtcp2_crypto_ossl.a") - -target_link_libraries(core PRIVATE - "${CURL_LDIR}/libcurl.a" - "${CURL_LDIR}/libnghttp2.a" - "${CURL_LDIR}/libnghttp3.a" - "${CURL_LDIR}/libngtcp2.a" - "${NGTCP2_CRYPTO}" - "${CURL_LDIR}/libssh2.a" - "${CURL_LDIR}/libpsl.a" - "${CURL_LDIR}/libssl.a" - "${CURL_LDIR}/libcrypto.a" - "${CURL_LDIR}/libbrotlidec.a" - "${CURL_LDIR}/libbrotlicommon.a" - "${CURL_LDIR}/libbrotlienc.a" - "${CURL_LDIR}/libzstd.a" - "${CURL_LDIR}/libidn2.a" - "${CURL_LDIR}/libunistring.a" - "${CURL_LDIR}/libiconv.a" - "${CURL_LDIR}/libz.a" - ws2_32 wldap32 crypt32 normaliz advapi32 secur32 bcrypt -) - -# --- rcheevos --- -file(GLOB_RECURSE RCH_ALL - "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src/*.c" -) - -list(FILTER RCH_ALL EXCLUDE REGEX - "rc_libretro.c|rc_client_external.c|rhash/aes.c" -) - -target_sources(core PRIVATE ${RCH_ALL}) - -target_include_directories(core PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include" - "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src" -) - -target_compile_definitions(core PRIVATE RETROACHIEVEMENTS_ENABLED) - #if(CMAKE_BUILD_TYPE MATCHES "Debug") # set( # CMAKE_C_FLAGS diff --git a/src/NDS.cpp b/src/NDS.cpp index 2dccc7aa62..4a49802ec8 100644 --- a/src/NDS.cpp +++ b/src/NDS.cpp @@ -44,17 +44,14 @@ #include "DSi_DSP.h" #include "ARMJIT.h" #include "ARMJIT_Memory.h" -#include "RetroAchievements/RAClient.h" #include +#ifdef RETROACHIEVEMENTS_ENABLED +#include "RetroAchievements/RAClient.h" +#endif + namespace melonDS { - - namespace Config { - std::string RA_Username = ""; - std::string RA_Token = ""; - } - using namespace Platform; const s32 kMaxIterationCycles = 64; @@ -766,7 +763,6 @@ bool NDS::DoSavestate(Savestate* file) if (!file->Saving) { - ::RAContext::Get().DisableHardcore("Load state"); GPU.SetPowerCnt(PowerControl9); SPU.SetPowerCnt(PowerControl7 & 0x0001); @@ -785,21 +781,19 @@ bool NDS::DoSavestate(Savestate* file) void NDS::SetNDSCart(std::unique_ptr&& cart) { NDSCartSlot.SetCart(std::move(cart)); - // The existing cart will always be ejected; + // The existing cart will always be ejected; // if cart is null, then that's equivalent to ejecting a cart // without inserting a new one. - - if (NDSCartSlot.GetCart()) { #ifdef RETROACHIEVEMENTS_ENABLED - auto cart = NDSCartSlot.GetCart(); - if (cart) - { - const char* h = cart->GetRAHash(); - if (h && h[0]) - RAContext::Get().SetPendingGameHash(h); - } -#endif + if (ra) { + auto cart = NDSCartSlot.GetCart(); + if (cart) { + const char* h = cart->GetRAHash(); + if (h && h[0]) + ra->SetPendingGameHash(h); + } } + #endif } void NDS::SetNDSSave(const u8* savedata, u32 savelen) @@ -946,7 +940,11 @@ void NDS::RunSystemSleep(u64 timestamp) template u32 NDS::RunFrame() { - RAContext::Get().DoFrame(); + #ifdef RETROACHIEVEMENTS_ENABLED + if (ra) { + ra->DoFrame(); + } + #endif Current = this; FrameStartTimestamp = SysTimestamp; @@ -1622,7 +1620,11 @@ void NDS::MonitorARM9Jump(u32 addr) { Log(LogLevel::Info, "Game is now booting\n"); RunningGame = true; - RAContext::Get().AttachNDS(this); + #ifdef RETROACHIEVEMENTS_ENABLED + if (ra) { + ra->AttachNDS(this); + } + #endif } } } diff --git a/src/NDS.h b/src/NDS.h index aa67567f80..106fdc17ad 100644 --- a/src/NDS.h +++ b/src/NDS.h @@ -44,13 +44,13 @@ #include "DMA.h" #include "FreeBIOS.h" +#ifdef RETROACHIEVEMENTS_ENABLED +#include "RetroAchievements/RAClient.h" +#endif // when touching the main loop/timing code, pls test a lot of shit // with this enabled, to make sure it doesn't desync //#define DEBUG_CHECK_DESYNC -class RAContext; -extern RAContext* g_RAContext; - namespace melonDS { struct NDSArgs; @@ -251,8 +251,11 @@ class NDS #endif public: // TODO: Encapsulate the rest of these members - bool IsGameRunning() const { return RunningGame; } + #ifdef RETROACHIEVEMENTS_ENABLED RAContext* ra = nullptr; + void SetRAContext(RAContext* ctx) noexcept { ra = ctx; } + bool IsGameRunning() const { return RunningGame; } + #endif void* UserData; int ConsoleType; diff --git a/src/NDSCart.cpp b/src/NDSCart.cpp index 610b3806a5..dffc71e0ed 100644 --- a/src/NDSCart.cpp +++ b/src/NDSCart.cpp @@ -26,12 +26,12 @@ #include "melonDLDI.h" #include "FATStorage.h" #include "Utils.h" + +#ifdef RETROACHIEVEMENTS_ENABLED #include "RetroAchievements/RAClient.h" #include +#endif -namespace Config { - extern bool RA_Enabled; -} namespace melonDS { @@ -208,18 +208,25 @@ CartCommon::CartCommon(std::unique_ptr&& rom, u32 len, u32 chipid, bool ba CartType(type), UserData(userdata) { -if (ROM && ROMLength > 0) { - if (rc_hash_generate_from_buffer( - this->ra_hash, - RC_CONSOLE_NINTENDO_DS, - ROM.get(), - ROMLength)) - { - //printf("RetroAchievements: Hash gry to: %s\n", this->ra_hash); - - RAContext::Get().SetPendingGameHash(this->ra_hash); - } -} + #ifdef RETROACHIEVEMENTS_ENABLED + if (ROM && ROMLength > 0) + { + const bool ok = rc_hash_generate_from_buffer( + this->ra_hash, + RC_CONSOLE_NINTENDO_DS, + ROM.get(), + ROMLength + ); + + if (ok) + { + if (ra) + { + ra->SetPendingGameHash(this->ra_hash); + } + } + } + #endif memcpy(&Header, ROM.get(), sizeof(Header)); IsDSi = Header.IsDSi() && !badDSiDump; DSiBase = Header.DSiRegionStart << 19; diff --git a/src/NDSCart.h b/src/NDSCart.h index ae682ce901..c814db2cff 100644 --- a/src/NDSCart.h +++ b/src/NDSCart.h @@ -30,6 +30,10 @@ #include "FATStorage.h" #include "ROMList.h" +#ifdef RETROACHIEVEMENTS_ENABLED +#include "RetroAchievements/RAClient.h" +#endif + namespace melonDS { class NDS; @@ -76,8 +80,11 @@ struct NDSCartArgs class CartCommon { public: + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = nullptr; const char* GetRAHash() const { return ra_hash; } char ra_hash[33] = {0}; + #endif CartCommon(const u8* rom, u32 len, u32 chipid, bool badDSiDump, ROMListEntry romparams, CartType type, void* userdata); CartCommon(std::unique_ptr&& rom, u32 len, u32 chipid, bool badDSiDump, ROMListEntry romparams, CartType type, void* userdata); virtual ~CartCommon(); diff --git a/src/RetroAchievements/RAClient.cpp b/src/RetroAchievements/RAClient.cpp index f7c7596396..d20116c2e1 100644 --- a/src/RetroAchievements/RAClient.cpp +++ b/src/RetroAchievements/RAClient.cpp @@ -10,34 +10,25 @@ extern const unsigned char _accacert[]; extern const size_t _accacert_len; -static AchievementUnlockedCallback g_onAchievementUnlocked; -std::function m_onLogin; -std::string m_displayName; -void SetDisplayName(const char* name) +void RAContext::SetDisplayName(const char* name) { m_displayName = name ? name : ""; } const std::string& RAContext::GetDisplayName() const { - return m_displayName; + return RAContext::m_displayName; } static uint64_t g_ra_mem_reads = 0; static uint64_t g_ra_mem_logged = 0; - -// ================= cURL helper ================= - static size_t CurlWrite(void* ptr, size_t size, size_t nmemb, void* userdata) { std::string* s = static_cast(userdata); s->append(static_cast(ptr), size * nmemb); return size * nmemb; } - -// ================= RAContext ================= - bool RAContext::CanPause(uint32_t* frames_remaining) { - if (!client) return true; // Zmienione z m_Client na client + if (!client) return true; return rc_client_can_pause(client, frames_remaining) != 0; } @@ -47,13 +38,11 @@ void RAContext::SetPaused(bool paused) { RAContext::RAContext() : nds(nullptr), client(nullptr) { - curl_global_init(CURL_GLOBAL_ALL); } RAContext::~RAContext() { Shutdown(); - curl_global_cleanup(); } void RAContext::Init(melonDS::NDS* nds_) @@ -63,11 +52,8 @@ void RAContext::Init(melonDS::NDS* nds_) &RAContext::ReadMemory, &RAContext::ServerCall ); - // NDS = Nintendo DS - rc_client_set_userdata(client, this); // ← TO JEST KLUCZ + rc_client_set_userdata(client, this); rc_client_set_allow_background_memory_reads(client, 1); - - // 3. 🔥 TUTAJ WKLEJAMY OBSŁUGĘ ZDARZEŃ (EVENT HANDLER) 🔥 rc_client_set_event_handler(client, [](const rc_client_event_t* event, rc_client_t* client) { @@ -77,26 +63,24 @@ void RAContext::Init(melonDS::NDS* nds_) { const rc_client_achievement_t* ach = (const rc_client_achievement_t*)event->achievement; - printf("[RA] ACHIEVEMENT TRIGGERED LOCAL: %s\n", ach->title); + RAContext* ctx = static_cast(rc_client_get_userdata(client)); + if (!ctx) return; - if (g_onAchievementUnlocked) - { - g_onAchievementUnlocked( + if (ctx->m_onAchievementUnlocked) + ctx->m_onAchievementUnlocked( ach->title, ach->description, ach->badge_url - ); - } + ); break; } - default: break; } -}); + }); +}; + - //printf("[RA] Client initialized (rcheevos 12+)\n"); -} void RAContext::Shutdown() { @@ -108,11 +92,8 @@ void RAContext::Shutdown() void RAContext::DoFrame() { - // 1. Zostawiamy Twoje podstawowe warunki if (!nds || !client || m_IsPaused) return; - - // 2. Zostawiamy Twoją logikę sprawdzania hashu std::string currentHash = pendingGameHash.value_or(""); if (currentHash != m_lastHash) { gameLoaded = false; @@ -120,31 +101,19 @@ void RAContext::DoFrame() pendingLoadFailed = false; isLoading = false; } - - // 3. Zostawiamy Twoje blokady ładowania if (pendingLoadFailed || isLoading) return; - - // 4. Zostawiamy Twoje wywołanie LoadGame if (!gameLoaded && pendingGameHash.has_value() && nds->IsGameRunning()) { isLoading = true; LoadGame(pendingGameHash->c_str()); - return; // <--- Dodałem to, żeby nie leciał dalej w tej samej klatce + return; } - - // --- MOJA GŁÓWNA POPRAWKA PONIŻEJ --- - - // 5. Blokada: dopóki gra nie jest w pełni rozpoznana, nie mielimy klatek RA if (!gameLoaded) return; - - // 6. Twoje sprawdzenie, czy logika jest wymagana (teraz w dobrym miejscu) if (rc_client_is_processing_required(client) == 0) { return; } - - // 7. Dopiero na końcu faktyczne przetwarzanie klatki rc_client_do_frame(client); } // ================= Login / Load ================= @@ -153,11 +122,10 @@ void RAContext::DoFrame() { if (m_logged_in == v) return; - //printf("zalogowano i wykonano setloggedin"); m_logged_in = v; - if (m_onLogin) - m_onLogin(); + if (RAContext::m_onLogin) + RAContext::m_onLogin(); } static void LoginPasswordCallback(int result, const char* err, rc_client_t* client, void* userdata) @@ -165,21 +133,11 @@ static void LoginPasswordCallback(int result, const char* err, rc_client_t* clie RAContext* ctx = static_cast(userdata); if (result == RC_OK) { - const rc_client_user_t* user_info = rc_client_get_user_info(client); - printf("[RA] Logged in as %s\n", user_info->display_name); - if (ctx) - { - //printf("[RA] Login OK, waiting for game hash\n"); - } - //const rc_client_game_t* game = rc_client_get_game(client); - // Zapisz otrzymany token z RA + const rc_client_user_t* user_info = rc_client_get_user_info(client); ctx->SetToken(user_info->token ? user_info->token : ""); ctx->SetLoggedIn(true); - - // Możemy zapisać token w configu - //printf("[RA] New token saved: %s\n", ctx->GetToken().c_str()); if (ctx->onLoginResponse) { - ctx->onLoginResponse(true, "Zalogowano pomyślnie!"); + ctx->onLoginResponse(true, "Logged in Successfully!"); } } else @@ -187,7 +145,6 @@ static void LoginPasswordCallback(int result, const char* err, rc_client_t* clie ctx->loginSuccess = false; ctx->loginErrorMessage = err ? err : "Unknown error"; ctx->SetLoggedIn(false); - printf("[RA] Login failed: %s\n", err ? err : "unknown error"); if (ctx->onLoginResponse) { ctx->onLoginResponse(false, ctx->loginErrorMessage); } @@ -199,29 +156,21 @@ static void LoginPasswordCallback(int result, const char* err, rc_client_t* clie void RAContext::LoadGame(const char* hash) { if (!client) return; - //printf("[RA] LoadGame called with hash: %s\n", hash); - - // Dodajemy [this], aby lambda widziała zmienną gameLoaded rc_client_begin_load_game( client, hash, [](int result, const char* err, rc_client_t*, void* userdata) { - // Rzutujemy userdata z powrotem na RAContext*, aby mieć dostęp do klasy auto* context = static_cast(userdata); if (result == 0) { context->pendingLoadFailed = false; context->isLoading = false; - printf("[RA] Game loaded\n"); - context->gameLoaded = true; // Używamy wskaźnika przekazanego przez userdata + context->gameLoaded = true; context->currentGameInfo = rc_client_get_game_info(context->client); - //printf("[RA] Przzed Ongameloaded"); if (context->onGameLoaded){ - //printf("[RA] W Ongameloaded"); context->onGameLoaded();} } else { - printf("[RA] Load failed: %s\n", err); context->gameLoaded = false; context->pendingLoadError = err; context->pendingLoadFailed = true; @@ -232,7 +181,7 @@ void RAContext::LoadGame(const char* hash) } }, - this // Przekazujemy 'this' jako userdata + this ); } @@ -243,10 +192,8 @@ uint32_t RAContext::ReadMemory(uint32_t address, uint32_t size, rc_client_t* client) { - //printf("[RA][ReadMemory] called addr=%08X size=%u\n", address, size); if (address >= 0x04000000 && address < 0x04000400) { - //printf("[RA][IO READ] %08X\n", address); } g_ra_mem_reads++; RAContext* ctx = @@ -259,7 +206,6 @@ uint32_t RAContext::ReadMemory(uint32_t address, return 0; // ===== ARM9 MAIN RAM ===== - // RA: 0x00000000 -> ARM9 0x02000000 (4MB) constexpr uint32_t ARM9_RA_BASE = 0x00000000; constexpr uint32_t ARM9_RA_SIZE = 0x00400000; constexpr uint32_t ARM9_PHYS_BASE = 0x02000000; @@ -269,24 +215,12 @@ uint32_t RAContext::ReadMemory(uint32_t address, memcpy(buffer, ctx->nds->MainRAM + address, size); - /* - if (++g_ra_mem_logged % 10 == 0) - { - printf("[RA][ReadMemory][ARM9] reads=%llu addr=%08X size=%u val=%02X\n", - (unsigned long long)g_ra_mem_reads, - address, - size, - buffer[0]); - } - */ - return size; } // ===== WRAM ===== - // RA: 0x03000000 -> WRAM constexpr uint32_t WRAM_RA_BASE = 0x03000000; - constexpr uint32_t WRAM_SIZE = 0x00008000; // 32KB + constexpr uint32_t WRAM_SIZE = 0x00008000; if (address >= WRAM_RA_BASE && address + size <= WRAM_RA_BASE + WRAM_SIZE) @@ -298,10 +232,9 @@ uint32_t RAContext::ReadMemory(uint32_t address, return size; } - // ===== ARM7 RAM (opcjonalne, ale bardzo zalecane) ===== - // RA: 0x01000000 -> ARM7 0x03800000 + // ===== ARM7 RAM ===== constexpr uint32_t ARM7_RA_BASE = 0x01000000; - constexpr uint32_t ARM7_SIZE = 0x00010000; // 64KB + constexpr uint32_t ARM7_SIZE = 0x00010000; constexpr uint32_t ARM7_PHYS_BASE = 0x03800000; if (address >= ARM7_RA_BASE && @@ -313,34 +246,9 @@ uint32_t RAContext::ReadMemory(uint32_t address, size); return size; } - - /* - // ===== VRAM ===== - constexpr uint32_t VRAM_BASE = 0x06000000; - constexpr uint32_t VRAM_SIZE = 0x00180000; // 1.5 MB - if (address >= VRAM_BASE && address + size <= VRAM_BASE + VRAM_SIZE) - { - // w melonDS VRAM jest podzielone na banki, ale dla RA można narazie: - memset(buffer, 0, size); - return size; - } - - // ===== ARM9 IO ===== - constexpr uint32_t ARM9_IO_BASE = 0x04000000; - constexpr uint32_t ARM9_IO_SIZE = 0x00000400; // 1 KB - if (address >= ARM9_IO_BASE && address + size <= ARM9_IO_BASE + ARM9_IO_SIZE) - { - // tu można ewentualnie zwracać prawdziwe rejestry IO - // na razie tylko zero: - memset(buffer, 0, size); - return size; - } - */ - // ===== NIEOBSŁUGIWANE ===== return 0; } -// funkcja do zapisu cacert do pliku tymczasowego static std::string WriteCACertToTempFile() { #ifdef _WIN32 char tempPath[MAX_PATH]; @@ -373,7 +281,6 @@ static std::string WriteCACertToTempFile() { #endif } -// funkcja do usuwania tymczasowego pliku static void RemoveTempFile(const std::string& path) { if (!path.empty()) { #ifdef _WIN32 @@ -384,7 +291,6 @@ static void RemoveTempFile(const std::string& path) { } } -// główna funkcja void RAContext::ServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, void* userdata, @@ -396,10 +302,8 @@ void RAContext::ServerCall(const rc_api_request_t* request, return; } - // zapisz cacert do pliku tymczasowego std::string cacertPath = WriteCACertToTempFile(); if (cacertPath.empty()) { - printf("[RA] Failed to create temp file for CA cert!\n"); callback(nullptr, userdata); curl_easy_cleanup(curl); return; @@ -430,35 +334,23 @@ void RAContext::ServerCall(const rc_api_request_t* request, api_resp.http_status_code = static_cast(http_code); callback(&api_resp, userdata); } else { - printf("[RA] cURL Error %d: %s\n", res, curl_easy_strerror(res)); callback(nullptr, userdata); } curl_easy_cleanup(curl); RemoveTempFile(cacertPath); } -///////RESET void RAContext::Reset() { if (client) rc_client_reset(client); } -void RAContext::DisableHardcore(const char* reason) -{ - if (client && rc_client_get_hardcore_enabled(client)) - { - rc_client_set_hardcore_enabled(client, 0); - printf("[RA] Hardcore disabled: %s\n", reason); - } -} void RAContext::LoginWithPassword(const char* username, const char* password, bool hardcore) { if (!client) return; - // hardcore ustawiamy wcześniej rc_client_set_hardcore_enabled(client, hardcore ? 1 : 0); - // Wywołujemy logowanie hasłem rc_client_begin_login_with_password( client, username, @@ -471,7 +363,6 @@ void RAContext::SetCredentials(const char* user, const char* password, bool hard m_user = user; m_password = password; m_hardcore = hardcore; - //printf("[RA] Credentials set. user='%s'\n", m_user.c_str()); } void RAContext::SetPendingGameHash(const char* hash) @@ -481,32 +372,13 @@ void RAContext::SetPendingGameHash(const char* hash) pendingGameHash = std::string(hash); gameLoaded = false; - - //printf("[RA] Pending game hash set: %s\n", hash); -} -/* -void RAContext::SetPendingGame(const char* hash) -{ - pendingGameHash = hash; - - if (IsLoggedIn()) - { - LoadGame(pendingGameHash.c_str()); - } } -*/ void RAContext::AttachNDS(melonDS::NDS* nds_) { if (nds == nds_) return; nds = nds_; - //printf("[RA] NDS attached: %p\n", (void*)nds); -} -RAContext& RAContext::Get() -{ - static RAContext instance; - return instance; } void RAContext::Enable() @@ -535,54 +407,25 @@ void RAContext::Disable() m_logged_in = false; gameLoaded = false; pendingGameHash.reset(); - - printf("[RA] Disabled\n"); } void RAContext::LoginNow() { - /* - if (!client) - { - // inicjalizacja klienta, podłączenie readmemory - client = rc_client_create(&RAContext::ReadMemory, &RAContext::ServerCall); - rc_client_set_userdata(client, this); - rc_client_set_allow_background_memory_reads(client, 1); - printf("[RA] Client initialized (rcheevos 12+)\n"); - } - */ if (m_logged_in) return; if (!m_user.empty() && !m_password.empty()) { - //printf("[RA] LoginNow: password\n"); LoginWithPassword(m_user.c_str(), m_password.c_str(), m_hardcore); } - else - { - printf("[RA] LoginNow: no credentials\n"); - } } void RAContext::SetOnAchievementUnlocked(AchievementUnlockedCallback cb) { - g_onAchievementUnlocked = std::move(cb); + RAContext::m_onAchievementUnlocked = std::move(cb); } void RAContext::SetOnGameLoadedCallback(std::function cb) { - //printf("[RA] SetOnGameLoadedCallback called\n"); onGameLoaded = std::move(cb); -} - -/* -void RAContext::SetOnLogin(LoginCallback cb) -{ - m_onLogin = std::move(cb); - - // jeśli już zalogowany, odpal callback od razu - if (m_logged_in && m_onLogin) - m_onLogin(); -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/RetroAchievements/RAClient.h b/src/RetroAchievements/RAClient.h index d04153e508..9244c27104 100644 --- a/src/RetroAchievements/RAClient.h +++ b/src/RetroAchievements/RAClient.h @@ -11,19 +11,19 @@ using AchievementUnlockedCallback = std::function; - -// forward declaration – ZERO include hell namespace melonDS { class NDS; } - class RAContext { public: + RAContext(); + ~RAContext(); + bool m_shuttingDown = false; bool m_IsPaused = false; bool CanPause(uint32_t* frames_remaining); void SetPaused(bool paused); std::function onLoginResponse; - bool pendingLoginResult = false; // Czy mamy nowy wynik logowania do pokazania? - bool loginSuccess = false; // Czy się udało? - std::string loginErrorMessage; // Treść błędu jeśli się nie udało + bool pendingLoginResult = false; + bool loginSuccess = false; + std::string loginErrorMessage; bool isLoading = false; std::string m_lastHash; std::string pendingLoadError; @@ -37,93 +37,49 @@ class RAContext { void Enable(); void Disable(); bool IsEnabled() const { return m_enabled; } + bool IsHardcoreEnabled() const { return m_hardcore; } using LoginCallback = std::function; - void SetOnLogin(LoginCallback cb) { m_onLogin = std::move(cb); } - //bool IsLoggedIn() const; - static RAContext& Get(); - void AttachNDS(melonDS::NDS* nds_); void SetPendingGameHash(const char* hash); - //void SetPendingGame(const char* hash); - void LoginOnce(); void SetToken(const std::string& token) { m_token = token; } void SetLoggedIn(bool v); - // Singleton - /* - static RAContext& Instance() { - return Get(); // Przekierowanie do jedynej słusznej instancji - } - - static RAContext& Instance() { - static RAContext instance; - return instance; - } - */ - - - - // Inicjalizacja i zamknięcie void Init(melonDS::NDS* nds); void Shutdown(); - - // Wywoływane co klatkę void DoFrame(); - - // Logowanie hasłem void LoginWithPassword(const char* username, const char* password, bool hardcore); - - // Ustawienia konta void SetCredentials(const char* user, const char* password, bool hardcore); - - // Ładowanie gry po hash void LoadGame(const char* hash); - - // Hardcore / reset void Reset(); - void DisableHardcore(const char* reason); - - // Gettery bool IsLoggedIn() const { return m_logged_in; } const std::string& GetUser() const { return m_user; } const std::string& GetToken() const { return m_token; } const std::string& GetDisplayName() const; - - // Wskaźniki melonDS::NDS* nds = nullptr; rc_client_t* client = nullptr; - - //new bool m_logged_in = false; private: - // Konstruktor / Destruktor - RAContext(); - ~RAContext(); + void SetDisplayName(const char* name); + AchievementUnlockedCallback m_onAchievementUnlocked; + std::string m_displayName; const rc_client_game_t* currentGameInfo = nullptr; bool m_enabled = false; LoginCallback m_onLogin; std::optional pendingGameHash; - // Dane konta i stan bool gameLoaded = false; - bool Loaowaniegry = false; std::string m_user; std::string m_token; std::string m_password; - bool m_hardcore = false; - - - // Callbacki rcheevos static uint32_t ReadMemory( uint32_t address, uint8_t* buffer, uint32_t size, rc_client_t* client ); - static void ServerCall( const rc_api_request_t* request, rc_client_server_callback_t callback, From 1789c3c55f90ece0fa87383953c39d92b2f8af60 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Mon, 5 Jan 2026 23:45:36 +0100 Subject: [PATCH 34/41] changes to frontend nr.1 --- src/frontend/qt_sdl/CMakeLists.txt | 25 +- src/frontend/qt_sdl/Config.cpp | 126 ++++----- src/frontend/qt_sdl/Config.h | 17 +- src/frontend/qt_sdl/EmuInstance.cpp | 49 +++- src/frontend/qt_sdl/EmuInstance.h | 10 +- src/frontend/qt_sdl/EmuSettingsDialog.cpp | 109 +------- src/frontend/qt_sdl/EmuSettingsDialog.h | 18 +- src/frontend/qt_sdl/EmuThread.cpp | 90 ++++--- src/frontend/qt_sdl/Window.cpp | 304 ++++++++++++++-------- src/frontend/qt_sdl/Window.h | 23 +- src/frontend/qt_sdl/main.cpp | 33 +-- 11 files changed, 417 insertions(+), 387 deletions(-) diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index 087fb041fc..157759a858 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -7,14 +7,6 @@ set(SOURCES_QT_SDL main_shaders.h Screen.cpp Window.cpp - toast/ToastManager.cpp - toast/ToastManager.h - toast/AchievementToast.cpp - toast/AchievementToast.h - toast/ToastOverlay.cpp - toast/ToastOverlay.h - toast/BadgeCache.h - toast/BadgeCache.cpp EmuInstance.cpp EmuInstanceAudio.cpp EmuInstanceInput.cpp @@ -26,7 +18,6 @@ set(SOURCES_QT_SDL EmuSettingsDialog.cpp PowerManagement/PowerManagementDialog.cpp PowerManagement/resources/battery.qrc - retroachievements/resources/ra.qrc InputConfig/InputConfigDialog.cpp InputConfig/MapButton.h InputConfig/resources/ds.qrc @@ -71,6 +62,22 @@ set(SOURCES_QT_SDL NetplayDialog.cpp ) +if (ENABLE_RETROACHIEVEMENTS) + list(APPEND SOURCES_QT_SDL + toast/ToastManager.cpp + toast/ToastManager.h + toast/AchievementToast.cpp + toast/AchievementToast.h + toast/ToastOverlay.cpp + toast/ToastOverlay.h + toast/BadgeCache.h + toast/BadgeCache.cpp + retroachievements/resources/ra.qrc + RASettingsDialog.cpp + ) + add_compile_definitions(RETROACHIEVEMENTS_ENABLED) +endif() + option(USE_QT6 "Use Qt 6 instead of Qt 5" ON) if (USE_QT6) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 0d53e7bb78..06318afe4c 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -33,15 +33,11 @@ using namespace std::string_literals; + namespace Config { using namespace melonDS; -std::string RA_Username = ""; -std::string RA_Token = ""; -std::string RA_Password = ""; -bool RA_Enabled; -bool RA_HardcoreMode; const char* kConfigFile = "melonDS.toml"; @@ -50,7 +46,6 @@ const char* kLegacyUniqueConfigFile = "melonDS.%d.ini"; toml::value RootTable; - DefaultList DefaultInts = { {"Instance*.Keyboard", -1}, @@ -108,6 +103,10 @@ DefaultList DefaultBools = {"Emu.DirectBoot", true}, {"Instance*.DS.Battery.LevelOkay", true}, {"Instance*.DSi.Battery.Charging", true}, +#ifdef RETROACHIEVEMENTS_ENABLED + {"Instance*.RetroAchievements.Enabled", false}, + {"Instance*.RetroAchievements.HardcoreMode", false}, +#endif #ifdef JIT_ENABLED {"JIT.BranchOptimisations", true}, {"JIT.LiteralOptimisations", true}, @@ -122,7 +121,12 @@ DefaultList DefaultStrings = { {"DLDI.ImagePath", "dldi.bin"}, {"DSi.SD.ImagePath", "dsisd.bin"}, - {"Instance*.Firmware.Username", "melonDS"} + {"Instance*.Firmware.Username", "melonDS"}, +#ifdef RETROACHIEVEMENTS_ENABLED + {"Instance*.RetroAchievements.Username", ""}, + {"Instance*.RetroAchievements.Password", ""}, + {"Instance*.RetroAchievements.Token", ""}, +#endif }; DefaultList DefaultDoubles = @@ -160,47 +164,47 @@ LegacyEntry LegacyFile[] = {"Joy_X", 0, "Joystick.X", true}, {"Joy_Y", 0, "Joystick.Y", true}, - {"HKKey_Lid", 0, "Keyboard.HK_Lid", true}, - {"HKKey_Mic", 0, "Keyboard.HK_Mic", true}, - {"HKKey_Pause", 0, "Keyboard.HK_Pause", true}, - {"HKKey_Reset", 0, "Keyboard.HK_Reset", true}, - {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, - {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, - {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, - {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, - {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, - {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, + {"HKKey_Lid", 0, "Keyboard.HK_Lid", true}, + {"HKKey_Mic", 0, "Keyboard.HK_Mic", true}, + {"HKKey_Pause", 0, "Keyboard.HK_Pause", true}, + {"HKKey_Reset", 0, "Keyboard.HK_Reset", true}, + {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, + {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, + {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, + {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, + {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, + {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, {"HKKey_SolarSensorDecrease", 0, "Keyboard.HK_SolarSensorDecrease", true}, {"HKKey_SolarSensorIncrease", 0, "Keyboard.HK_SolarSensorIncrease", true}, - {"HKKey_FrameStep", 0, "Keyboard.HK_FrameStep", true}, - {"HKKey_PowerButton", 0, "Keyboard.HK_PowerButton", true}, - {"HKKey_VolumeUp", 0, "Keyboard.HK_VolumeUp", true}, - {"HKKey_VolumeDown", 0, "Keyboard.HK_VolumeDown", true}, - {"HKKey_GuitarGripGreen", 0, "Keyboard.HK_GuitarGripGreen", true}, - {"HKKey_GuitarGripRed", 0, "Keyboard.HK_GuitarGripRed", true}, - {"HKKey_GuitarGripYellow", 0, "Keyboard.HK_GuitarGripYellow", true}, - {"HKKey_GuitarGripBlue", 0, "Keyboard.HK_GuitarGripBlue", true}, - - {"HKJoy_Lid", 0, "Joystick.HK_Lid", true}, - {"HKJoy_Mic", 0, "Joystick.HK_Mic", true}, - {"HKJoy_Pause", 0, "Joystick.HK_Pause", true}, - {"HKJoy_Reset", 0, "Joystick.HK_Reset", true}, - {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, - {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, - {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, - {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, - {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, - {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, + {"HKKey_FrameStep", 0, "Keyboard.HK_FrameStep", true}, + {"HKKey_PowerButton", 0, "Keyboard.HK_PowerButton", true}, + {"HKKey_VolumeUp", 0, "Keyboard.HK_VolumeUp", true}, + {"HKKey_VolumeDown", 0, "Keyboard.HK_VolumeDown", true}, + {"HKKey_GuitarGripGreen", 0, "Keyboard.HK_GuitarGripGreen", true}, + {"HKKey_GuitarGripRed", 0, "Keyboard.HK_GuitarGripRed", true}, + {"HKKey_GuitarGripYellow", 0, "Keyboard.HK_GuitarGripYellow", true}, + {"HKKey_GuitarGripBlue", 0, "Keyboard.HK_GuitarGripBlue", true}, + + {"HKJoy_Lid", 0, "Joystick.HK_Lid", true}, + {"HKJoy_Mic", 0, "Joystick.HK_Mic", true}, + {"HKJoy_Pause", 0, "Joystick.HK_Pause", true}, + {"HKJoy_Reset", 0, "Joystick.HK_Reset", true}, + {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, + {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, + {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, + {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, + {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, + {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, {"HKJoy_SolarSensorDecrease", 0, "Joystick.HK_SolarSensorDecrease", true}, {"HKJoy_SolarSensorIncrease", 0, "Joystick.HK_SolarSensorIncrease", true}, - {"HKJoy_FrameStep", 0, "Joystick.HK_FrameStep", true}, - {"HKJoy_PowerButton", 0, "Joystick.HK_PowerButton", true}, - {"HKJoy_VolumeUp", 0, "Joystick.HK_VolumeUp", true}, - {"HKJoy_VolumeDown", 0, "Joystick.HK_VolumeDown", true}, - {"HKJoy_GuitarGripGreen", 0, "Joystick.HK_GuitarGripGreen", true}, - {"HKJoy_GuitarGripRed", 0, "Joystick.HK_GuitarGripRed", true}, - {"HKJoy_GuitarGripYellow", 0, "Joystick.HK_GuitarGripYellow", true}, - {"HKJoy_GuitarGripBlue", 0, "Joystick.HK_GuitarGripBlue", true}, + {"HKJoy_FrameStep", 0, "Joystick.HK_FrameStep", true}, + {"HKJoy_PowerButton", 0, "Joystick.HK_PowerButton", true}, + {"HKJoy_VolumeUp", 0, "Joystick.HK_VolumeUp", true}, + {"HKJoy_VolumeDown", 0, "Joystick.HK_VolumeDown", true}, + {"HKJoy_GuitarGripGreen", 0, "Joystick.HK_GuitarGripGreen", true}, + {"HKJoy_GuitarGripRed", 0, "Joystick.HK_GuitarGripRed", true}, + {"HKJoy_GuitarGripYellow", 0, "Joystick.HK_GuitarGripYellow", true}, + {"HKJoy_GuitarGripBlue", 0, "Joystick.HK_GuitarGripBlue", true}, {"JoystickID", 0, "JoystickID", true}, @@ -344,29 +348,6 @@ LegacyEntry LegacyFile[] = {"", -1, "", false} }; - -void SyncRAConfig() -{ - Table tbl = GetGlobalTable(); - RA_Enabled = tbl.GetBool("RetroAchievements.Enabled"); - RA_HardcoreMode = tbl.GetBool("RetroAchievements.HardcoreMode"); - RA_Username = tbl.GetString("RetroAchievements.Username"); - RA_Password = tbl.GetString("RetroAchievements.Password"); - RA_Token = tbl.GetString("RetroAchievements.Token"); -} - -void SaveRAConfig() -{ - Table tbl = GetGlobalTable(); - tbl.SetBool("RetroAchievements.Enabled", RA_Enabled); - tbl.SetBool("RetroAchievements.HardcoreMode", RA_HardcoreMode); - tbl.SetString("RetroAchievements.Username", RA_Username); - tbl.SetString("RetroAchievements.Password", RA_Password); - tbl.SetString("RetroAchievements.Token", RA_Token); - Save(); -} - - static std::string GetDefaultKey(std::string path) { std::string tables[] = {"Instance", "Window", "Camera"}; @@ -822,15 +803,12 @@ bool Load() if (!Platform::FileExists(cfgpath)) { - bool ret = LoadLegacy(); - SyncRAConfig(); - return ret; + return LoadLegacy(); } try { RootTable = toml::parse(std::filesystem::u8path(cfgpath)); - SyncRAConfig(); } catch (toml::syntax_error& err) { @@ -843,10 +821,6 @@ bool Load() void Save() { Table tbl = GetGlobalTable(); - tbl.SetBool("RetroAchievements.Enabled", RA_Enabled); - tbl.SetBool("RetroAchievements.HardcoreMode", RA_HardcoreMode); - tbl.SetString("RetroAchievements.Username", RA_Username); - tbl.SetString("RetroAchievements.Token", RA_Token); auto cfgpath = Platform::GetLocalFilePath(kConfigFile); if (!Platform::CheckFileWritable(cfgpath)) @@ -872,4 +846,4 @@ Table GetLocalTable(int instance) return Table(tbl, key); } -} // namespace Config +} diff --git a/src/frontend/qt_sdl/Config.h b/src/frontend/qt_sdl/Config.h index 451bd69c09..48b0f3d99f 100644 --- a/src/frontend/qt_sdl/Config.h +++ b/src/frontend/qt_sdl/Config.h @@ -29,16 +29,6 @@ namespace Config { - extern bool RA_Enabled; - extern bool RA_HardcoreMode; - extern std::string RA_Username; - extern std::string RA_Token; - extern std::string RA_Password; - - void SyncRAConfig(); - void SaveRAConfig(); - - void Save(); struct LegacyEntry { @@ -102,6 +92,13 @@ class Table Table(toml::value& data, const std::string& path); ~Table() {} + #ifdef RETROACHIEVEMENTS_ENABLED + bool RA_Enabled = false; + bool RA_HardcoreMode = false; + std::string RA_Username; + std::string RA_Password; + #endif + Table& operator=(const Table& b); Array GetArray(const std::string& path); diff --git a/src/frontend/qt_sdl/EmuInstance.cpp b/src/frontend/qt_sdl/EmuInstance.cpp index 30c0f8d2aa..52e99b0734 100755 --- a/src/frontend/qt_sdl/EmuInstance.cpp +++ b/src/frontend/qt_sdl/EmuInstance.cpp @@ -47,7 +47,10 @@ #include "DSi_I2C.h" #include "FreeBIOS.h" #include "main.h" + +#ifdef RETROACHIEVEMENTS_ENABLED #include "../../RetroAchievements/RAClient.h" +#endif using std::make_unique; using std::pair; @@ -137,6 +140,10 @@ EmuInstance::EmuInstance(int inst) : deleting(false), for (int i = 0; i < kMaxWindows; i++) windowList[i] = nullptr; + #ifdef RETROACHIEVEMENTS_ENABLED + ra = std::make_unique(); + #endif + if (inst == 0) topWindow = nullptr; createWindow(); @@ -159,7 +166,10 @@ EmuInstance::~EmuInstance() emuThread->emuExit(); emuThread->wait(); - RAContext::Get().Shutdown(); + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = getRA(); + ra->Shutdown(); + #endif delete emuThread; net.UnregisterInstance(instanceID); @@ -224,6 +234,12 @@ void EmuInstance::createWindow(int id) { win->actNewWindow->setEnabled(enable); }); + if (id == 0) + { + #ifdef RETROACHIEVEMENTS_ENABLED + SyncRetroAchievementsFromConfig(); + #endif + } } void EmuInstance::deleteWindow(int id, bool close) @@ -1371,6 +1387,13 @@ bool EmuInstance::updateConsole() noexcept else nds = new NDS(std::move(ndsargs), this); + #ifdef RETROACHIEVEMENTS_ENABLED + if (ra) + { + nds->SetRAContext(ra.get()); + } + #endif + nds->Reset(); loadRTCData(); //emuThread->updateVideoRenderer(); // not actually needed? @@ -2275,3 +2298,27 @@ void EmuInstance::animatedROMIcon(const u8 (&data)[8][512], const u16 (&palette) animatedSequenceRef.push_back(i); } } +#ifdef RETROACHIEVEMENTS_ENABLED + +void EmuInstance::SyncRetroAchievementsFromConfig() +{ + auto& cfg = getLocalConfig(); + + if (!ra) + return; + + if (!cfg.GetBool("RetroAchievements.Enabled")) + { + ra->Disable(); + return; + } + + ra->SetCredentials( + cfg.GetString("RetroAchievements.Username").c_str(), + cfg.GetString("RetroAchievements.Password").c_str(), + cfg.GetBool("RetroAchievements.HardcoreMode") + ); + + ra->Enable(); +} +#endif \ No newline at end of file diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index dce0d9cdec..fab3bb30cd 100755 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -29,6 +29,10 @@ #include "Config.h" #include "SaveManager.h" +#ifdef RETROACHIEVEMENTS_ENABLED +#include "../../RetroAchievements/RAClient.h" +#endif + const int kMaxWindows = 4; enum @@ -84,9 +88,13 @@ int getEventKeyVal(QKeyEvent* event); class EmuInstance { public: + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* getRA() const { return ra.get(); } + std::unique_ptr ra; + void SyncRetroAchievementsFromConfig(); + #endif EmuInstance(int inst); ~EmuInstance(); - int getInstanceID() { return instanceID; } int getConsoleType() { return consoleType; } EmuThread* getEmuThread() { return emuThread; } diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.cpp b/src/frontend/qt_sdl/EmuSettingsDialog.cpp index d6ed716c89..10f20e4e0d 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.cpp +++ b/src/frontend/qt_sdl/EmuSettingsDialog.cpp @@ -16,12 +16,6 @@ with melonDS. If not, see http://www.gnu.org/licenses/. */ -#include -#include -#include -#include -#include - #include #include @@ -32,8 +26,6 @@ #include "EmuSettingsDialog.h" #include "ui_EmuSettingsDialog.h" #include "main.h" -#include -#include "RetroAchievements/RAClient.h" using namespace melonDS::Platform; using namespace melonDS; @@ -168,99 +160,6 @@ EmuSettingsDialog::EmuSettingsDialog(QWidget* parent) : QDialog(parent), ui(new SET_ORIGVAL(QComboBox, currentIndex); SET_ORIGVAL(QCheckBox, isChecked); - // --- Sekcja RetroAchievements --- - QWidget* raTab = new QWidget(); - QVBoxLayout* tabLayout = new QVBoxLayout(raTab); - - groupRA = new QGroupBox("RetroAchievements Settings", raTab); - QFormLayout* raForm = new QFormLayout(groupRA); - - cbRAEnabled = new QCheckBox("Enable RetroAchievements"); - cbRAHardcore = new QCheckBox("Hardcore Mode (No Savestates)"); - leRAUsername = new QLineEdit(); - leRAPassword = new QLineEdit(); - leRAPassword->setEchoMode(QLineEdit::Password); - leRAPassword->setPlaceholderText("Enter your RA password"); - - btnRALogin = new QPushButton(); - lblRAStatus = new QLabel(); - lblRAStatus->setStyleSheet("font-weight: bold; color: #2ecc71;"); - - raForm->addRow(cbRAEnabled); - raForm->addRow(cbRAHardcore); - raForm->addRow("Username:", leRAUsername); - raForm->addRow("Password:", leRAPassword); - raForm->addRow("", btnRALogin); - raForm->addRow("", lblRAStatus); - - tabLayout->addWidget(groupRA); - tabLayout->addStretch(); - - QLabel* trademarkLabel = new QLabel("Integration added by PanMenel™"); - - trademarkLabel->setStyleSheet( - "QLabel {" - " color: #3498db;" - " font-weight: bold;" - " font-family: 'Segoe UI', sans-serif;" - " font-size: 11px;" - " letter-spacing: 2px;" - " padding: 10px;" - " border-top: 1px solid #333;" - "}" - ); - - trademarkLabel->setAlignment(Qt::AlignRight); - tabLayout->addWidget(trademarkLabel); - ui->tabWidget->addTab(raTab, "RA"); - - auto UpdateRAUI = [this]() { - if (RAContext::Get().IsLoggedIn()) { - btnRALogin->setText("Logout"); - const rc_client_user_t* user = rc_client_get_user_info(RAContext::Get().client); - if (user && user->display_name) - lblRAStatus->setText(QString("Logged in as: %1").arg(user->display_name)); - else - lblRAStatus->setText("Logged in"); - } else { - btnRALogin->setText("Login Now"); - lblRAStatus->setText("Not logged in"); - lblRAStatus->setStyleSheet("color: gray;"); - } - }; - - UpdateRAUI(); - - connect(btnRALogin, &QPushButton::clicked, this, [this, UpdateRAUI]() { - if (RAContext::Get().IsLoggedIn()) { - // LOGOUT - RAContext::Get().SetLoggedIn(false); - RAContext::Get().SetToken(""); - UpdateRAUI(); - } else { - // LOGIN - std::string user = leRAUsername->text().toStdString(); - std::string pass = leRAPassword->text().toStdString(); - if (user.empty() || pass.empty()) return; - RAContext::Get().LoginWithPassword(user.c_str(), pass.c_str(), cbRAHardcore->isChecked()); - } - }); - - MainWindow* mainWin = qobject_cast(this->parent()); - - RAContext::Get().onLoginResponse = [this, UpdateRAUI, mainWin](bool success, const std::string& msg) { - QMetaObject::invokeMethod(this, [this, UpdateRAUI, mainWin, success, msg]() { - UpdateRAUI(); - if (mainWin) { - mainWin->ShowRALoginToast(success, msg); - } - }); - }; - - cbRAEnabled->setChecked(Config::RA_Enabled); - cbRAHardcore->setChecked(Config::RA_HardcoreMode); - leRAUsername->setText(QString::fromStdString(Config::RA_Username)); - leRAPassword->setText(QString::fromStdString(Config::RA_Password)); #undef SET_ORIGVAL } @@ -325,12 +224,6 @@ void EmuSettingsDialog::done(int r) if (r == QDialog::Accepted) { - Config::RA_Enabled = cbRAEnabled->isChecked(); - Config::RA_HardcoreMode = cbRAHardcore->isChecked(); - Config::RA_Username = leRAUsername->text().toStdString(); - Config::RA_Password = leRAPassword->text().toStdString(); - - Config::SaveRAConfig(); bool modified = false; #define CHECK_ORIGVAL(type, val) \ @@ -691,4 +584,4 @@ void EmuSettingsDialog::on_chkExternalBIOS_toggled() ui->btnBIOS7Browse->setDisabled(disabled); ui->txtFirmwarePath->setDisabled(disabled); ui->btnFirmwareBrowse->setDisabled(disabled); -} +} \ No newline at end of file diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.h b/src/frontend/qt_sdl/EmuSettingsDialog.h index a35861ce37..ec01bf167a 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.h +++ b/src/frontend/qt_sdl/EmuSettingsDialog.h @@ -21,11 +21,6 @@ #include -class QPushButton; -class QLabel; -class QCheckBox; -class QLineEdit; -class QGroupBox; namespace Ui { class EmuSettingsDialog; } class EmuSettingsDialog; @@ -87,23 +82,12 @@ private slots: void on_cbGdbEnabled_toggled(); private: - class QPushButton* btnRALogin; - class QLabel* lblRAStatus; - class QLabel* lblStatusText; - void UpdateLoginUI(); void verifyFirmware(); void updateLastBIOSFolder(QString& filename); Ui::EmuSettingsDialog* ui; EmuInstance* emuInstance; QString lastBIOSFolder; - - class QCheckBox* cbRAEnabled; - class QCheckBox* cbRAHardcore; - class QLineEdit* leRAUsername; - class QLineEdit* leRAToken; - class QLineEdit* leRAPassword; - class QGroupBox* groupRA; }; -#endif // EMUSETTINGSDIALOG_H +#endif // EMUSETTINGSDIALOG_H \ No newline at end of file diff --git a/src/frontend/qt_sdl/EmuThread.cpp b/src/frontend/qt_sdl/EmuThread.cpp index fa8caeff38..254c6a376f 100755 --- a/src/frontend/qt_sdl/EmuThread.cpp +++ b/src/frontend/qt_sdl/EmuThread.cpp @@ -51,7 +51,10 @@ #include "GPU3D_Soft.h" #include "GPU3D_OpenGL.h" #include "GPU3D_Compute.h" + +#ifdef RETROACHIEVEMENTS_ENABLED #include "../../RetroAchievements/RAClient.h" +#endif #include "Savestate.h" @@ -345,35 +348,34 @@ void EmuThread::run() winUpdateCount = 0; } - if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) { - if (Config::RA_Enabled && Config::RA_HardcoreMode) { + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { + if (emuInstance->hotkeyPressed(HK_FastForwardToggle) || emuInstance->hotkeyPressed(HK_FastForward)) { emuInstance->osdAddMessage(0xFFA0A0, "HARDCORE: Fast Forward is blocked!"); - } else { - emuInstance->fastForwardToggled = !emuInstance->fastForwardToggled; } - } - - if (emuInstance->hotkeyPressed(HK_SlowMoToggle)) { - if (Config::RA_Enabled && Config::RA_HardcoreMode) { + if (emuInstance->hotkeyPressed(HK_SlowMoToggle) || emuInstance->hotkeyPressed(HK_SlowMo)) { emuInstance->osdAddMessage(0xFFA0A0, "HARDCORE: Slow-mo is blocked!"); - } else { - emuInstance->slowmoToggled = !emuInstance->slowmoToggled; - - } - } - if (Config::RA_Enabled && Config::RA_HardcoreMode) - { - if (emuInstance->hotkeyPressed(HK_FastForward) || emuInstance->hotkeyPressed(HK_SlowMo)) - { - emuInstance->osdAddMessage(0xFFA0A0, "Speed manipulation is disabled in Hardcore Mode"); } } - bool enablefastforward = (emuInstance->hotkeyDown(HK_FastForward) | emuInstance->fastForwardToggled) - && !(Config::RA_Enabled && Config::RA_HardcoreMode); + #endif - bool enableslowmo = (emuInstance->hotkeyDown(HK_SlowMo) | emuInstance->slowmoToggled) - && !(Config::RA_Enabled && Config::RA_HardcoreMode); + if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) emuInstance->fastForwardToggled = !emuInstance->fastForwardToggled; + if (emuInstance->hotkeyPressed(HK_SlowMoToggle)) emuInstance->slowmoToggled = !emuInstance->slowmoToggled; + bool enablefastforward = emuInstance->hotkeyDown(HK_FastForward) | emuInstance->fastForwardToggled; + bool enableslowmo = emuInstance->hotkeyDown(HK_SlowMo) | emuInstance->slowmoToggled; + + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { + enablefastforward = false; + enableslowmo = false; + emuInstance->fastForwardToggled = false; + emuInstance->slowmoToggled = false; + } + #endif + if (useOpenGL) { // when using OpenGL: when toggling fast-forward or slowmo, change the vsync interval @@ -652,39 +654,52 @@ void EmuThread::handleMessages() break; case msg_SaveState: - if (Config::RA_Enabled && Config::RA_HardcoreMode) + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Save states are disabled"); break; } + #endif msgResult = emuInstance->saveState(msg.param.value().toStdString()); break; + case msg_LoadState: - if (Config::RA_Enabled && Config::RA_HardcoreMode) + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Load states are disabled"); break; } + #endif msgResult = emuInstance->loadState(msg.param.value().toStdString()); break; case msg_UndoStateLoad: - if (Config::RA_Enabled && Config::RA_HardcoreMode) + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Undo load is disabled"); break; } + #endif emuInstance->undoStateLoad(); msgResult = 1; break; case msg_ImportSavefile: - if (Config::RA_Enabled && Config::RA_HardcoreMode) + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Importing savefiles is disabled"); break; } + #endif { msgResult = 0; auto f = Platform::OpenFile(msg.param.value().toStdString(), Platform::FileMode::Read); @@ -705,11 +720,14 @@ void EmuThread::handleMessages() break; case msg_EnableCheats: - if (Config::RA_Enabled && Config::RA_HardcoreMode) + #ifdef RETROACHIEVEMENTS_ENABLED + if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && + Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) { emuInstance->osdAddMessage(0xFFA0A0, "HARDCORE: Cheats are forbidden!"); break; } + #endif emuInstance->enableCheats(msg.param.value()); break; } @@ -764,19 +782,26 @@ void EmuThread::emuRun() void EmuThread::emuPause(bool broadcast) { - if (Config::RA_Enabled && Config::RA_HardcoreMode) +#ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + if (ra && ra->IsHardcoreEnabled()) { uint32_t frames_left = 0; - if (!RAContext::Get().CanPause(&frames_left)) + if (!ra->CanPause(&frames_left)) { if (frames_left > 0) { float seconds = frames_left / 60.0f; - emuInstance->osdAddMessage(0xFFA0A0, "Hardcore: Wait %.1f seconds to pause", seconds); + emuInstance->osdAddMessage( + 0xFFA0A0, + "Hardcore: Wait %.1f seconds to pause", + seconds + ); } return; } } +#endif sendMessage(msg_EmuPause); waitMessage(); @@ -815,11 +840,14 @@ void EmuThread::emuExit() void EmuThread::emuFrameStep() { - if (Config::RA_HardcoreMode) + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + if (ra->IsHardcoreEnabled()) { emuInstance->osdAddMessage(0xFFA0A0, "Frame step is disabled in Hardcore Mode"); return; } + #endif if (emuPauseStack < emuPauseStackPauseThreshold) sendMessage(msg_EmuPause); sendMessage(msg_EmuFrameStep); diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 48a40eba7a..c8705880d7 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -27,12 +27,6 @@ #include #include -#include -#include -#include -#include -#include -#include #include #include #include @@ -87,11 +81,12 @@ #include "CameraManager.h" #include "Window.h" #include "AboutDialog.h" + +#ifdef RETROACHIEVEMENTS_ENABLED +#include "RASettingsDialog.h" #include "RetroAchievements/RAClient.h" -#include "toast/ToastManager.h" #include "toast/BadgeCache.h" -//#include "retroachievements/AchievementsDialog.h" -//#include "../../RetroAchievements/RAClient.h" +#endif using namespace melonDS; @@ -245,9 +240,12 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : windowCfg(localCfg.GetTable("Window"+std::to_string(id), "Window0")), emuThread(inst->getEmuThread()), enabledSaved(false), + #ifdef RETROACHIEVEMENTS_ENABLED + m_badgeCache(this), + m_toastManager(this), + #endif focused(true) { - m_oldRAEnabled = Config::RA_Enabled; #ifndef _WIN32 if (!parent) { @@ -652,6 +650,11 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actMPSettings = menu->addAction("Multiplayer settings"); connect(actMPSettings, &QAction::triggered, this, &MainWindow::onOpenMPSettings); + #ifdef RETROACHIEVEMENTS_ENABLED + actRASettings = menu->addAction("RA settings"); + connect(actRASettings, &QAction::triggered, this, &MainWindow::onOpenRASettings); + #endif + actWifiSettings = menu->addAction("Wifi settings"); connect(actWifiSettings, &QAction::triggered, this, &MainWindow::onOpenWifiSettings); @@ -719,8 +722,6 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : panel = nullptr; createScreenPanel(); - ToastManager::Get().Init(this); - if (hasMenu) { actEjectCart->setEnabled(false); @@ -806,50 +807,42 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actPreferences->setEnabled(false); #endif // __APPLE__ } - - RAContext::Get().SetOnGameLoadedCallback([this](){ - ShowGameLoadToast(); - }); - - RAContext::Get().onLoginResponse = [this](bool success, const std::string& message) { - ShowRALoginToast(success, message); - }; - - QTimer::singleShot(1000, this, []() - { - if (!Config::RA_Enabled) - return; - - ToastManager::Get().ShowAchievement( - "RetroAchievements", - "Enabled", - QPixmap(":/ra/icons/ra-icon.png") - ); - }); - - RAContext::Get().SetOnAchievementUnlocked( - [this](const char* title, - const char* desc, - const char* badgeUrl) - { - QMetaObject::invokeMethod( - this, - [this, - t = QString::fromUtf8(title), - d = QString::fromUtf8(desc), - b = QString::fromUtf8(badgeUrl)]() + #ifdef RETROACHIEVEMENTS_ENABLED + m_oldRAEnabled = Config::GetLocalTable(emuInstance->instanceID).GetBool("Instance*.RetroAchievements.Enabled"); + m_toastManager.Init(this); + if (auto* ra = inst->getRA()) + { + ra->SetOnGameLoadedCallback([this]() { + ShowGameLoadToast(); + }); + + ra->onLoginResponse = [this](bool success, const std::string& message) { + ShowRALoginToast(success, message); + }; + + ra->SetOnAchievementUnlocked( + [this](const char* title, + const char* desc, + const char* badgeUrl) { - OnAchievementUnlocked(t, d, b); - }, - Qt::QueuedConnection - ); - } - ); - + QMetaObject::invokeMethod( + this, + [this, + t = QString::fromUtf8(title), + d = QString::fromUtf8(desc), + b = QString::fromUtf8(badgeUrl)]() + { + OnAchievementUnlocked(t, d, b); + }, + Qt::QueuedConnection + ); + }); + } + #endif if (emuThread->emuIsActive()) onEmuStart(); } - + QObject::connect(qApp, &QApplication::applicationStateChanged, this, &MainWindow::onAppStateChanged); onUpdateInterfaceSettings(); @@ -1618,11 +1611,17 @@ void MainWindow::onEjectGBACart() void MainWindow::onSaveState() { - if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + auto cfg = Config::GetLocalTable(emuInstance->instanceID); + if (cfg.GetBool("Instance*.RetroAchievements.Enabled") && + cfg.GetBool("Instance*.RetroAchievements.HardcoreMode") && + ra->IsLoggedIn()) { emuInstance->osdAddMessage(0xFFA0A0, "Savestates states is disabled in Hardcore Mode"); return; } + #endif int slot = ((QAction*)sender())->data().toInt(); QString filename; @@ -1658,11 +1657,17 @@ void MainWindow::onSaveState() void MainWindow::onLoadState() { - if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + auto cfg = Config::GetLocalTable(emuInstance->instanceID); + if (cfg.GetBool("Instance*.RetroAchievements.Enabled") && + cfg.GetBool("Instance*.RetroAchievements.HardcoreMode") && + ra->IsLoggedIn()) { emuInstance->osdAddMessage(0xFFA0A0, "Savestates disabled in Hardcore Mode"); return; } + #endif int slot = ((QAction*)sender())->data().toInt(); QString filename; @@ -1713,11 +1718,17 @@ void MainWindow::onUndoStateLoad() void MainWindow::onImportSavefile() { - if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + auto cfg = Config::GetLocalTable(emuInstance->instanceID); + if (cfg.GetBool("Instance*.RetroAchievements.Enabled") && + cfg.GetBool("Instance*.RetroAchievements.HardcoreMode") && + ra->IsLoggedIn()) { QMessageBox::critical(this, "melonDS", "Importing savefiles is disabled in Hardcore Mode."); return; } + #endif QString path = QFileDialog::getOpenFileName(this, "Select savefile", globalCfg.GetQString("LastROMFolder"), @@ -1809,12 +1820,18 @@ void MainWindow::onOpenPowerManagement() void MainWindow::onEnableCheats(bool checked) { - if (checked && Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + auto cfg = Config::GetLocalTable(emuInstance->instanceID); + if (checked && cfg.GetBool("Instance*.RetroAchievements.Enabled") && + cfg.GetBool("Instance*.RetroAchievements.HardcoreMode") && + ra->IsLoggedIn()) { emuInstance->osdAddMessage(0xFFA0A0, "Cheats are disabled in Hardcore Mode"); actEnableCheats->setChecked(false); return; } + #endif localCfg.SetBool("EnableCheats", checked); emuThread->enableCheats(checked); @@ -1826,11 +1843,17 @@ void MainWindow::onEnableCheats(bool checked) void MainWindow::onSetupCheats() { - if (Config::RA_Enabled && Config::RA_HardcoreMode && RAContext::Get().IsLoggedIn()) + #ifdef RETROACHIEVEMENTS_ENABLED + RAContext* ra = emuInstance->getRA(); + auto cfg = Config::GetLocalTable(emuInstance->instanceID); + if (cfg.GetBool("Instance*.RetroAchievements.Enabled") && + cfg.GetBool("Instance*.RetroAchievements.HardcoreMode") && + ra->IsLoggedIn()) { emuInstance->osdAddMessage(0xFFA0A0, "Cheat menu is disabled in Hardcore Mode"); return; } + #endif emuThread->emuPause(); CheatsDialog* dlg = CheatsDialog::openDlg(this); @@ -1959,26 +1982,33 @@ void MainWindow::onEmuSettingsDialogFinished(int res) if (!emuThread->emuIsActive()) actTitleManager->setEnabled(!globalCfg.GetString("DSi.NANDPath").empty()); - - const bool newRAEnabled = Config::RA_Enabled; + +#ifdef RETROACHIEVEMENTS_ENABLED +if (auto* ra = emuInstance->getRA()) +{ + const bool newRAEnabled = + localCfg.GetBool("Instance*.RetroAchievements.Enabled"); if (m_oldRAEnabled != newRAEnabled) { if (newRAEnabled) { - - std::string user = globalCfg.GetString("RetroAchievements.Username"); - std::string pass = globalCfg.GetString("RetroAchievements.Password"); - bool hardcore = globalCfg.GetBool("RetroAchievements.Hardcore"); - - RAContext::Get().SetCredentials( - user.c_str(), - pass.c_str(), - hardcore + std::string user = + localCfg.GetString("Instance*.RetroAchievements.Username"); + std::string pass = + localCfg.GetString("Instance*.RetroAchievements.Password"); + bool hardcore = + localCfg.GetBool("Instance*.RetroAchievements.Hardcore"); + + ra->SetCredentials( + user.c_str(), + pass.c_str(), + hardcore ); - RAContext::Get().Enable(); - ToastManager::Get().ShowAchievement( + ra->Enable(); + + m_toastManager.ShowAchievement( "RetroAchievements", "Enabled", QPixmap(":/ra/icons/ra-icon.png") @@ -1986,17 +2016,20 @@ void MainWindow::onEmuSettingsDialogFinished(int res) } else { - ToastManager::Get().ShowAchievement( - "RetroAchievements", - "Disabled", - QPixmap(":/ra/icons/ra-icon.png") + ra->Disable(); + + m_toastManager.ShowAchievement( + "RetroAchievements", + "Disabled", + QPixmap(":/ra/icons/ra-icon.png") ); - RAContext::Get().Disable(); } + m_oldRAEnabled = newRAEnabled; } - - emuThread->emuUnpause(); +} +#endif +emuThread->emuUnpause(); } void MainWindow::onOpenInputConfig() @@ -2123,6 +2156,35 @@ void MainWindow::onMPSettingsFinished(int res) emuThread->emuUnpause(); } +#ifdef RETROACHIEVEMENTS_ENABLED +void MainWindow::onOpenRASettings() +{ + emuThread->emuPause(); + if (!emuInstance) + return; + + RASettingsDialog* dlg = new RASettingsDialog(emuInstance, this); + connect(dlg, &QDialog::finished, + this, &MainWindow::onRASettingsFinished); + + dlg->show(); +} + +void MainWindow::onRASettingsFinished(int res) +{ + if (!emuInstance) + return; + + if (res == QDialog::Accepted) + { + emuInstance->SyncRetroAchievementsFromConfig(); + } + + if (RASettingsDialog::needsReset) + onReset(); + emuThread->emuUnpause(); +} +#endif void MainWindow::onOpenWifiSettings() { @@ -2431,15 +2493,17 @@ void MainWindow::onEmuReset() actUndoStateLoad->setEnabled(false); } + +#ifdef RETROACHIEVEMENTS_ENABLED void MainWindow::OnAchievementUnlocked( const QString& title, const QString& desc, const QString& badgeUrl) { - BadgeCache::Get().DownloadBadge(badgeUrl, - [title, desc](const QPixmap& pix) + m_badgeCache.DownloadBadge(badgeUrl, + [this, title, desc](const QPixmap& pix) { - ToastManager::Get().ShowAchievement(title, desc, pix); + m_toastManager.ShowAchievement(title, desc, pix); }); } @@ -2447,34 +2511,60 @@ void MainWindow::ShowRALoginToast(bool success, const std::string& message) { QString title = "RetroAchievements"; QString toastMsg; - - if (success) { - toastMsg = "Logged In!"; - } else { - toastMsg = QString("Login Error: %1").arg(QString::fromStdString(message)); - } - - QMetaObject::invokeMethod(QApplication::instance(), [title, toastMsg]() { - ToastManager::Get().ShowAchievement( - title, - toastMsg, - QPixmap(":/ra/icons/ra-icon.png") - ); + QPixmap pixmap(":/ra/icons/ra-icon.png"); + + if (success && emuInstance) + { + RAContext* ra = emuInstance->getRA(); + const rc_client_user_t* user = + (ra && ra->client) ? rc_client_get_user_info(ra->client) : nullptr; + + if (user && user->display_name) + { + toastMsg = QString("Logged in as %1").arg(user->display_name); + + if (user->avatar_url && !QString(user->avatar_url).isEmpty()) + { + QString avatarUrl = QString::fromUtf8(user->avatar_url); + + m_badgeCache.DownloadBadge(avatarUrl, [this, title, toastMsg](const QPixmap& avatarPix) + { + QMetaObject::invokeMethod(this, [this, title, toastMsg, avatarPix]() { + m_toastManager.ShowAchievement(title, toastMsg, avatarPix); + }); + }); + + return; + } + } + else + { + toastMsg = "Logged in!"; + } + } + else + { + toastMsg = QString("Login Error: %1") + .arg(QString::fromStdString(message)); + } + + QMetaObject::invokeMethod(this, [this, title, toastMsg, pixmap]() { + m_toastManager.ShowAchievement(title, toastMsg, pixmap); }); } void MainWindow::ShowGameLoadToast() { - - if (RAContext::Get().pendingLoadFailed) + RAContext* ra = emuInstance->getRA(); + if (ra->pendingLoadFailed) { - QString errorMsg = QString::fromStdString(RAContext::Get().pendingLoadError); + QString errorMsg = QString::fromStdString(ra->pendingLoadError); - QTimer::singleShot(100, this, [errorMsg]() + QTimer::singleShot(100, this, [this, errorMsg]() { - QMetaObject::invokeMethod(QApplication::instance(), [errorMsg]() + QMetaObject::invokeMethod(QApplication::instance(), [this, errorMsg]() { - ToastManager::Get().ShowAchievement( + m_toastManager.ShowAchievement( "RetroAchievements", QString("ERROR: %1").arg(errorMsg), QPixmap(":/ra/icons/ra-icon.png") @@ -2485,7 +2575,7 @@ void MainWindow::ShowGameLoadToast() return; } - const rc_client_game_t* game = RAContext::Get().GetCurrentGameInfo(); + const rc_client_game_t* game = ra->GetCurrentGameInfo(); if (!game) return; @@ -2497,7 +2587,7 @@ void MainWindow::ShowGameLoadToast() rc_client_achievement_list_t* achList = rc_client_create_achievement_list( - RAContext::Get().client, + ra->client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); @@ -2531,21 +2621,21 @@ void MainWindow::ShowGameLoadToast() .arg(unlockedAchievements) .arg(totalAchievements); - QTimer::singleShot(100, this, [title, desc, badgeUrl]() + QTimer::singleShot(100, this, [this, title, desc, badgeUrl]() { - BadgeCache::Get().DownloadBadge(badgeUrl, - [title, desc](const QPixmap& pix) + m_badgeCache.DownloadBadge(badgeUrl, + [this, title, desc](const QPixmap& pix) { QMetaObject::invokeMethod( QApplication::instance(), - [title, desc, pix]() + [this, title, desc, pix]() { - ToastManager::Get().ShowAchievement(title, desc, pix); + m_toastManager.ShowAchievement(title, desc, pix); }); }); }); } - +#endif void MainWindow::onUpdateVideoSettings(bool glchange) { if (!emuInstance) return; diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index ac8d64a439..5b0e719a33 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -22,7 +22,6 @@ #include "glad/glad.h" #include "ScreenLayout.h" #include "duckstation/gl/context.h" -#include "toast/ToastManager.h" #include #include @@ -38,6 +37,12 @@ #include "Config.h" #include "MPInterface.h" +#ifdef RETROACHIEVEMENTS_ENABLED +#include "../../RetroAchievements/RAClient.h" +#include "toast/ToastManager.h" +#include "toast/BadgeCache.h" +#endif + class EmuInstance; class EmuThread; @@ -106,9 +111,16 @@ class MainWindow : public QMainWindow Q_OBJECT public: + #ifdef RETROACHIEVEMENTS_ENABLED + ToastManager m_toastManager; + BadgeCache m_badgeCache; void ShowRALoginToast(bool success, const std::string& message); bool m_oldRAEnabled; void showRALoginToast(); + void ShowGameLoadToast(); + void OnAchievementUnlocked(const QString& title, const QString& desc, const QString& badgeUrl); + #endif + explicit MainWindow(int id, EmuInstance* inst, QWidget* parent = nullptr); ~MainWindow(); @@ -209,6 +221,10 @@ private slots: void onAudioSettingsFinished(int res); void onOpenMPSettings(); void onMPSettingsFinished(int res); + #ifdef RETROACHIEVEMENTS_ENABLED + void onOpenRASettings(); + void onRASettingsFinished(int res); + #endif void onOpenWifiSettings(); void onWifiSettingsFinished(int res); void onOpenFirmwareSettings(); @@ -247,8 +263,6 @@ private slots: void onScreenEmphasisToggled(); private: - void ShowGameLoadToast(); - void OnAchievementUnlocked(const QString& title, const QString& desc, const QString& badgeUrl); virtual void closeEvent(QCloseEvent* event) override; QStringList currentROM; @@ -333,6 +347,9 @@ private slots: QAction* actCameraSettings; QAction* actAudioSettings; QAction* actMPSettings; + #ifdef RETROACHIEVEMENTS_ENABLED + QAction* actRASettings; + #endif QAction* actWifiSettings; QAction* actFirmwareSettings; QAction* actPathSettings; diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index 4e4bfa7dd2..8b124c930d 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -64,7 +64,10 @@ #include "Net_PCap.h" #include "Net_Slirp.h" + +#ifdef RETROACHIEVEMENTS_ENABLED #include "RetroAchievements/RAClient.h" +#endif using namespace melonDS; @@ -266,24 +269,6 @@ bool MelonApplication::event(QEvent *event) return QApplication::event(event); } -void MyFrontendLoginHandler() -{ - if (!Config::RA_Enabled) - return; - - EmuInstance* inst = emuInstances[0]; - if (!inst) return; - - NDS* nds = inst->getNDS(); - RAContext::Get().Init(nds); - RAContext::Get().SetCredentials( - Config::RA_Username.c_str(), - Config::RA_Password.c_str(), - Config::RA_HardcoreMode - ); - RAContext::Get().LoginNow(); -} - int main(int argc, char** argv) { sysTimer.start(); @@ -383,7 +368,12 @@ int main(int argc, char** argv) NetInit(); createEmuInstance(); - MyFrontendLoginHandler(); + #ifdef RETROACHIEVEMENTS_ENABLED + if (emuInstances[0]) + { + emuInstances[0]->SyncRetroAchievementsFromConfig(); + } + #endif { MainWindow* win = emuInstances[0]->getMainWindow(); @@ -429,8 +419,3 @@ int main(int argc, char** argv) SDL_Quit(); return ret; } - -extern "C" { - void SSL_set_quic_tls_transport_params(void* a, void* b, size_t c) {} - void SSL_set_quic_tls_cbs(void* a, void* b) {} -} \ No newline at end of file From 8c7c3c91455fe0e384680431f5a1a844686f1c7f Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:06:13 +0100 Subject: [PATCH 35/41] Some cleanup --- src/CMakeLists.txt | 1 - src/RetroAchievements/RAFunctions.h | 11 - src/RetroAchievements/RAMemory.cpp | 29 --- src/frontend/qt_sdl/Config.cpp | 9 +- src/frontend/qt_sdl/Config.h | 2 + src/frontend/qt_sdl/EmuInstance.h | 1 + src/frontend/qt_sdl/EmuThread.cpp | 12 +- src/frontend/qt_sdl/RASettingsDialog.cpp | 197 ++++++++++++++++++ src/frontend/qt_sdl/RASettingsDialog.h | 43 ++++ src/frontend/qt_sdl/main.cpp | 2 + .../resources/icons/ra-generic-user.png | Bin 7916 -> 0 bytes .../resources/icons/ra-icon.webp | Bin 12338 -> 0 bytes .../resources/icons/trophy-icon-gray.svg | 10 - .../resources/icons/trophy-icon-star.svg | 13 -- .../resources/icons/trophy-icon.svg | 10 - .../qt_sdl/retroachievements/resources/ra.qrc | 6 - .../resources/sounds/lbsubmit.wav | Bin 131284 -> 0 bytes .../resources/sounds/message.wav | Bin 195772 -> 0 bytes src/frontend/qt_sdl/toast/BadgeCache.cpp | 3 +- src/frontend/qt_sdl/toast/BadgeCache.h | 2 +- src/frontend/qt_sdl/toast/ToastManager.cpp | 5 +- src/frontend/qt_sdl/toast/ToastOverlay.cpp | 15 +- src/frontend/qt_sdl/toast/ToastOverlay.h | 8 +- 23 files changed, 265 insertions(+), 114 deletions(-) delete mode 100644 src/RetroAchievements/RAFunctions.h delete mode 100644 src/RetroAchievements/RAMemory.cpp create mode 100644 src/frontend/qt_sdl/RASettingsDialog.cpp create mode 100644 src/frontend/qt_sdl/RASettingsDialog.h delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/ra-generic-user.png delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.webp delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-gray.svg delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/sounds/lbsubmit.wav delete mode 100644 src/frontend/qt_sdl/retroachievements/resources/sounds/message.wav diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 60d3b2cef6..656dd554b7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -286,7 +286,6 @@ if (ENABLE_RETROACHIEVEMENTS) target_sources(core PRIVATE RetroAchievements/RAClient.cpp RetroAchievements/RAClient.h - RetroAchievements/RAFunctions.h ) file(GLOB_RECURSE RCH_ALL "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/src/*.c") diff --git a/src/RetroAchievements/RAFunctions.h b/src/RetroAchievements/RAFunctions.h deleted file mode 100644 index 2318ce9b73..0000000000 --- a/src/RetroAchievements/RAFunctions.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include -#include - -struct RAContext; - -extern "C" uint32_t ra_read_memory(uint32_t addr, uint8_t* buf, uint32_t size, rc_client_t* client); -extern "C" void ra_server_call(const rc_api_request_t* request, - rc_client_server_callback_t callback, - void* userdata, - rc_client_t* client); diff --git a/src/RetroAchievements/RAMemory.cpp b/src/RetroAchievements/RAMemory.cpp deleted file mode 100644 index da60370dea..0000000000 --- a/src/RetroAchievements/RAMemory.cpp +++ /dev/null @@ -1,29 +0,0 @@ -/* -#include "RAClient.h" -#include "../NDS.h" -#include - -uint32_t ra_read_memory( - uint32_t address, - uint8_t* buffer, - uint32_t size, - rc_client_t* client) -{ - RAContext* ctx = - static_cast(rc_client_get_userdata(client)); - - if (!ctx) - return 0; - - melonDS::NDS* nds = ctx->nds; // ← BEZ GetNDS() - - if (!nds) { - memset(buffer, 0, size); - return size; - } - - // TODO: real mapping - memset(buffer, 0, size); - return size; -} - */ \ No newline at end of file diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 06318afe4c..8ca32a8dd9 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -121,12 +121,12 @@ DefaultList DefaultStrings = { {"DLDI.ImagePath", "dldi.bin"}, {"DSi.SD.ImagePath", "dsisd.bin"}, - {"Instance*.Firmware.Username", "melonDS"}, -#ifdef RETROACHIEVEMENTS_ENABLED + #ifdef RETROACHIEVEMENTS_ENABLED {"Instance*.RetroAchievements.Username", ""}, {"Instance*.RetroAchievements.Password", ""}, {"Instance*.RetroAchievements.Token", ""}, -#endif + #endif + {"Instance*.Firmware.Username", "melonDS"} }; DefaultList DefaultDoubles = @@ -348,6 +348,7 @@ LegacyEntry LegacyFile[] = {"", -1, "", false} }; + static std::string GetDefaultKey(std::string path) { std::string tables[] = {"Instance", "Window", "Camera"}; @@ -824,7 +825,7 @@ void Save() auto cfgpath = Platform::GetLocalFilePath(kConfigFile); if (!Platform::CheckFileWritable(cfgpath)) - return; + return; std::ofstream file; file.open(std::filesystem::u8path(cfgpath), std::ofstream::out | std::ofstream::trunc); diff --git a/src/frontend/qt_sdl/Config.h b/src/frontend/qt_sdl/Config.h index 48b0f3d99f..61a4e03996 100644 --- a/src/frontend/qt_sdl/Config.h +++ b/src/frontend/qt_sdl/Config.h @@ -142,5 +142,7 @@ void Save(); Table GetLocalTable(int instance); inline Table GetGlobalTable() { return GetLocalTable(-1); } + } + #endif // CONFIG_H diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index fab3bb30cd..9bf5162ce2 100755 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -95,6 +95,7 @@ class EmuInstance #endif EmuInstance(int inst); ~EmuInstance(); + int getInstanceID() { return instanceID; } int getConsoleType() { return consoleType; } EmuThread* getEmuThread() { return emuThread; } diff --git a/src/frontend/qt_sdl/EmuThread.cpp b/src/frontend/qt_sdl/EmuThread.cpp index 254c6a376f..a90547fa3c 100755 --- a/src/frontend/qt_sdl/EmuThread.cpp +++ b/src/frontend/qt_sdl/EmuThread.cpp @@ -375,7 +375,7 @@ void EmuThread::run() emuInstance->slowmoToggled = false; } #endif - + if (useOpenGL) { // when using OpenGL: when toggling fast-forward or slowmo, change the vsync interval @@ -653,7 +653,7 @@ void EmuThread::handleMessages() emuInstance->ejectGBACart(); break; - case msg_SaveState: + case msg_SaveState: #ifdef RETROACHIEVEMENTS_ENABLED if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) @@ -666,7 +666,7 @@ void EmuThread::handleMessages() break; - case msg_LoadState: + case msg_LoadState: #ifdef RETROACHIEVEMENTS_ENABLED if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) @@ -678,7 +678,7 @@ void EmuThread::handleMessages() msgResult = emuInstance->loadState(msg.param.value().toStdString()); break; - case msg_UndoStateLoad: + case msg_UndoStateLoad: #ifdef RETROACHIEVEMENTS_ENABLED if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) @@ -691,7 +691,7 @@ void EmuThread::handleMessages() msgResult = 1; break; - case msg_ImportSavefile: + case msg_ImportSavefile: #ifdef RETROACHIEVEMENTS_ENABLED if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) @@ -719,7 +719,7 @@ void EmuThread::handleMessages() } break; - case msg_EnableCheats: + case msg_EnableCheats: #ifdef RETROACHIEVEMENTS_ENABLED if (Config::GetGlobalTable().GetBool("RetroAchievements.Enabled") && Config::GetGlobalTable().GetBool("RetroAchievements.HardcoreMode")) diff --git a/src/frontend/qt_sdl/RASettingsDialog.cpp b/src/frontend/qt_sdl/RASettingsDialog.cpp new file mode 100644 index 0000000000..43903b604a --- /dev/null +++ b/src/frontend/qt_sdl/RASettingsDialog.cpp @@ -0,0 +1,197 @@ +#include "RASettingsDialog.h" +#include "ui_RASettingsDialog.h" + +#include "Config.h" +#include "main.h" +#include "RetroAchievements/RAClient.h" + +#include +#include +#include +#include +#include + +bool RASettingsDialog::needsReset = false; +RASettingsDialog::RASettingsDialog(EmuInstance* inst, QWidget* parent) + : QDialog(parent) + , ui(new Ui::RASettingsDialog) + , emuInstance(inst) +{ + ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &RASettingsDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &RASettingsDialog::reject); + + setAttribute(Qt::WA_DeleteOnClose); + + auto& instcfg = emuInstance->getLocalConfig(); + ui->cbRAEnabled->setChecked(instcfg.GetBool("RetroAchievements.Enabled")); + ui->cbRAHardcore->setChecked(instcfg.GetBool("RetroAchievements.HardcoreMode")); + ui->leRAUsername->setText(QString::fromStdString(instcfg.GetString("RetroAchievements.Username"))); + ui->leRAPassword->setText(QString::fromStdString(instcfg.GetString("RetroAchievements.Password"))); + + auto UpdateRAUI = [this]() { + RAContext* ra = emuInstance->getRA(); + if (!ra) return; + + bool loggedIn = ra->IsLoggedIn(); + bool emuRunning = emuInstance->emuIsActive(); + + ui->cbRAEnabled->setEnabled(!loggedIn); + ui->cbRAHardcore->setEnabled(!loggedIn); + ui->leRAUsername->setEnabled(!loggedIn); + ui->leRAPassword->setEnabled(!loggedIn); + ui->btnRALogin->setEnabled(!emuRunning); + + if (emuRunning) { + ui->btnRALogin->setToolTip( + "Stop the emulation before logging in or out of RetroAchievements." + ); + } else { + ui->btnRALogin->setToolTip(QString()); + } + + if (loggedIn) { + ui->btnRALogin->setText("Logout"); + + const rc_client_user_t* user = rc_client_get_user_info(ra->client); + if (user && user->display_name) + ui->lblRAStatus->setText(QString("Logged in as: %1").arg(user->display_name)); + else + ui->lblRAStatus->setText("Logged in"); + + ui->lblRAStatus->setStyleSheet("font-weight: bold; color: #2ecc71;"); + } else { + ui->btnRALogin->setText("Login Now"); + ui->lblRAStatus->setText("Not logged in"); + ui->lblRAStatus->setStyleSheet("color: gray;"); + } + }; + + UpdateRAUI(); + + connect(ui->btnRALogin, &QPushButton::clicked, this, [this, UpdateRAUI]() { + RAContext* ra = emuInstance->getRA(); + if (!ra) return; + + if (ra->IsLoggedIn()) { + ra->SetLoggedIn(false); + ra->SetToken(""); + UpdateRAUI(); + + ui->cbRAEnabled->setProperty("user_originalValue", !ui->cbRAEnabled->isChecked()); + ui->cbRAHardcore->setProperty("user_originalValue", !ui->cbRAHardcore->isChecked()); + ui->leRAUsername->setProperty("user_originalValue", ""); + ui->leRAPassword->setProperty("user_originalValue", ""); + + if (emuInstance && emuInstance->emuIsActive()) { + done(QDialog::Accepted); + } + + } else { + std::string user = ui->leRAUsername->text().toStdString(); + std::string pass = ui->leRAPassword->text().toStdString(); + if (user.empty() || pass.empty()) return; + + ra->LoginWithPassword(user.c_str(), pass.c_str(), ui->cbRAHardcore->isChecked()); + + ui->cbRAEnabled->setProperty("user_originalValue", !ui->cbRAEnabled->isChecked()); + ui->cbRAHardcore->setProperty("user_originalValue", !ui->cbRAHardcore->isChecked()); + ui->leRAUsername->setProperty("user_originalValue", ""); + ui->leRAPassword->setProperty("user_originalValue", ""); + + if (emuInstance && emuInstance->emuIsActive()) { + done(QDialog::Accepted); + } + } + }); + + RAContext* raTmp = emuInstance->getRA(); + if (raTmp) { + auto mainWin = emuInstance->getMainWindow(); + raTmp->onLoginResponse = [this, UpdateRAUI, mainWin](bool success, const std::string& msg) { + QMetaObject::invokeMethod(this, [this, UpdateRAUI, mainWin, success, msg]() { + UpdateRAUI(); + if (mainWin) { + mainWin->ShowRALoginToast(success, msg); + } + }); + }; + } + +#define SET_ORIGVAL(type, val) \ + for (type* w : findChildren(nullptr)) \ + w->setProperty("user_originalValue", w->val()); + + SET_ORIGVAL(QLineEdit, text); + SET_ORIGVAL(QCheckBox, isChecked); + +#undef SET_ORIGVAL +} + +RASettingsDialog::~RASettingsDialog() +{ + delete ui; +} + +void RASettingsDialog::done(int r) +{ + if (!emuInstance) + { + QDialog::done(r); + return; + } + + needsReset = false; + +#ifdef RETROACHIEVEMENTS_ENABLED + if (r == QDialog::Accepted) + { + bool modified = false; + +#define CHECK_ORIGVAL(type, val) \ + if (!modified) for (type* w : findChildren(nullptr)) \ + { \ + QVariant v = w->val(); \ + if (v != w->property("user_originalValue")) \ + { \ + modified = true; \ + break; \ + } \ + } + + CHECK_ORIGVAL(QLineEdit, text); + CHECK_ORIGVAL(QCheckBox, isChecked); + +#undef CHECK_ORIGVAL + + if (modified) + { + if (emuInstance->emuIsActive() + && QMessageBox::warning( + this, + "Reset necessary to apply changes", + "The emulation will be reset for the changes to take place.", + QMessageBox::Ok, + QMessageBox::Cancel) != QMessageBox::Ok) + { + return; + } + + auto& instcfg = emuInstance->getLocalConfig(); + + instcfg.SetBool("RetroAchievements.Enabled", ui->cbRAEnabled->isChecked()); + instcfg.SetBool("RetroAchievements.HardcoreMode", ui->cbRAHardcore->isChecked()); + instcfg.SetString("RetroAchievements.Username", ui->leRAUsername->text().toStdString()); + instcfg.SetString("RetroAchievements.Password", ui->leRAPassword->text().toStdString()); + + emuInstance->SyncRetroAchievementsFromConfig(); + Config::Save(); + + needsReset = true; + } + } +#endif + + QDialog::done(r); +} \ No newline at end of file diff --git a/src/frontend/qt_sdl/RASettingsDialog.h b/src/frontend/qt_sdl/RASettingsDialog.h new file mode 100644 index 0000000000..b99f14f9d8 --- /dev/null +++ b/src/frontend/qt_sdl/RASettingsDialog.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class EmuInstance; + +namespace Ui { +class RASettingsDialog; +} + +class RASettingsDialog : public QDialog +{ + Q_OBJECT + +public: + static bool needsReset; + explicit RASettingsDialog(EmuInstance* inst, QWidget* parent = nullptr); + ~RASettingsDialog() override; + + void done(int r) override; + +private: + Ui::RASettingsDialog* ui; + + QGroupBox* groupRA; + QCheckBox* cbRAEnabled; + QCheckBox* cbRAHardcore; + QLineEdit* leRAUsername; + QLineEdit* leRAPassword; + QPushButton* btnRALogin; + QLabel* lblRAStatus; + + EmuInstance* emuInstance; +}; \ No newline at end of file diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index 8b124c930d..75cbdb755c 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -269,6 +269,7 @@ bool MelonApplication::event(QEvent *event) return QApplication::event(event); } + int main(int argc, char** argv) { sysTimer.start(); @@ -368,6 +369,7 @@ int main(int argc, char** argv) NetInit(); createEmuInstance(); + #ifdef RETROACHIEVEMENTS_ENABLED if (emuInstances[0]) { diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/ra-generic-user.png b/src/frontend/qt_sdl/retroachievements/resources/icons/ra-generic-user.png deleted file mode 100644 index 3d4a9608be89b25fc98e1c708e6c25b03d3349eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7916 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 UuIvH#zu17Pl9pnbJSyb>0BYL4F#rGn diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.webp b/src/frontend/qt_sdl/retroachievements/resources/icons/ra-icon.webp deleted file mode 100644 index 1566039267739c977969490b07e98f2501e36a80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12338 zcmV-2FwM_WNk&F0FaQ8oMM6+kP&il$0000G0002<0RR#K06|PpNOBwi00A5YZQDo= zf7shS5F%m%pp~j&K-Hh~s2f91*CSoXYyPlpTjn&|z9Y%Rl>osdxVuAe_ZIiy^588_ zy-15Ylu~L)ad#{3?g&|IgXWwj4a~wpGl1`#>S+oXwcz4>+(f)wSm|D60rAO$hw`V zH}GmyZ0e=Yv5pU?(i`yW?#sJQ4D0$qKE;9Sby>GaVVy^o)g0KFopQHK*8Shcs^j}G z!mVs4qU6iwsk5S;>RMo!!ep`@po*HfvI%|&Sxi@{T>_Ubsy#Nu?f#DY@jo> z2WGMyz=m31eH=_hF+9mw!jh?vhj|$8WP@F%KfYAv_Zb^*1^wY@&(7z8@!a+R;Ob3o zC>wBV4*-}*E%c2M`6c!QFqPRTHspRD0mktPj5ekaACCZiY4v1-Zs!@GB`LREMs?Fe za8=?|BGaf`l|2J|E<|Y}8}=#>0sg5NT`;b9i9H0IBUHg=T-@P*1(?HU9vk@P{~4e) z8_OLddmrX8{LV=xf(?C?d#1Z$xZ}%Zfw3KO&sV7_Ci^kd2rz?+|5Koh*u#q!Em(Q{ z4N_uOx2#aLvbk2O(75N&Z-3u+FJ52K<(V{N*r6;Q2V9`B2V)&Mxd+A*orprFJ=6hOlgO6{5gXzxnho*4yi!Ej zzDvLhjn82SLyjY-XG=oB619j*?0~~`4*z15bRKPi2#BK>QLndX-;*;aW`j8DsX=E5 zRCmAH_3N2yaBbjmQF?ve${brb1ha^ZCu@(*$x&RP6Hs0@LQ>M-xFphguF zfiOV$YK+f>v01q%xc>`rRw4fMt-BIc{{ZCYBRVJ+A|_Bcl@)I8h{i%VGAYD-vb+*? z20~rYpEKlIa$NRfmevR*5)m*~C4SReiGBjwFZyqc_QF)oa+IGVItp=DC*~1ZGegcL$K~0HYlUDd0;XuiYgMQp&TSvL zyD@4D)12`x*b)5+@g$a3yi$dZ!Jv@XJf9$L`UuyY!M%hw5Gbh@f2~q39cG-^F%0=V zIj)oGlzl?bQ;0=+aSH{S5A#s$8Hn_1!UA^chy0FcF+@a6y0$eV&<%PX{8D#Uqm z?0v^MJW*&2frfhVP6e8ag5g3ZFyvUFE<5?K;C>5Ymtx#bp>P7qKA9d@A?(G4ADoq0 z$`O49@h+ue(H;`5LDL81 z1aVNarxuC!q70Yl`3T}eeqoLCY*mC&5PwoB#v>HEqwJN~j3M`tWB(ZE@nRvr6#>08 zyQq-kIY@YHflD#^3awegyIziHAH)^SY}F-N2ja>vFZ2b(n?PZ;7TiGS3-LFnVmw2k zF9_PpYtN7y$#Gb`CX0oDcL*4)*#nD|GZAEwyuUFz2%od2&jKCMN{HLOig7iGj)EK# zmpu{UX@Ia%8*V8ygFsQurYTGUdEg~2k|CFqevvXt0|UNQ}R4v4eARU%8+x( zaS^L#y9Bz3fYpj|FNs!I)J>M#5sW%QU)DUOurLiGJdt8wD@?O!tgJG2h^y|xPVIQ8 zP$B~XwH5nKp{qqpWld$siR3t63cx9W4nV9`Y`>cnVYgHcxuhlM1n%1AX1A6;}|lts_;3)w@hOCkWq&IjFMbNqHESfq!HAJ zA&-%G6obDL{9YiS4_%^JAY4!3rZu6d1TDtsD^yMgiS~0ub0IGJN{KWOpQj+T6|S)f zdJB=5SD4PI^@Iix$jz62h1^oRX+ z!VBP5kjWPh@iIjC5r;bo71I$=maf|kkU9zn$wV^b26F6k3+OL_&O^+m+lk?Bk=cdO zR%pb)?{W)cAl}3!Tk<(bT3Ll_T#9ExJnKc} zO>4qZOBu~lU$Pj1CTJYIE~RnsLjwB1%>Vq9}@|?Y!as| zQtsy%r@c+0Mkd5fUttv%j~2q-AfTbJb0)EYN4a-5|^X#1i>pB0i6~4R8wId z$Wk)LnHb}R&^Wlu7DqG_VxN~jkNp&OgG@IE-x!RtLUYEiF4TswXVGUD$vIzu3^Rw; z4Ee8{uuccF1&?b8=&jEea zR8FAStC_)MhFnYHA>Hf~Xf?zJMNX1pc}9&7HiOd`ZG<9;x=AW2G=&IFs7Gy;KVXiK z30Ad3Jn;}Fv3^GgbPoZ|)L2D|g_2-qn!$92oKKG3j_c@%Gy`I*7O~k?<}z$26Y#i( z5h0YZ>xjz*#92Yc?-K=)SwpB{)0MrxQ1~N)Evo`=3X|#~#_etb?HTg4 zyD*paJx`zm2pGlR){Im;GOjg`2`tB$OpYyAbavlI=nHX~e#Ymdn&Sn~K{Ec(_Yn5t zLM@xlxS5di4T8Ne`>v$?Hv=khn^_gL?qBNmC%!5RRXgi?TYLvCoqUp2KJ(gv4q$ ztQJ5@xKJt`AtD*us}u52mPaypFB7Q1kdFg|_N=+B487Nm#|U&30nMqtSk{@GID)|+ z`J2F8j8!DA*6updUjS#M;ShURrpFzh-Y7pga|WYNBjb0wfzevfxc2ryQO>wdL-DZc?#++BG=CIvh$S*p?x$lGuy2Z}FcTi2y$fv;+ZzNbxsXZ3_tHdIjgnt*$y6i|M=>Hm{nvnN{cI(Jte*kVQct;_) z3X$S<8dRD8iuJjj%y2M6p7s<*vG&~^6;A^AQ=mBrn9hppIw`!{;g76STX9p^i7}DH zquRX>b42?9o(Bt+AfD1X>4e-(CTzB{e!n3zESvz5R!k_8q}>4$#{+OjfvzDqs<7hD zO4-N#{Lk4K8|(R+hbata$ipO#X6*+`g^~gOltv?9F|8F9A3MJ~`ZWx(nSBAXqEmURgqx`e< z1;RpsenP+~Vi^@wXe!>ZoMfIGF&22Vef!WuTrWk8s(i@Puj$_Ef?m`pRJ~@w5CL`>S`a!H@hULa|@hG;=CUXX3 zD2ZzUHjqj1Q9xTvC1TJi95wpfMB!295EP5iLD~s$)}ldegv|N^9Ypw@of~SHs0qxg?55C`A)mMjMH2yL zlZIZm0eGe~5CP-KJ@hisZWR966#m3mOyW4!KC^)qG#??_U&wDoxb4dgU5#Z@QPN77 zK(-GU5z-NWdlpUVB5)Id)+4wIkqb&>qQxk@!2|{{STewJ()K>WDyh320Mp4~ zt})~}3P%{ zvFJyX54j9&D#jraE2ROfC(V7M5%x&cG7+{3om3zlCM1Ch%|_uRhIR*|zchsb2Wv-b z5i-IB_cI7z6U&@Ub6_w+Dfk&mT4@8TZG-q&RPeq7kX!(u1p<#3Xfgs?%bZUEZ(#lv zQlWk*yv@MoG30I%YcSw`0`!YTI3wjxMK~jK9?>Wm{Gmi=QMiF&`9)#$m*yh$N1%lW z8Px^aiSQ${RLgak%<4)JHk5OQ)rcXjp@Q2TfKTBNEfILC)Efc)rBXu}KB+|OQMj8? zt-#nqVuT&wga<_MV}#2VA?z)}MX|IUhC3-0`GE4k$C!fNVYHSOG2kS^{1t&~3-kwq zEtl9*X)sUxRcIOtPcfzr4EZ8Z2z(0gu?$E%hy_w>1dJ9-b76S8QqV(`gaU@N4PzyV zO&Rb8fqKUxM0p85&k&x;^^b$G=2D?vD7@5&a=piBAuU6gg+jj|a0h`FA)ts@>J7tx zDA6&L%u)t4kRk7S2$mRx$}r6#)=1SL7RjZHFkDHgx*deK8ISuej1?rdW5C-Mn4qT! zaXAFbC4|?pMr(@KVHX+O;+!YAiO`wSgs3w;-z;Jt|f(a;B+>B-v zLtZ5@`zwT|K)oRTl**6!qbiA2ZlUJV$PQU znGK;D+Yt6kIjjf=1yfHL{#D8C0Lq7OBk{hCF+qxCz|QwXLL zFkDH6D%e2y7bB_5ke`Z6`w{GHyc4C42^aTu$Qu4Zsa@E5qe#6*EVzE@Sx-EI%L%1Q@KN{vumoVg`acNm_hMEg*ldmBS%hI|<+oiMMJQWZM@CWxkkFrUgRm9~TMx5lsu z;~x^k)6DC>uRx~|9>o-0&5pu*lxQ)^hkQnm{{u!x=}R{8X3{u>=;ESj5sbZsQs4^^ z{?iB|88X&e@Hl5?YouZy05DT_ek%%Jx2VuC6s~LlhcT9rSlMP~Z-NBcj}Q=7G>wPh zkxHIdQ4Z*@XgWqiX)>F7FKG}0D$91i1M?z>3N=RI4!WDfkheVr_lxGXLkdkoSSOoW z!|*I6+KF<*Q)iYl81qOh%jW(*w?G>ZBIC)Xy)Y>yl!_#S@Ccn%vqRV-q**4%!=)Ao zXeGR|9fdb5(QK5sAbm|?$jc7vojdKNVdJA+E;hLB95=kh$PKmxlx#^*k zu(uckq&Dml)|G}MI15RqpJBL;l7AEk57tR%hD-^Q)@qmj-VA<{3MK+zg!te%n6paM z3FU2wJ~m@)ATi{bc1JInL5!zB+YlbbmQI6U(8);0KzNQW@+V@nm%6g{gHDFa_=|HAO%}`QgbZ*kz<~ zC@1m8Fyv!jX{&Z`0xb8-z)Zlr@CJQnOnjGLa) zUhNV?Dfod&oRsJS!fpa|8YUpNv9b;1eQqd^+Zf-FSTIq$jYO8}4eOg>qX>oR2!GQO zs0D+`Mp_8M^P!sCA<}C|k*s}JGUFl{0O&=4euMd%)|lfpNNO3R1q^wC=@Ft+(7>sYE;&$z}kU953_??FUeF=l&M$2swu7lHtA#J6kNv!=q zlgjrV0D~ELN5fn)QY(~`IBPMElYC0McR5UoHX&U1CO{ow&`OC8fN&3#oNqCPNF`IX z+d*dC2>}He&`FqAK1!w2K^}U63}VQXBGOpazME+U#2`$jP%Q%mqm*bN2#*EXg0Y)K z%O&lS^OzR>gzzjD16mC8F1J!h97ud#h(d`N{iG_a{W|)uN6AVROgDT80%}mm^B#ql zD$xiKUI;OQA>Zefz6R*$An01kN7J=hAr?}gQ7~y0mAtNkq?JS1i?N;Lo7%k%a}W~4 zE^0sm3hob4c()QY2I0K`B{MMENj1{7+d&S4@4G6fPl4LOa9btX3c`)Fo5ql@gQNwl zeWc5?f&=Y>x#Ou+JPG8oc2_W#lNfkUyQJ(c&-=t2N@t+(NF`bT!UMFbXNRyik?OPd z|B%ar4jgD9%$qQ!kQX3N{4|=$kaxYLp8&eKK!-@=c#Fb|lxPqLPu0fv7RE{vEswN& zU%&zr$OJs zJPlOxd5FQso$xI~zRW2NVC{b)S85xN;KwNZy%Kc>;kH@wxPb8w$$J1gx>S+LXiztp zloCqrCqS;aXQhS>ql1(;O}p3}E)_cPa665{>y)TG2oK51EQXBpl}54lf0JvqiwD)p zK;ec;vI@Yhli+g}uk%uhd#IW>cwbs&;PzTrBh=QXmP1 z`zp}{ki>AM-!bm_NMl+1zsc1)PlU$9#AH+Qx`n}isRTX3m`|d|34nGkmn{#GfX662 zQHk1uWY$#poFOynO2t_Fm!U2frV^pgV61tSXcq<_Q}_$xGRac`R*~y<-OGib!zlcR zS{V>-D}*Lse5Y_5ptB1WT|6CQkis>LB|52;iNZ~kXa&fF07oMjv%J)VwSN(mrI2T? z59Y^MA$uwuwnEO(3C*{HChu@p%6``hnKszbHY?;#H|e`XtLCdI%R!f{uJ0XIP&gox z6953PUI3i|D)a#p0X~sJn@c6Xqamizx(o0U31V*ieA9L;E{pzOn?wo{hR#P z?hnpy@}H<4;QYY9V|qb(fc?h(wD({CRr=xXL-w2Yci;o{yZ?9ZZ^g%`C+;`@|8Otb z&;Fjx|Mow2|Mz^7|5yK|`_b?*`=|f^|ChK2|4&d4{(WryiT;`R9s0@lyV|=Ttp->c zNZCtv;E_wE;^Is<1;StD{^PtP5y zKr{Mnb*#%Jg0-?6v4oOXW|uIKI}#jLGqfL$kt*`wnQ*p{@HUdUr4r;V7YYKI(qPnq z+WQ=t#ff{Ucov$qg#$^W*Ve|ZWJ6MT7w3=qSR+bwcgk*#6^UsC{`lz+ZV1j#;V_%o zL&!fJH6!hgsIj;ovT^8m$BUUp!$&67H7D)SZ#dMJTL2U zo`q#H$y+ujR9UG0>qJ!yOBPwQS$j^l%0a_EYDyvAq^KFjv}CAs`vA? zlT`}OB*UiiP{}89zu}Vc4vh*H3$fsl?Bak9zjo))tBbPR+@GysR%1u6D*kv4IjXTk zK%kIrMCd5Gk>Xsn;cJ(9VWgOuESXd^bPzpQh`~Di#z`_p_mv#@G@)5msV~990kSH9 zB~UjknsaRtbj7Zzeg@}_zo}YTuX#CM5BxJ@eF&>b1Z>Bu%xS!)1X+CwY|_>L$-nq= zx7Ly~0n$u5LvSyO)r&~SYr2u;8jCrF;d6N@OWAbso2O-LYA!#D4_7f3`cA6LF=$C=M&M$6T3I>KiaFOerfRG>h%JZL>%R_aG( z-TsJgXD_Lx%9^(cq+ioihMg+Ly&iFf@$@RzcP67}4jWsnI^@=yKZsL~=|hD~yd_n9 zCQ>lGx?p3Nv2q>pgBWKWymu^cjh@j~3z$-=V*uX7U$>=QJv~SH40p@2;AO;L( zn#=0>SIx7w>;f?(!1aV{+*{B|RVfgY9*v*?3z^>)Vkx!Gqeacn%qrAXs#w`3(qo5U zr6v&M90DMBjyB~T#jA#&Pk z55F^=PAwLV2VjFY5Hi31fXD_P04nFB5w-&`&MpGLQY9;2#uvtV^CEtLvn1L?l@JiV zmIv)W4LTsxYw3z4{6B>C;0{gnj`J_$+*aAWTK)H|x96|uBYQ)Tj z4V7yv795_33u0iPxPaJAzeWAH`bk2M*EC~Oi{mKE!+CW_=oT=auXtJ*f7T#hRH=;k zahcEpkVB-V({5`z7xThIcGmq_JbLY1VB)7|W1{$MEV-5@Z^J2Qf#^^>N zfNVzT32bJ#0Lonn9&;XZ3YJCmY{`CbaoOuylhUipRiL6_Lauc!7}@=Q)(uM z4Lafepr5(tDN&a?sBF9sNGuC16Vtk2U-N#K$A-V@>XLD>1aV8G*M$PV?s24;9rbqk z2wDcgVrb3W;}L(Jc=H4XBo`|NuChHCo`3984Hv1Xjy=*0Te6YE)w0+1`BC}(;826Z z>d$DEzk~~riSkI+j}A^tl6kkg6U@YTCd6qG!~j|TzI5LV))wbLSMj%zfut8&SRz3A z9KIvK2n(nqiW*7&ZTX;rKg8~0FdatpGz%r*yk~G6Mnyf@e%%tH;%^PuS%U$b7B{Wc zp=l{_6SM;xKxEoX6O-y%D)pQy+&J*cy;aruNnWC$oh9 z9AlGCp5oda=bH0#Nr4=nUR!>KlUHuqjfFk6{-Kxb5vlANKTELlug1u(dBjk$=nJUi zHsx^OW1S!kg75QR0B0OM9W+t5%=_MvTvMY(<|9^da{KuT1DG$u&DGV;!5{NQk}b1u zisVcj;`%VTq$6-Lv*?wR;@1+5H}>4R^2qt$dIt|aWcnm;P~VRRBBjacgA2K;|O4F%t<@f*msGWG%?UQAxEM!kPv!{gQ^buLwszNV#2v_u!bq!F| z=vP7!S;||S<7bwWC_sK(ZnaGi%R>WfrGg$Mf|1l}G>;Z{mVYu2G$!-GFI3AY4Sggy zh>07G`J>?>EvQn%@*9}dpFYE^ol@Ms;(n!U*{uvZ8LrvAWHs6#UWYM`8_Qv+y@1n5 zS4SH{4{k4M*Zm~z84W)(s3>8;94ZC`88%6|fmKSs%m!6d{tP)XavB z)2VpM0ZPJVR3n;R$tB{GWPgR>{$!d6;vTDQGAbxy(OLj`aX&5T_{YisEyDo7z>A1Y zOu>;-Te;isJyy08{&BQnwF8WNXAP<52BRR9ucsavSJgg#jmqvc6`ns{Q1t?i)7{|lVTaPhDCFu}$HupDv<$BU!*{iQ@ZVc_qU-LEV;6p*gYMo6*EAnM>2ShH`g)V? z=kVHH7%GR{aCc|ab8|!fcp@NcjF!J%ssGC%6*-Kpx%V_jiMvrdZ`Rz%A}Yd$I>S? zkip3=(hJA`zXN1DXyOj6aCN7Y4wKXq>kXn(@sG<>e4w(%alY%vDrIvcN;{(KTI-#9e$ zW#M~^czP%2S&I?p{>U8VRrSfNHR4*v_{)7`F|mAh3;@0$@)cfx2E6gjFz*25>B%XO z|IN)0xWJ)0jO1RE0Al#TYmRts!c~Z^-v|H)v49u#mE_@4JHIE~NabJFc&A^{d4ITj zP&tQmLwj3MhM(t_u$AW`db@fqf@Rk4kvgEUkMBW4?`|eNz(v+*4YLbr+yiEJfdcIF z3M*tyXxJB~j4B0_fTA96aVD4}Y#8wFF*_Rhuv0g!aU8gwX_rsK)j>|kik@c)&hbW@Q z9?=T+vni2Oa<0JnD)P552#dUz^pngaSYqk==r%7g;FbrnhG6S%LmlD2`KSN$D*yVv zai$$Dm7s9~emV@bjesF$Ip=U6cR@RP>}AWQ){6hsubfwDB&9j8OZk%EkpC70Z)o2T z|J;l~HUM~-JeNp4So-{53Q*YDd~u&l*O+b<@cxlwT;B1Bu>j^n9595bQyvi=H^#UO z#Nn1mdeGxOyb8H=69I(&UhJGclM0-@Kkv!Z#*lz~-^V&luX->=(VtYVM31?53G&Vo zs3g&S_{K3V&RyqqwGOXJTxF*xL>zWkomICG3}GKaAe$85sY6o5EB6znOF~6J=OK8G z;>q_mhmu4f$L)HFu+!>;(mnf`y{i|R=D~~O<7LLpA(1MxPmuKHtXh(3UAURl4)r_fRP5>-FFlW))rFPiCY|%umNh%Rt?7 zDopp;*)NoAA@+}>`m*r=THn{7GU=pLr3Kp;7kRzHI+`u*HsWw>*#5M`zp{{>Y%k@a zXDbo$0aepvn2=}yfVQnlZwfA$D#k%)!583f{Y4j(S;Wck>NOiAJK|9E-WIIc$+Wj5 z+ie|%r<{_!?j89P1@k=REG6wIun@jV3PsVvgfa9;sZhdR#cT`e$U|p{ATN#sC|f*O z@iS&dvh;k9p8wDpG6`MV#HYUB3@dP5r!glJE{P8mb@#T#4PMAc2(G^XVCr zWD5o^RF?WeGVs-7k;>oZI*c{vu{M_S? z#(E*7BOuGXO6~8s*($Rdxf6M(bd;n{BY1;8B|VqTskaV?GzLEdkus0gCmu{QP+wvor(9(B~*VH=lB2k5+z_DOT14J7Y z2h&F)IXWeih5Af*^j5N>o60}ieV=lFfa3=ek%9|~R%1+Ey8Q}X_|G>b_GJBZE}wJA8Ur7Sf^ z%ed?BJY!KSe0V)E186bFlNIt+>pA|ee$h=EDpMjFgTXT!&c9G^(5{C^jiqD3B2zeX zKY#$wBaG~lz8WXAJm)76TOchZwL#AJomaP0_?6hvJyOMsWe+Fx1zcON(WDelLn#OT zakNV0ncF0NK^y+xf34RkCSWgmG=+lMJOG&YN`TtM6wQnH{@sh2M%?Ry#Pw5yBC1+q z{X%;ELsN?85KR?GvT2x$tRllp*duDfRnaZY*~gO4{)IehcA?sa#nwAxx~d(gBd3GS(k}31>4#w*@WSF{&ECxb}{~n^1)5>Jty0-g;&}xg5us4#TMi=NSlMB{VuG_X> zPMW{pcw8?&YUx`K9V*e@T_N>HZwF`RR&nbRszV7SsyULbI(OrE54Y3xVDgDJ4iUfG=}5k=Z>wLq*I1;k_lHt z$vIj=yI5cu$VX4(OAD%`>(~5S*c45ERXaN*;V772jIk=s@&zD>(%o8TDKhkI*wWF& z$h_~?uz^@qH{4^RzNETp7UT@K?FB+;x3GXbR+inlv$@#Rk=uRm9g+CL4&#$LDJ2E5 zImv3rkDR!2rFQ9agMtX}OHNa@Z>Lm!HufeJP*0XtJ|L~jbb3)z@Hf6nzNf-BCfH8tA6X0lQy5!E5tkLA}GN-4B1QLYEFzUww4_HMWz$DMsROd zdP0XHf7c5SDq0L4@aPUn0-gCFT&g&=kj^u5tzs_lP;yUjxk|HKY65}`Pi!wbt$qB4 zLXLAIzNj_~Uerg$J4)Yn0ZA^V=0o6SaUyXoqh>3`l4{x^Bf!$v6Dj>OcgNuH`X_-v zh5qL*Z1s;A=}3mAiiz0%)hrd_O&k0rj*}0GzE79&VMPXG8xdU}HE@v`+6(~D==wSC zVTr?2+5i!*BiwRU>$?ge2htkPOJ84d9Vz)`5`+j@ON)^78;{cECUHU1rcI*`S`*A$ zQ$HB(So+*JBc({%in3pvsWYGQWgczA@9^et>ks_f&p9Yp=rN{c9L - - trophy - - - - - - - \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg b/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg deleted file mode 100644 index db374ea946..0000000000 --- a/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon-star.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - trophy - - - - - - - - - - \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg b/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg deleted file mode 100644 index 02b24390a1..0000000000 --- a/src/frontend/qt_sdl/retroachievements/resources/icons/trophy-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - trophy - - - - - - - \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/ra.qrc b/src/frontend/qt_sdl/retroachievements/resources/ra.qrc index 6413e69fbf..e73c32fa23 100644 --- a/src/frontend/qt_sdl/retroachievements/resources/ra.qrc +++ b/src/frontend/qt_sdl/retroachievements/resources/ra.qrc @@ -2,15 +2,9 @@ icons/placeholder.png - icons/ra-generic-user.png icons/ra-icon.png - icons/trophy-icon.svg - icons/trophy-icon-gray.svg - icons/trophy-icon-star.svg sounds/unlock.wav - sounds/message.wav - sounds/lbsubmit.wav \ No newline at end of file diff --git a/src/frontend/qt_sdl/retroachievements/resources/sounds/lbsubmit.wav b/src/frontend/qt_sdl/retroachievements/resources/sounds/lbsubmit.wav deleted file mode 100644 index d1b15e789a3c738a839feab653621df98521e5b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131284 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 diff --git a/src/frontend/qt_sdl/retroachievements/resources/sounds/message.wav b/src/frontend/qt_sdl/retroachievements/resources/sounds/message.wav deleted file mode 100644 index a71c02709e46eda6b24c4fd81d0003f81df6a3e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 195772 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

Os-gQ)MDy-Av2&?xt;g@7=cOtiBt)alZ_u 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 m_cache; signals: - void BadgeReady(const QString& url, const QPixmap& pix); // <-- dodaj tu + void BadgeReady(const QString& url, const QPixmap& pix); }; \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastManager.cpp b/src/frontend/qt_sdl/toast/ToastManager.cpp index 7f4c34ccf1..da5910a7e9 100644 --- a/src/frontend/qt_sdl/toast/ToastManager.cpp +++ b/src/frontend/qt_sdl/toast/ToastManager.cpp @@ -12,18 +12,15 @@ void ToastManager::Init(QWidget* widget) { if (m_overlay) return; - // Używamy 'widget' zamiast 'parent' m_overlay = new ToastOverlay(widget); - // Ustaw overlay na cały rozmiar okna m_overlay->setGeometry(widget->rect()); widget->installEventFilter(m_overlay); m_overlay->show(); - m_overlay->raise(); // Ważne: musi być na wierzchu! + m_overlay->raise(); } void ToastManager::ShowAchievement(const QString& title, const QString& description, const QPixmap& icon) { - // JEŚLI TU JEST CRASH, TO ZNACZY ŻE m_overlay TO NULL if (!m_overlay) { printf("ToastManager: Próba pokazania toasta przed Init()!\n"); return; diff --git a/src/frontend/qt_sdl/toast/ToastOverlay.cpp b/src/frontend/qt_sdl/toast/ToastOverlay.cpp index ed4c61b81f..0ea9863920 100644 --- a/src/frontend/qt_sdl/toast/ToastOverlay.cpp +++ b/src/frontend/qt_sdl/toast/ToastOverlay.cpp @@ -5,12 +5,9 @@ #include #include -// --- IMPLEMENTACJA ToastWidget --- - ToastWidget::ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent) : QWidget(parent) { - // Prosty layout dla pojedynczego toasta auto* layout = new QHBoxLayout(this); layout->setContentsMargins(10, 10, 10, 10); @@ -34,7 +31,6 @@ ToastWidget::ToastWidget(const QString& title, const QString& description, const QSize ToastWidget::sizeHint() const { - // Zwracamy sugerowany rozmiar return layout()->sizeHint(); } @@ -42,13 +38,11 @@ void ToastWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); - p.setBrush(QColor(40, 40, 40, 230)); // Ciemne tło + p.setBrush(QColor(40, 40, 40, 230)); p.setPen(Qt::NoPen); p.drawRoundedRect(rect(), 8, 8); } -// --- IMPLEMENTACJA ToastOverlay --- - ToastOverlay::ToastOverlay(QWidget* parent) : QWidget(parent) { @@ -56,7 +50,7 @@ ToastOverlay::ToastOverlay(QWidget* parent) setAttribute(Qt::WA_TranslucentBackground); m_soundEffect.setSource(QUrl("qrc:/ra/sounds/unlock.wav")); - m_soundEffect.setVolume(0.7f); // 70% głośności + m_soundEffect.setVolume(0.7f); } void ToastOverlay::ShowToast(const QString& title, const QString& description, const QPixmap& icon) @@ -69,7 +63,6 @@ void ToastOverlay::ShowToast(const QString& title, const QString& description, c RepositionToasts(); - // Auto-usuwanie po 3 sekundach QTimer::singleShot(3000, toast, [this, toast]() { m_toasts.removeOne(toast); toast->deleteLater(); @@ -79,7 +72,6 @@ void ToastOverlay::ShowToast(const QString& title, const QString& description, c void ToastOverlay::resizeEvent(QResizeEvent* event) { - // Gdy okno zmienia rozmiar, overlay też musi if (parentWidget()) setGeometry(parentWidget()->rect()); @@ -89,7 +81,6 @@ void ToastOverlay::resizeEvent(QResizeEvent* event) void ToastOverlay::paintEvent(QPaintEvent*) { - // Overlay jest przezroczysty, nic nie rysuje } void ToastOverlay::RepositionToasts() @@ -98,7 +89,7 @@ void ToastOverlay::RepositionToasts() return; const int margin = 20; - const int topOffset = 60; // 👈 TO JEST KLUCZ (menu height buffer) + const int topOffset = 60; int y = topOffset; const int rightEdge = width() - margin; diff --git a/src/frontend/qt_sdl/toast/ToastOverlay.h b/src/frontend/qt_sdl/toast/ToastOverlay.h index 2791ae7a48..0f6f5ef362 100644 --- a/src/frontend/qt_sdl/toast/ToastOverlay.h +++ b/src/frontend/qt_sdl/toast/ToastOverlay.h @@ -7,29 +7,27 @@ #include #include -// 1. Definicja klasy ToastWidget (pojedynczy dymek) class ToastWidget : public QWidget { Q_OBJECT public: ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent = nullptr); - QSize sizeHint() const override; // To naprawia błąd sizeHint + QSize sizeHint() const override; protected: void paintEvent(QPaintEvent* event) override; }; -// 2. Definicja klasy ToastOverlay (warstwa zarządzająca) class ToastOverlay : public QWidget { Q_OBJECT public: - explicit ToastOverlay(QWidget* parent = nullptr); // Tylko jedna deklaracja! + explicit ToastOverlay(QWidget* parent = nullptr); void ShowToast(const QString& title, const QString& description, const QPixmap& icon); protected: bool eventFilter(QObject* obj, QEvent* event) override; - void resizeEvent(QResizeEvent* event) override; // To naprawia błąd resizeEvent + void resizeEvent(QResizeEvent* event) override; void paintEvent(QPaintEvent* event) override; private: From 4e60f9ee79202fbf64061271224858d6a07d12f9 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:14:02 +0100 Subject: [PATCH 36/41] forgor .ui --- src/frontend/qt_sdl/RASettingsDialog.ui | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/frontend/qt_sdl/RASettingsDialog.ui diff --git a/src/frontend/qt_sdl/RASettingsDialog.ui b/src/frontend/qt_sdl/RASettingsDialog.ui new file mode 100644 index 0000000000..10cd329c1d --- /dev/null +++ b/src/frontend/qt_sdl/RASettingsDialog.ui @@ -0,0 +1,116 @@ + + + RASettingsDialog + + + + 0 + 0 + 420 + 300 + + + + RetroAchievements Settings + + + + + + + RA + + + + + + RetroAchievements Settings + + + + + + Enable RetroAchievements + + + + + + + Hardcore Mode (No Savestates) + + + + + + + Username: + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + Login Now + + + + + + + Not logged in + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok|QDialogButtonBox::Cancel + + + + + + + + \ No newline at end of file From c025f972072fda515b013a61ef1d7ac4972cc2ed Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:46:06 +0100 Subject: [PATCH 37/41] Toast Manager fixes to fix compiling --- src/frontend/qt_sdl/toast/BadgeCache.cpp | 7 -- src/frontend/qt_sdl/toast/BadgeCache.h | 14 +-- src/frontend/qt_sdl/toast/ToastManager.cpp | 131 ++++++++++++++++----- src/frontend/qt_sdl/toast/ToastManager.h | 33 ++++-- 4 files changed, 134 insertions(+), 51 deletions(-) diff --git a/src/frontend/qt_sdl/toast/BadgeCache.cpp b/src/frontend/qt_sdl/toast/BadgeCache.cpp index eb92311e68..1fa9913e56 100644 --- a/src/frontend/qt_sdl/toast/BadgeCache.cpp +++ b/src/frontend/qt_sdl/toast/BadgeCache.cpp @@ -1,12 +1,5 @@ #include "BadgeCache.h" #include -#include - -BadgeCache& BadgeCache::Get() -{ - static BadgeCache inst; - return inst; -} BadgeCache::BadgeCache(QObject* parent) : QObject(parent) diff --git a/src/frontend/qt_sdl/toast/BadgeCache.h b/src/frontend/qt_sdl/toast/BadgeCache.h index a66f42f07a..d829e5caf5 100644 --- a/src/frontend/qt_sdl/toast/BadgeCache.h +++ b/src/frontend/qt_sdl/toast/BadgeCache.h @@ -1,23 +1,21 @@ #pragma once -#include #include -#include #include -#include class BadgeCache : public QObject { Q_OBJECT public: - static BadgeCache& Get(); + explicit BadgeCache(QObject* parent = nullptr); QPixmap GetBadge(const QString& url); - void DownloadBadge(const QString& url, std::function callback); + void DownloadBadge(const QString& url, + std::function callback); + +signals: + void BadgeReady(const QString& url, const QPixmap& pix); private: - BadgeCache(QObject* parent = nullptr); QNetworkAccessManager m_net; QHash m_cache; -signals: - void BadgeReady(const QString& url, const QPixmap& pix); }; \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastManager.cpp b/src/frontend/qt_sdl/toast/ToastManager.cpp index da5910a7e9..42945f9780 100644 --- a/src/frontend/qt_sdl/toast/ToastManager.cpp +++ b/src/frontend/qt_sdl/toast/ToastManager.cpp @@ -1,40 +1,117 @@ -#include "ToastManager.h" -#include "toast/ToastOverlay.h" -#include "toast/AchievementToast.h" +#include "ToastOverlay.h" +#include +#include -ToastManager& ToastManager::Get() +ToastWidget::ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent) + : QWidget(parent) { - static ToastManager inst; - return inst; + auto* layout = new QHBoxLayout(this); + layout->setContentsMargins(10, 10, 10, 10); + + auto* iconLabel = new QLabel; + iconLabel->setPixmap(icon.scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + layout->addWidget(iconLabel); + + auto* textLayout = new QVBoxLayout; + auto* titleLabel = new QLabel(title); + titleLabel->setStyleSheet("font-weight: bold; color: white;"); + auto* descLabel = new QLabel(description); + descLabel->setStyleSheet("color: #ddd;"); + + textLayout->addWidget(titleLabel); + textLayout->addWidget(descLabel); + layout->addLayout(textLayout); + + setAttribute(Qt::WA_StyledBackground, false); + setAttribute(Qt::WA_TranslucentBackground); } -void ToastManager::Init(QWidget* widget) +QSize ToastWidget::sizeHint() const { - if (m_overlay) return; + return layout()->sizeHint(); +} - m_overlay = new ToastOverlay(widget); - - m_overlay->setGeometry(widget->rect()); - widget->installEventFilter(m_overlay); - m_overlay->show(); - m_overlay->raise(); +void ToastWidget::paintEvent(QPaintEvent*) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setBrush(QColor(40, 40, 40, 230)); + p.setPen(Qt::NoPen); + p.drawRoundedRect(rect(), 8, 8); +} + +ToastOverlay::ToastOverlay(QWidget* parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_TranslucentBackground); + + m_soundEffect.setSource(QUrl("qrc:/ra/sounds/unlock.wav")); + m_soundEffect.setVolume(0.7f); } -void ToastManager::ShowAchievement(const QString& title, const QString& description, const QPixmap& icon) + +void ToastOverlay::ShowToast(const QString& title, const QString& description, const QPixmap& icon) { - if (!m_overlay) { - printf("ToastManager: Próba pokazania toasta przed Init()!\n"); - return; - } + auto* toast = new ToastWidget(title, description, icon, this); + toast->show(); + m_toasts.append(toast); + + m_soundEffect.play(); - m_overlay->ShowToast(title, description, icon); + RepositionToasts(); + + QTimer::singleShot(3000, toast, [this, toast]() { + m_toasts.removeOne(toast); + toast->deleteLater(); + RepositionToasts(); + }); } -void TestAchievementToast() + +void ToastOverlay::resizeEvent(QResizeEvent* event) { - QPixmap icon(":/ra/placeholder.png"); + if (parentWidget()) + setGeometry(parentWidget()->rect()); + + RepositionToasts(); + QWidget::resizeEvent(event); +} + +void ToastOverlay::paintEvent(QPaintEvent*) +{ +} + +void ToastOverlay::RepositionToasts() +{ + if (!parentWidget()) + return; + + const int margin = 20; + const int topOffset = 60; + + int y = topOffset; + const int rightEdge = width() - margin; + + for (auto* toast : m_toasts) + { + toast->adjustSize(); + + int x = rightEdge - toast->width(); + toast->move(x, y); + + y += toast->height() + 10; + } +} +bool ToastOverlay::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == parentWidget()) + { + if (event->type() == QEvent::Resize || + event->type() == QEvent::WindowStateChange) + { + setGeometry(parentWidget()->rect()); + RepositionToasts(); + } + } - ToastManager::Get().ShowAchievement( - "Test Achievement", - "To jest testowy toast", - icon - ); + return QWidget::eventFilter(obj, event); } \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastManager.h b/src/frontend/qt_sdl/toast/ToastManager.h index 2f84dcfbd9..6f6444b03d 100644 --- a/src/frontend/qt_sdl/toast/ToastManager.h +++ b/src/frontend/qt_sdl/toast/ToastManager.h @@ -1,19 +1,34 @@ #pragma once -#include +#include +#include +#include +#include -class ToastOverlay; +class ToastWidget : public QWidget +{ + Q_OBJECT +public: + ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent = nullptr); + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent* event) override; +}; -class ToastManager : public QObject +class ToastOverlay : public QWidget { Q_OBJECT public: - static ToastManager& Get(); + explicit ToastOverlay(QWidget* parent = nullptr); + void ShowToast(const QString& title, const QString& description, const QPixmap& icon); - void Init(QWidget* screenWidget); - void ShowAchievement(const QString& title, - const QString& desc, - const QPixmap& icon); +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void paintEvent(QPaintEvent* event) override; private: - ToastOverlay* m_overlay = nullptr; + QList m_toasts; + void RepositionToasts(); + QSoundEffect m_soundEffect; }; \ No newline at end of file From 738a8ca22c2e396a03cce5617815da7b65f9ff26 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:59:21 +0100 Subject: [PATCH 38/41] now toast should be good i guess --- .../qt_sdl/toast/AchievementToast.cpp | 1 - src/frontend/qt_sdl/toast/ToastManager.cpp | 124 +++--------------- src/frontend/qt_sdl/toast/ToastManager.h | 33 ++--- src/frontend/qt_sdl/toast/ToastOverlay.cpp | 3 - src/frontend/qt_sdl/toast/ToastOverlay.h | 3 - 5 files changed, 27 insertions(+), 137 deletions(-) diff --git a/src/frontend/qt_sdl/toast/AchievementToast.cpp b/src/frontend/qt_sdl/toast/AchievementToast.cpp index c4df7c9a7e..0d7cc5a6dc 100644 --- a/src/frontend/qt_sdl/toast/AchievementToast.cpp +++ b/src/frontend/qt_sdl/toast/AchievementToast.cpp @@ -1,6 +1,5 @@ #include "AchievementToast.h" #include -#include #include AchievementToast::AchievementToast(const QString& title, diff --git a/src/frontend/qt_sdl/toast/ToastManager.cpp b/src/frontend/qt_sdl/toast/ToastManager.cpp index 42945f9780..3118baed04 100644 --- a/src/frontend/qt_sdl/toast/ToastManager.cpp +++ b/src/frontend/qt_sdl/toast/ToastManager.cpp @@ -1,117 +1,29 @@ -#include "ToastOverlay.h" -#include -#include +#include "ToastManager.h" +#include "toast/ToastOverlay.h" +#include "toast/AchievementToast.h" -ToastWidget::ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent) - : QWidget(parent) -{ - auto* layout = new QHBoxLayout(this); - layout->setContentsMargins(10, 10, 10, 10); - - auto* iconLabel = new QLabel; - iconLabel->setPixmap(icon.scaled(32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - layout->addWidget(iconLabel); - - auto* textLayout = new QVBoxLayout; - auto* titleLabel = new QLabel(title); - titleLabel->setStyleSheet("font-weight: bold; color: white;"); - auto* descLabel = new QLabel(description); - descLabel->setStyleSheet("color: #ddd;"); - - textLayout->addWidget(titleLabel); - textLayout->addWidget(descLabel); - layout->addLayout(textLayout); - - setAttribute(Qt::WA_StyledBackground, false); - setAttribute(Qt::WA_TranslucentBackground); -} - -QSize ToastWidget::sizeHint() const -{ - return layout()->sizeHint(); -} - -void ToastWidget::paintEvent(QPaintEvent*) -{ - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - p.setBrush(QColor(40, 40, 40, 230)); - p.setPen(Qt::NoPen); - p.drawRoundedRect(rect(), 8, 8); -} - -ToastOverlay::ToastOverlay(QWidget* parent) - : QWidget(parent) -{ - setAttribute(Qt::WA_TransparentForMouseEvents); - setAttribute(Qt::WA_TranslucentBackground); - - m_soundEffect.setSource(QUrl("qrc:/ra/sounds/unlock.wav")); - m_soundEffect.setVolume(0.7f); -} - -void ToastOverlay::ShowToast(const QString& title, const QString& description, const QPixmap& icon) -{ - auto* toast = new ToastWidget(title, description, icon, this); - toast->show(); - m_toasts.append(toast); - - m_soundEffect.play(); - - RepositionToasts(); - - QTimer::singleShot(3000, toast, [this, toast]() { - m_toasts.removeOne(toast); - toast->deleteLater(); - RepositionToasts(); - }); -} - -void ToastOverlay::resizeEvent(QResizeEvent* event) -{ - if (parentWidget()) - setGeometry(parentWidget()->rect()); - - RepositionToasts(); - QWidget::resizeEvent(event); -} - -void ToastOverlay::paintEvent(QPaintEvent*) +ToastManager::ToastManager(QObject* parent) + : QObject(parent) { } -void ToastOverlay::RepositionToasts() +void ToastManager::Init(QWidget* widget) { - if (!parentWidget()) - return; - - const int margin = 20; - const int topOffset = 60; - - int y = topOffset; - const int rightEdge = width() - margin; + if (m_overlay) return; - for (auto* toast : m_toasts) - { - toast->adjustSize(); - - int x = rightEdge - toast->width(); - toast->move(x, y); - - y += toast->height() + 10; - } + m_overlay = new ToastOverlay(widget); + + m_overlay->setGeometry(widget->rect()); + widget->installEventFilter(m_overlay); + m_overlay->show(); + m_overlay->raise(); } -bool ToastOverlay::eventFilter(QObject* obj, QEvent* event) +void ToastManager::ShowAchievement(const QString& title, const QString& description, const QPixmap& icon) { - if (obj == parentWidget()) - { - if (event->type() == QEvent::Resize || - event->type() == QEvent::WindowStateChange) - { - setGeometry(parentWidget()->rect()); - RepositionToasts(); - } + if (!m_overlay) { + printf("ToastManager: Próba pokazania toasta przed Init()!\n"); + return; } - return QWidget::eventFilter(obj, event); + m_overlay->ShowToast(title, description, icon); } \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastManager.h b/src/frontend/qt_sdl/toast/ToastManager.h index 6f6444b03d..d7fe3f1723 100644 --- a/src/frontend/qt_sdl/toast/ToastManager.h +++ b/src/frontend/qt_sdl/toast/ToastManager.h @@ -1,34 +1,19 @@ #pragma once -#include -#include -#include -#include +#include -class ToastWidget : public QWidget -{ - Q_OBJECT -public: - ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent = nullptr); - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent* event) override; -}; +class ToastOverlay; -class ToastOverlay : public QWidget +class ToastManager : public QObject { Q_OBJECT public: - explicit ToastOverlay(QWidget* parent = nullptr); - void ShowToast(const QString& title, const QString& description, const QPixmap& icon); + explicit ToastManager(QObject* parent = nullptr); -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - void resizeEvent(QResizeEvent* event) override; - void paintEvent(QPaintEvent* event) override; + void Init(QWidget* screenWidget); + void ShowAchievement(const QString& title, + const QString& desc, + const QPixmap& icon); private: - QList m_toasts; - void RepositionToasts(); - QSoundEffect m_soundEffect; + ToastOverlay* m_overlay = nullptr; }; \ No newline at end of file diff --git a/src/frontend/qt_sdl/toast/ToastOverlay.cpp b/src/frontend/qt_sdl/toast/ToastOverlay.cpp index 0ea9863920..42945f9780 100644 --- a/src/frontend/qt_sdl/toast/ToastOverlay.cpp +++ b/src/frontend/qt_sdl/toast/ToastOverlay.cpp @@ -1,9 +1,6 @@ #include "ToastOverlay.h" #include -#include -#include #include -#include ToastWidget::ToastWidget(const QString& title, const QString& description, const QPixmap& icon, QWidget* parent) : QWidget(parent) diff --git a/src/frontend/qt_sdl/toast/ToastOverlay.h b/src/frontend/qt_sdl/toast/ToastOverlay.h index 0f6f5ef362..6f6444b03d 100644 --- a/src/frontend/qt_sdl/toast/ToastOverlay.h +++ b/src/frontend/qt_sdl/toast/ToastOverlay.h @@ -1,10 +1,7 @@ #pragma once -#include -#include #include #include #include -#include #include class ToastWidget : public QWidget From 0ec40be455419e9444bc8e40ef726572f63437f1 Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:19:16 +0100 Subject: [PATCH 39/41] More cleaning up --- src/CMakeLists.txt | 69 ++++++++++++------------------- src/NDSCart.cpp | 1 - src/frontend/qt_sdl/Config.cpp | 4 -- src/frontend/qt_sdl/EmuInstance.h | 2 +- src/frontend/qt_sdl/EmuThread.cpp | 1 - src/frontend/qt_sdl/Screen.cpp | 12 +++--- src/frontend/qt_sdl/Window.cpp | 36 ++++++++-------- src/frontend/qt_sdl/main.cpp | 2 +- 8 files changed, 53 insertions(+), 74 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 656dd554b7..7051c92ed4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -231,56 +231,41 @@ option(ENABLE_RETROACHIEVEMENTS "Enable RetroAchievements support" OFF) if (ENABLE_RETROACHIEVEMENTS) message(STATUS "RetroAchievements: ENABLED") - target_compile_definitions(core PUBLIC RETROACHIEVEMENTS_ENABLED HAVE_CURL CURL_STATICLIB) - - set(CURL_DEPENDENCY_NAMES - curl - nghttp2 - nghttp3 - ngtcp2 - ssh2 - psl - ssl - crypto - brotlidec - brotlicommon - brotlienc - zstd - idn2 - unistring - iconv - z - ) - - set(CURL_FOUND_LIBS "") - - foreach(LIB_NAME ${CURL_DEPENDENCY_NAMES}) - find_library(LIB_PATH_${LIB_NAME} NAMES ${LIB_NAME} REQUIRED) + if (WIN32) + target_compile_definitions(core PUBLIC RETROACHIEVEMENTS_ENABLED HAVE_CURL CURL_STATICLIB) - if(LIB_PATH_${LIB_NAME}) - message(STATUS "Found ${LIB_NAME}: ${LIB_PATH_${LIB_NAME}}") - list(APPEND CURL_FOUND_LIBS ${LIB_PATH_${LIB_NAME}}) - else() - message(FATAL_ERROR "Could not find library: ${LIB_NAME}") + set(CURL_DEPENDENCY_NAMES + curl nghttp2 nghttp3 ngtcp2 ssh2 psl ssl crypto brotlidec + brotlicommon brotlienc zstd idn2 unistring iconv z + ) + + set(CURL_FOUND_LIBS "") + foreach(LIB_NAME ${CURL_DEPENDENCY_NAMES}) + find_library(LIB_PATH_${LIB_NAME} NAMES ${LIB_NAME} REQUIRED) + if(LIB_PATH_${LIB_NAME}) + list(APPEND CURL_FOUND_LIBS ${LIB_PATH_${LIB_NAME}}) + endif() + endforeach() + + find_library(LIB_NGTCP2_CRYPTO NAMES ngtcp2_crypto_ossl ngtcp2_crypto_openssl) + if(LIB_NGTCP2_CRYPTO) + list(INSERT CURL_FOUND_LIBS 4 ${LIB_NGTCP2_CRYPTO}) endif() - endforeach() - find_library(LIB_NGTCP2_CRYPTO NAMES ngtcp2_crypto_ossl ngtcp2_crypto_openssl) - if(LIB_NGTCP2_CRYPTO) - message(STATUS "Found ngtcp2 crypto: ${LIB_NGTCP2_CRYPTO}") - list(INSERT CURL_FOUND_LIBS 4 ${LIB_NGTCP2_CRYPTO}) + target_link_libraries(core PRIVATE + ${CURL_FOUND_LIBS} + ws2_32 wldap32 crypt32 normaliz advapi32 secur32 bcrypt + ) else() - message(FATAL_ERROR "Could not find ngtcp2_crypto (checked ossl and openssl variants)") + find_package(CURL REQUIRED) + target_compile_definitions(core PUBLIC RETROACHIEVEMENTS_ENABLED HAVE_CURL) + target_link_libraries(core PRIVATE ${CURL_LIBRARIES}) + target_include_directories(core PRIVATE ${CURL_INCLUDE_DIRS}) endif() - target_link_libraries(core PRIVATE - ${CURL_FOUND_LIBS} - ws2_32 wldap32 crypt32 normaliz advapi32 secur32 bcrypt - ) - target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/RetroAchievements" - "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}" ) target_sources(core PRIVATE diff --git a/src/NDSCart.cpp b/src/NDSCart.cpp index dffc71e0ed..d34997ed6b 100644 --- a/src/NDSCart.cpp +++ b/src/NDSCart.cpp @@ -32,7 +32,6 @@ #include #endif - namespace melonDS { using Platform::Log; diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 8ca32a8dd9..816cf8b766 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -803,9 +803,7 @@ bool Load() RootTable = toml::value(); if (!Platform::FileExists(cfgpath)) - { return LoadLegacy(); - } try { @@ -821,8 +819,6 @@ bool Load() void Save() { - Table tbl = GetGlobalTable(); - auto cfgpath = Platform::GetLocalFilePath(kConfigFile); if (!Platform::CheckFileWritable(cfgpath)) return; diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index 9bf5162ce2..55b413a8ba 100755 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -95,7 +95,7 @@ class EmuInstance #endif EmuInstance(int inst); ~EmuInstance(); - + int getInstanceID() { return instanceID; } int getConsoleType() { return consoleType; } EmuThread* getEmuThread() { return emuThread; } diff --git a/src/frontend/qt_sdl/EmuThread.cpp b/src/frontend/qt_sdl/EmuThread.cpp index a90547fa3c..44843f349e 100755 --- a/src/frontend/qt_sdl/EmuThread.cpp +++ b/src/frontend/qt_sdl/EmuThread.cpp @@ -664,7 +664,6 @@ void EmuThread::handleMessages() #endif msgResult = emuInstance->saveState(msg.param.value().toStdString()); break; - case msg_LoadState: #ifdef RETROACHIEVEMENTS_ENABLED diff --git a/src/frontend/qt_sdl/Screen.cpp b/src/frontend/qt_sdl/Screen.cpp index f0e42c43e2..3126ea8fde 100755 --- a/src/frontend/qt_sdl/Screen.cpp +++ b/src/frontend/qt_sdl/Screen.cpp @@ -43,8 +43,6 @@ #include "OSD_shaders.h" #include "font.h" #include "version.h" -#include "RetroAchievements/RAClient.h" -#include using namespace melonDS; @@ -339,10 +337,10 @@ void ScreenPanel::tabletEvent(QTabletEvent* event) void ScreenPanel::touchEvent(QTouchEvent* event) { - #if QT_VERSION_MAJOR == 6 +#if QT_VERSION_MAJOR == 6 if (event->device()->type() == QInputDevice::DeviceType::TouchPad) return; - #endif +#endif event->accept(); if (!emuInstance->emuIsActive()) { touching = false; return; } @@ -351,15 +349,15 @@ void ScreenPanel::touchEvent(QTouchEvent* event) { case QEvent::TouchBegin: case QEvent::TouchUpdate: - #if QT_VERSION_MAJOR == 6 +#if QT_VERSION_MAJOR == 6 if (event->points().length() > 0) { QPointF lastPosition = event->points().first().lastPosition(); - #else +#else if (event->touchPoints().length() > 0) { QPointF lastPosition = event->touchPoints().first().lastPos(); - #endif +#endif int x = (int)lastPosition.x(); int y = (int)lastPosition.y(); diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index c8705880d7..4de23dfd5b 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -246,7 +246,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : #endif focused(true) { - #ifndef _WIN32 +#ifndef _WIN32 if (!parent) { if (socketpair(AF_UNIX, SOCK_STREAM, 0, signalFd)) @@ -265,7 +265,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : sa.sa_flags |= SA_RESTART; sigaction(SIGINT, &sa, 0); } - #endif +#endif showOSD = windowCfg.GetBool("ShowOSD"); @@ -274,12 +274,12 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : setAcceptDrops(true); setFocusPolicy(Qt::ClickFocus); - #if QT_VERSION_MAJOR == 6 && WIN32 +#if QT_VERSION_MAJOR == 6 && WIN32 // The "windows11" theme has pretty massive padding around menubar items, this makes Config and Help not fit in a window at 1x screen sizing // So let's reduce the padding a bit. if (QApplication::style()->name() == "windows11") setStyleSheet("QMenuBar::item { padding: 4px 8px; }"); - #endif +#endif //hasMenu = (!parent); hasMenu = true; @@ -629,11 +629,11 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actEmuSettings = menu->addAction("Emu settings"); connect(actEmuSettings, &QAction::triggered, this, &MainWindow::onOpenEmuSettings); - #ifdef __APPLE__ +#ifdef __APPLE__ actPreferences = menu->addAction("Preferences..."); connect(actPreferences, &QAction::triggered, this, &MainWindow::onOpenEmuSettings); actPreferences->setMenuRole(QAction::PreferencesRole); - #endif +#endif actInputConfig = menu->addAction("Input and hotkeys"); connect(actInputConfig, &QAction::triggered, this, &MainWindow::onOpenInputConfig); @@ -701,12 +701,12 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actMPNewInstance->setText("Fart"); } - #ifdef Q_OS_MAC +#ifdef Q_OS_MAC QPoint screenCenter = screen()->availableGeometry().center(); QRect frameGeo = frameGeometry(); frameGeo.moveCenter(screenCenter); move(frameGeo.topLeft()); - #endif +#endif std::string geom = windowCfg.GetString("Geometry"); if (!geom.empty()) @@ -722,6 +722,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : panel = nullptr; createScreenPanel(); + if (hasMenu) { actEjectCart->setEnabled(false); @@ -803,10 +804,11 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actWifiSettings->setEnabled(false); actInterfaceSettings->setEnabled(false); - #ifdef __APPLE__ +#ifdef __APPLE__ actPreferences->setEnabled(false); - #endif // __APPLE__ +#endif // __APPLE__ } + #ifdef RETROACHIEVEMENTS_ENABLED m_oldRAEnabled = Config::GetLocalTable(emuInstance->instanceID).GetBool("Instance*.RetroAchievements.Enabled"); m_toastManager.Init(this); @@ -842,7 +844,7 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : if (emuThread->emuIsActive()) onEmuStart(); } - + QObject::connect(qApp, &QApplication::applicationStateChanged, this, &MainWindow::onAppStateChanged); onUpdateInterfaceSettings(); @@ -1680,9 +1682,9 @@ void MainWindow::onLoadState() // TODO: specific 'last directory' for savestate files? emuThread->emuPause(); filename = QFileDialog::getOpenFileName(this, - "Load state", - globalCfg.GetQString("LastROMFolder"), - "melonDS savestates (*.ml*);;Any file (*.*)"); + "Load state", + globalCfg.GetQString("LastROMFolder"), + "melonDS savestates (*.ml*);;Any file (*.*)"); emuThread->emuUnpause(); if (filename.isEmpty()) return; @@ -1855,7 +1857,7 @@ void MainWindow::onSetupCheats() } #endif emuThread->emuPause(); - + CheatsDialog* dlg = CheatsDialog::openDlg(this); connect(dlg, &CheatsDialog::finished, this, &MainWindow::onCheatsDialogFinished); } @@ -1988,7 +1990,6 @@ if (auto* ra = emuInstance->getRA()) { const bool newRAEnabled = localCfg.GetBool("Instance*.RetroAchievements.Enabled"); - if (m_oldRAEnabled != newRAEnabled) { if (newRAEnabled) @@ -2029,7 +2030,8 @@ if (auto* ra = emuInstance->getRA()) } } #endif -emuThread->emuUnpause(); + + emuThread->emuUnpause(); } void MainWindow::onOpenInputConfig() diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index 75cbdb755c..bfbb874e7a 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -369,7 +369,7 @@ int main(int argc, char** argv) NetInit(); createEmuInstance(); - + #ifdef RETROACHIEVEMENTS_ENABLED if (emuInstances[0]) { From 785e00b4f2aa9d0b7f846a16e7b099a89df3ab3e Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:57:34 +0100 Subject: [PATCH 40/41] EVEN MORE CLEANING UP --- src/frontend/qt_sdl/EmuSettingsDialog.cpp | 2 +- src/frontend/qt_sdl/EmuSettingsDialog.h | 2 +- src/rcheevos/include/module.modulemap | 70 - src/rcheevos/src/rc_client_external.c | 277 - src/rcheevos/src/rc_client_external.h | 173 - .../src/rc_client_external_versions.h | 171 - src/rcheevos/src/rc_client_raintegration.c | 566 - .../src/rc_client_raintegration_internal.h | 61 - src/rcheevos/src/rc_client_types.natvis | 396 - src/rcheevos/src/rc_libretro.c | 915 -- src/rcheevos/src/rc_libretro.h | 98 - .../src/rcheevos/rc_runtime_types.natvis | 541 - src/rcheevos/src/rhash/aes.c | 480 - src/rcheevos/test/Makefile | 224 - src/rcheevos/test/libretro.h | 205 - src/rcheevos/test/rapi/test_rc_api_common.c | 941 -- src/rcheevos/test/rapi/test_rc_api_editor.c | 931 -- src/rcheevos/test/rapi/test_rc_api_info.c | 537 - src/rcheevos/test/rapi/test_rc_api_runtime.c | 2213 ---- src/rcheevos/test/rapi/test_rc_api_user.c | 998 -- src/rcheevos/test/rcheevos-test.sln | 46 - src/rcheevos/test/rcheevos-test.vcxproj | 239 - .../test/rcheevos-test.vcxproj.filters | 335 - src/rcheevos/test/rcheevos/mock_memory.h | 32 - src/rcheevos/test/rcheevos/test_condition.c | 570 - src/rcheevos/test/rcheevos/test_condset.c | 5170 -------- src/rcheevos/test/rcheevos/test_consoleinfo.c | 203 - src/rcheevos/test/rcheevos/test_format.c | 112 - src/rcheevos/test/rcheevos/test_lboard.c | 746 -- src/rcheevos/test/rcheevos/test_memref.c | 520 - src/rcheevos/test/rcheevos/test_operand.c | 692 -- src/rcheevos/test/rcheevos/test_rc_validate.c | 500 - .../test/rcheevos/test_richpresence.c | 1542 --- src/rcheevos/test/rcheevos/test_runtime.c | 1667 --- .../test/rcheevos/test_runtime_progress.c | 1821 --- src/rcheevos/test/rcheevos/test_timing.c | 166 - src/rcheevos/test/rcheevos/test_trigger.c | 2492 ---- src/rcheevos/test/rcheevos/test_value.c | 868 -- src/rcheevos/test/rhash/data.c | 657 - src/rcheevos/test/rhash/data.h | 32 - src/rcheevos/test/rhash/mock_filereader.c | 236 - src/rcheevos/test/rhash/mock_filereader.h | 31 - src/rcheevos/test/rhash/test_cdreader.c | 920 -- src/rcheevos/test/rhash/test_hash.c | 310 - src/rcheevos/test/rhash/test_hash_disc.c | 1450 --- src/rcheevos/test/rhash/test_hash_rom.c | 899 -- src/rcheevos/test/rhash/test_hash_zip.c | 551 - src/rcheevos/test/test.c | 113 - src/rcheevos/test/test_framework.h | 205 - src/rcheevos/test/test_rc_client.c | 10329 ---------------- src/rcheevos/test/test_rc_client_external.c | 2170 ---- .../test/test_rc_client_raintegration.c | 441 - src/rcheevos/test/test_rc_libretro.c | 952 -- src/rcheevos/test/test_types.natvis | 9 - src/rcheevos/validator/Makefile | 25 - src/rcheevos/validator/validator.c | 658 - src/rcheevos/validator/validator.vcxproj | 152 - .../validator/validator.vcxproj.filters | 82 - 58 files changed, 2 insertions(+), 47742 deletions(-) delete mode 100644 src/rcheevos/include/module.modulemap delete mode 100644 src/rcheevos/src/rc_client_external.c delete mode 100644 src/rcheevos/src/rc_client_external.h delete mode 100644 src/rcheevos/src/rc_client_external_versions.h delete mode 100644 src/rcheevos/src/rc_client_raintegration.c delete mode 100644 src/rcheevos/src/rc_client_raintegration_internal.h delete mode 100644 src/rcheevos/src/rc_client_types.natvis delete mode 100644 src/rcheevos/src/rc_libretro.c delete mode 100644 src/rcheevos/src/rc_libretro.h delete mode 100644 src/rcheevos/src/rcheevos/rc_runtime_types.natvis delete mode 100644 src/rcheevos/src/rhash/aes.c delete mode 100644 src/rcheevos/test/Makefile delete mode 100644 src/rcheevos/test/libretro.h delete mode 100644 src/rcheevos/test/rapi/test_rc_api_common.c delete mode 100644 src/rcheevos/test/rapi/test_rc_api_editor.c delete mode 100644 src/rcheevos/test/rapi/test_rc_api_info.c delete mode 100644 src/rcheevos/test/rapi/test_rc_api_runtime.c delete mode 100644 src/rcheevos/test/rapi/test_rc_api_user.c delete mode 100644 src/rcheevos/test/rcheevos-test.sln delete mode 100644 src/rcheevos/test/rcheevos-test.vcxproj delete mode 100644 src/rcheevos/test/rcheevos-test.vcxproj.filters delete mode 100644 src/rcheevos/test/rcheevos/mock_memory.h delete mode 100644 src/rcheevos/test/rcheevos/test_condition.c delete mode 100644 src/rcheevos/test/rcheevos/test_condset.c delete mode 100644 src/rcheevos/test/rcheevos/test_consoleinfo.c delete mode 100644 src/rcheevos/test/rcheevos/test_format.c delete mode 100644 src/rcheevos/test/rcheevos/test_lboard.c delete mode 100644 src/rcheevos/test/rcheevos/test_memref.c delete mode 100644 src/rcheevos/test/rcheevos/test_operand.c delete mode 100644 src/rcheevos/test/rcheevos/test_rc_validate.c delete mode 100644 src/rcheevos/test/rcheevos/test_richpresence.c delete mode 100644 src/rcheevos/test/rcheevos/test_runtime.c delete mode 100644 src/rcheevos/test/rcheevos/test_runtime_progress.c delete mode 100644 src/rcheevos/test/rcheevos/test_timing.c delete mode 100644 src/rcheevos/test/rcheevos/test_trigger.c delete mode 100644 src/rcheevos/test/rcheevos/test_value.c delete mode 100644 src/rcheevos/test/rhash/data.c delete mode 100644 src/rcheevos/test/rhash/data.h delete mode 100644 src/rcheevos/test/rhash/mock_filereader.c delete mode 100644 src/rcheevos/test/rhash/mock_filereader.h delete mode 100644 src/rcheevos/test/rhash/test_cdreader.c delete mode 100644 src/rcheevos/test/rhash/test_hash.c delete mode 100644 src/rcheevos/test/rhash/test_hash_disc.c delete mode 100644 src/rcheevos/test/rhash/test_hash_rom.c delete mode 100644 src/rcheevos/test/rhash/test_hash_zip.c delete mode 100644 src/rcheevos/test/test.c delete mode 100644 src/rcheevos/test/test_framework.h delete mode 100644 src/rcheevos/test/test_rc_client.c delete mode 100644 src/rcheevos/test/test_rc_client_external.c delete mode 100644 src/rcheevos/test/test_rc_client_raintegration.c delete mode 100644 src/rcheevos/test/test_rc_libretro.c delete mode 100644 src/rcheevos/test/test_types.natvis delete mode 100644 src/rcheevos/validator/Makefile delete mode 100644 src/rcheevos/validator/validator.c delete mode 100644 src/rcheevos/validator/validator.vcxproj delete mode 100644 src/rcheevos/validator/validator.vcxproj.filters diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.cpp b/src/frontend/qt_sdl/EmuSettingsDialog.cpp index 10f20e4e0d..c442289c70 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.cpp +++ b/src/frontend/qt_sdl/EmuSettingsDialog.cpp @@ -584,4 +584,4 @@ void EmuSettingsDialog::on_chkExternalBIOS_toggled() ui->btnBIOS7Browse->setDisabled(disabled); ui->txtFirmwarePath->setDisabled(disabled); ui->btnFirmwareBrowse->setDisabled(disabled); -} \ No newline at end of file +} diff --git a/src/frontend/qt_sdl/EmuSettingsDialog.h b/src/frontend/qt_sdl/EmuSettingsDialog.h index ec01bf167a..b3b792185e 100644 --- a/src/frontend/qt_sdl/EmuSettingsDialog.h +++ b/src/frontend/qt_sdl/EmuSettingsDialog.h @@ -90,4 +90,4 @@ private slots: QString lastBIOSFolder; }; -#endif // EMUSETTINGSDIALOG_H \ No newline at end of file +#endif // EMUSETTINGSDIALOG_H diff --git a/src/rcheevos/include/module.modulemap b/src/rcheevos/include/module.modulemap deleted file mode 100644 index 2a53853df3..0000000000 --- a/src/rcheevos/include/module.modulemap +++ /dev/null @@ -1,70 +0,0 @@ -module rcheevos { - umbrella header "rcheevos.h" - - module rapi { - module rc_api_editor { - header "rc_api_editor.h" - export * - } - - module rc_api_info { - header "rc_api_info.h" - export * - } - - module rc_api_request { - header "rc_api_request.h" - export * - } - - module rc_api_runtime { - header "rc_api_runtime.h" - export * - } - - module rc_api_user { - header "rc_api_user.h" - export * - } - - export * - } - - module rc_client { - header "rc_client.h" - export * - } - - explicit module rc_client_raintegration { - header "rc_client_raintegration.h" - export * - } - - module rc_consoles { - header "rc_consoles.h" - export * - } - - module rc_error { - header "rc_error.h" - export * - } - - module rc_runtime { - header "rc_runtime.h" - export * - } - - module rc_runtime_types { - header "rc_runtime_types.h" - export * - } - - module rc_hash { - header "rc_hash.h" - export * - } - - export * - module * { export * } -} diff --git a/src/rcheevos/src/rc_client_external.c b/src/rcheevos/src/rc_client_external.c deleted file mode 100644 index 0cf90dbb02..0000000000 --- a/src/rcheevos/src/rc_client_external.c +++ /dev/null @@ -1,277 +0,0 @@ -#include "rc_client_external.h" - -#include "rc_client_external_versions.h" -#include "rc_client_internal.h" - -#include "rc_api_runtime.h" - -#define RC_CONVERSION_FILL(obj, obj_type, src_type) memset((uint8_t*)obj + sizeof(src_type), 0, sizeof(obj_type) - sizeof(src_type)) - -/* https://media.retroachievements.org/Badge/123456_lock.png is 58 with null terminator */ -#define RC_CLIENT_IMAGE_URL_BUFFER_SIZE 64 -/* https://media.retroachievements.org/UserPic/TwentyCharUserNameXX.png is 69 with null terminator */ -#define RC_CLIENT_USER_IMAGE_URL_BUFFER_SIZE 80 - -typedef struct rc_client_external_conversions_t { - rc_client_user_t user; - rc_client_game_t game; - rc_client_subset_t subsets[4]; - rc_client_achievement_t achievements[16]; - char user_avatar_url[RC_CLIENT_USER_IMAGE_URL_BUFFER_SIZE]; - char game_badge_url[RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; - char subset_badge_url[4][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; - char achievement_badge_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; - char achievement_badge_locked_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; - uint32_t next_subset_index; - uint32_t next_achievement_index; -} rc_client_external_conversions_t; - -static const char* rc_client_external_build_avatar_url(char buffer[], size_t buffer_size, uint32_t image_type, const char* image_name) -{ - rc_api_fetch_image_request_t image_request; - rc_api_request_t request; - int result; - - memset(&image_request, 0, sizeof(image_request)); - image_request.image_type = image_type; - image_request.image_name = image_name; - - result = rc_api_init_fetch_image_request(&request, &image_request); - if (result != RC_OK) - return NULL; - - snprintf(buffer, buffer_size, "%s", request.url); - return buffer; -} - -static void rc_client_external_conversions_init(const rc_client_t* client) -{ - if (!client->state.external_client_conversions) { - rc_client_t* mutable_client = (rc_client_t*)client; - rc_client_external_conversions_t* conversions = (rc_client_external_conversions_t*) - rc_buffer_alloc(&mutable_client->state.buffer, sizeof(rc_client_external_conversions_t)); - - memset(conversions, 0, sizeof(*conversions)); - - mutable_client->state.external_client_conversions = conversions; - } -} - -const rc_client_user_t* rc_client_external_convert_v1_user(const rc_client_t* client, const rc_client_user_t* v1_user) -{ - rc_client_user_t* converted; - - if (!v1_user) - return NULL; - - rc_client_external_conversions_init(client); - - converted = &client->state.external_client_conversions->user; - memcpy(converted, v1_user, sizeof(v1_rc_client_user_t)); - RC_CONVERSION_FILL(converted, rc_client_user_t, v1_rc_client_user_t); - - converted->avatar_url = rc_client_external_build_avatar_url( - client->state.external_client_conversions->user_avatar_url, - sizeof(client->state.external_client_conversions->user_avatar_url), - RC_IMAGE_TYPE_USER, v1_user->username); - - return converted; -} - -const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* client, const rc_client_game_t* v1_game) -{ - rc_client_game_t* converted; - - if (!v1_game) - return NULL; - - rc_client_external_conversions_init(client); - - converted = &client->state.external_client_conversions->game; - memcpy(converted, v1_game, sizeof(v1_rc_client_game_t)); - RC_CONVERSION_FILL(converted, rc_client_game_t, v1_rc_client_game_t); - - converted->badge_url = rc_client_external_build_avatar_url( - client->state.external_client_conversions->game_badge_url, - sizeof(client->state.external_client_conversions->game_badge_url), - RC_IMAGE_TYPE_GAME, v1_game->badge_name); - - return converted; -} - -const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset) -{ - rc_client_subset_t* converted = NULL; - char* badge_url = NULL; - const uint32_t num_subsets = sizeof(client->state.external_client_conversions->subsets) / sizeof(client->state.external_client_conversions->subsets[0]); - uint32_t index; - - if (!v1_subset) - return NULL; - - rc_client_external_conversions_init(client); - - for (index = 0; index < num_subsets; ++index) { - if (client->state.external_client_conversions->subsets[index].id == v1_subset->id) { - converted = &client->state.external_client_conversions->subsets[index]; - badge_url = client->state.external_client_conversions->subset_badge_url[index]; - break; - } - } - - if (!converted) { - index = client->state.external_client_conversions->next_subset_index; - converted = &client->state.external_client_conversions->subsets[index]; - badge_url = client->state.external_client_conversions->subset_badge_url[client->state.external_client_conversions->next_subset_index]; - client->state.external_client_conversions->next_subset_index = (index + 1) % num_subsets; - } - - memcpy(converted, v1_subset, sizeof(v1_rc_client_subset_t)); - RC_CONVERSION_FILL(converted, rc_client_subset_t, v1_rc_client_subset_t); - - converted->badge_url = rc_client_external_build_avatar_url(badge_url, - sizeof(client->state.external_client_conversions->subset_badge_url[0]), - RC_IMAGE_TYPE_GAME, v1_subset->badge_name); - - return converted; -} - -const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement) -{ - rc_client_achievement_t* converted = NULL; - char* badge_url = NULL; - char* badge_locked_url = NULL; - const uint32_t num_achievements = sizeof(client->state.external_client_conversions->achievements) / sizeof(client->state.external_client_conversions->achievements[0]); - uint32_t index; - - if (!v1_achievement) - return NULL; - - rc_client_external_conversions_init(client); - - for (index = 0; index < num_achievements; ++index) { - if (client->state.external_client_conversions->achievements[index].id == v1_achievement->id) { - converted = &client->state.external_client_conversions->achievements[index]; - badge_url = client->state.external_client_conversions->achievement_badge_url[index]; - badge_locked_url = client->state.external_client_conversions->achievement_badge_locked_url[index]; - break; - } - } - - if (!converted) { - index = client->state.external_client_conversions->next_achievement_index; - converted = &client->state.external_client_conversions->achievements[index]; - badge_url = client->state.external_client_conversions->achievement_badge_url[index]; - badge_locked_url = client->state.external_client_conversions->achievement_badge_locked_url[index]; - client->state.external_client_conversions->next_achievement_index = (index + 1) % num_achievements; - } - - memcpy(converted, v1_achievement, sizeof(v1_rc_client_achievement_t)); - RC_CONVERSION_FILL(converted, rc_client_achievement_t, v1_rc_client_achievement_t); - - converted->badge_url = rc_client_external_build_avatar_url(badge_url, - sizeof(client->state.external_client_conversions->achievement_badge_url[0]), - RC_IMAGE_TYPE_ACHIEVEMENT, v1_achievement->badge_name); - converted->badge_locked_url = rc_client_external_build_avatar_url(badge_locked_url, - sizeof(client->state.external_client_conversions->achievement_badge_locked_url[0]), - RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, v1_achievement->badge_name); - - return converted; -} - -typedef struct rc_client_achievement_list_wrapper_t { - rc_client_achievement_list_info_t info; - rc_client_achievement_list_t* source_list; - rc_client_achievement_t* achievements; - rc_client_achievement_t** achievements_pointers; - char* badge_url_buffer; -} rc_client_achievement_list_wrapper_t; - -static void rc_client_destroy_achievement_list_wrapper(rc_client_achievement_list_info_t* info) -{ - rc_client_achievement_list_wrapper_t* wrapper = (rc_client_achievement_list_wrapper_t*)info; - - if (wrapper->achievements) - free(wrapper->achievements); - if (wrapper->achievements_pointers) - free(wrapper->achievements_pointers); - if (wrapper->info.public_.buckets) - free((void*)wrapper->info.public_.buckets); - if (wrapper->badge_url_buffer) - free(wrapper->badge_url_buffer); - - rc_client_destroy_achievement_list(wrapper->source_list); - - free(wrapper); -} - -rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list) -{ - rc_client_achievement_list_wrapper_t* new_list; - (void)client; - - if (!v1_achievement_list) - return NULL; - - new_list = (rc_client_achievement_list_wrapper_t*)calloc(1, sizeof(*new_list)); - if (!new_list) - return NULL; - - new_list->source_list = v1_achievement_list; - new_list->info.destroy_func = rc_client_destroy_achievement_list_wrapper; - - if (v1_achievement_list->num_buckets) { - const v1_rc_client_achievement_bucket_t* src_bucket = (const v1_rc_client_achievement_bucket_t*)&v1_achievement_list->buckets[0]; - const v1_rc_client_achievement_bucket_t* stop_bucket = src_bucket + v1_achievement_list->num_buckets; - rc_client_achievement_bucket_t* bucket; - uint32_t num_achievements = 0; - char* badge_url = NULL; - - new_list->info.public_.buckets = bucket = (rc_client_achievement_bucket_t*)calloc(v1_achievement_list->num_buckets, sizeof(*new_list->info.public_.buckets)); - if (!new_list->info.public_.buckets) - return (rc_client_achievement_list_t*)new_list; - - for (; src_bucket < stop_bucket; src_bucket++) - num_achievements += src_bucket->num_achievements; - - if (num_achievements) { - new_list->achievements = (rc_client_achievement_t*)calloc(num_achievements, sizeof(*new_list->achievements)); - new_list->achievements_pointers = (rc_client_achievement_t**)malloc(num_achievements * sizeof(rc_client_achievement_t*)); - new_list->badge_url_buffer = badge_url = (char*)malloc(num_achievements * 2 * RC_CLIENT_IMAGE_URL_BUFFER_SIZE); - if (!new_list->achievements || !new_list->achievements_pointers || !new_list->badge_url_buffer) - return (rc_client_achievement_list_t*)new_list; - } - - num_achievements = 0; - src_bucket = (const v1_rc_client_achievement_bucket_t*)&v1_achievement_list->buckets[0]; - for (; src_bucket < stop_bucket; src_bucket++, bucket++) { - memcpy(bucket, src_bucket, sizeof(*src_bucket)); - - if (src_bucket->num_achievements) { - const v1_rc_client_achievement_t** src_achievement = (const v1_rc_client_achievement_t**)src_bucket->achievements; - const v1_rc_client_achievement_t** stop_achievement = src_achievement + src_bucket->num_achievements; - rc_client_achievement_t** achievement = &new_list->achievements_pointers[num_achievements]; - - bucket->achievements = (const rc_client_achievement_t**)achievement; - - for (; src_achievement < stop_achievement; ++src_achievement, ++achievement) { - *achievement = &new_list->achievements[num_achievements++]; - memcpy(*achievement, *src_achievement, sizeof(**src_achievement)); - - (*achievement)->badge_url = rc_client_external_build_avatar_url(badge_url, - sizeof(client->state.external_client_conversions->achievement_badge_url[0]), - RC_IMAGE_TYPE_ACHIEVEMENT, (*achievement)->badge_name); - badge_url += RC_CLIENT_IMAGE_URL_BUFFER_SIZE; - (*achievement)->badge_locked_url = rc_client_external_build_avatar_url(badge_url, - sizeof(client->state.external_client_conversions->achievement_badge_locked_url[0]), - RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, (*achievement)->badge_name); - badge_url += RC_CLIENT_IMAGE_URL_BUFFER_SIZE; - } - } - } - - new_list->info.public_.num_buckets = v1_achievement_list->num_buckets; - } - - return (rc_client_achievement_list_t*)new_list; -} diff --git a/src/rcheevos/src/rc_client_external.h b/src/rcheevos/src/rc_client_external.h deleted file mode 100644 index d73e581b8d..0000000000 --- a/src/rcheevos/src/rc_client_external.h +++ /dev/null @@ -1,173 +0,0 @@ -#ifndef RC_CLIENT_EXTERNAL_H -#define RC_CLIENT_EXTERNAL_H - -#include "rc_client.h" - -RC_BEGIN_C_DECLS - -/* NOTE: any function that is passed a callback also needs to be passed a client instance to pass - * to the callback, and the external interface has to capture both. */ - -typedef void (RC_CCONV *rc_client_external_enable_logging_func_t)(rc_client_t* client, int level, rc_client_message_callback_t callback); -typedef void (RC_CCONV *rc_client_external_set_event_handler_func_t)(rc_client_t* client, rc_client_event_handler_t handler); -typedef void (RC_CCONV *rc_client_external_set_read_memory_func_t)(rc_client_t* client, rc_client_read_memory_func_t handler); -typedef void (RC_CCONV *rc_client_external_set_get_time_millisecs_func_t)(rc_client_t* client, rc_get_time_millisecs_func_t handler); -typedef int (RC_CCONV *rc_client_external_can_pause_func_t)(uint32_t* frames_remaining); - -typedef void (RC_CCONV *rc_client_external_set_int_func_t)(int value); -typedef int (RC_CCONV *rc_client_external_get_int_func_t)(void); -typedef void (RC_CCONV *rc_client_external_set_string_func_t)(const char* value); -typedef size_t (RC_CCONV *rc_client_external_copy_string_func_t)(char buffer[], size_t buffer_size); -typedef void (RC_CCONV *rc_client_external_action_func_t)(void); - -typedef void (RC_CCONV *rc_client_external_async_handle_func_t)(rc_client_async_handle_t* handle); - -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_login_func_t)(rc_client_t* client, - const char* username, const char* pass_token, rc_client_callback_t callback, void* callback_userdata); -typedef const rc_client_user_t* (RC_CCONV *rc_client_external_get_user_info_func_t)(void); - -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_identify_and_load_game_func_t)( - rc_client_t* client, uint32_t console_id, const char* file_path, - const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_game_func_t)(rc_client_t* client, - const char* hash, rc_client_callback_t callback, void* callback_userdata); -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_subset_t)(rc_client_t* client, - uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); -typedef const rc_client_game_t* (RC_CCONV *rc_client_external_get_game_info_func_t)(void); -typedef const rc_client_subset_t* (RC_CCONV *rc_client_external_get_subset_info_func_t)(uint32_t subset_id); -typedef void (RC_CCONV* rc_client_external_get_user_subset_summary_func_t)(uint32_t subset_id, rc_client_user_game_summary_t* summary); -typedef void (RC_CCONV *rc_client_external_get_user_game_summary_func_t)(rc_client_user_game_summary_t* summary); -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_change_media_func_t)(rc_client_t* client, const char* file_path, - const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); -typedef void (RC_CCONV* rc_client_external_add_game_hash_func_t)(const char* hash, uint32_t game_id); - -/* NOTE: rc_client_external_create_achievement_list_func_t returns an internal wrapper structure which contains the public list - * and a destructor function. */ -struct rc_client_achievement_list_info_t; -typedef struct rc_client_achievement_list_info_t* (RC_CCONV *rc_client_external_create_achievement_list_func_t)(int category, int grouping); -typedef const rc_client_achievement_t* (RC_CCONV *rc_client_external_get_achievement_info_func_t)(uint32_t id); - -/* NOTE: rc_client_external_create_leaderboard_list_func_t returns an internal wrapper structure which contains the public list - * and a destructor function. */ -struct rc_client_leaderboard_list_info_t; -typedef struct rc_client_leaderboard_list_info_t* (RC_CCONV *rc_client_external_create_leaderboard_list_func_t)(int grouping); -typedef const rc_client_leaderboard_t* (RC_CCONV *rc_client_external_get_leaderboard_info_func_t)(uint32_t id); - -/* NOTE: rc_client_external_begin_fetch_leaderboard_entries_func_t and rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t - * pass an internal wrapper structure around the list, which contains the public list and a destructor function. */ -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_fetch_leaderboard_entries_func_t)(rc_client_t* client, - uint32_t leaderboard_id, uint32_t first_entry, uint32_t count, - rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); -typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t)(rc_client_t* client, - uint32_t leaderboard_id, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); - -/* NOTE: rc_client_external_create_subset_list_func_t returns an internal wrapper structure which contains the public list - * and a destructor function. */ -struct rc_client_subset_list_info_t; -typedef struct rc_client_subset_list_info_t* (RC_CCONV* rc_client_external_create_subset_list_func_t)(); - - -typedef size_t (RC_CCONV *rc_client_external_progress_size_func_t)(void); -typedef int (RC_CCONV *rc_client_external_serialize_progress_func_t)(uint8_t* buffer, size_t buffer_size); -typedef int (RC_CCONV *rc_client_external_deserialize_progress_func_t)(const uint8_t* buffer, size_t buffer_size); - -typedef struct rc_client_external_t -{ - rc_client_external_action_func_t destroy; - - rc_client_external_enable_logging_func_t enable_logging; - rc_client_external_set_event_handler_func_t set_event_handler; - rc_client_external_set_read_memory_func_t set_read_memory; - rc_client_external_set_get_time_millisecs_func_t set_get_time_millisecs; - rc_client_external_set_string_func_t set_host; - rc_client_external_copy_string_func_t get_user_agent_clause; - - rc_client_external_set_int_func_t set_hardcore_enabled; - rc_client_external_get_int_func_t get_hardcore_enabled; - rc_client_external_set_int_func_t set_unofficial_enabled; - rc_client_external_get_int_func_t get_unofficial_enabled; - rc_client_external_set_int_func_t set_encore_mode_enabled; - rc_client_external_get_int_func_t get_encore_mode_enabled; - rc_client_external_set_int_func_t set_spectator_mode_enabled; - rc_client_external_get_int_func_t get_spectator_mode_enabled; - - rc_client_external_async_handle_func_t abort_async; - - rc_client_external_begin_login_func_t begin_login_with_password; - rc_client_external_begin_login_func_t begin_login_with_token; - rc_client_external_action_func_t logout; - rc_client_external_get_user_info_func_t get_user_info; - - rc_client_external_begin_identify_and_load_game_func_t begin_identify_and_load_game; - rc_client_external_begin_load_game_func_t begin_load_game; - rc_client_external_get_game_info_func_t get_game_info; - rc_client_external_begin_load_subset_t begin_load_subset; /* DEPRECATED */ - rc_client_external_get_subset_info_func_t get_subset_info; - rc_client_external_action_func_t unload_game; - rc_client_external_get_user_game_summary_func_t get_user_game_summary; - rc_client_external_begin_change_media_func_t begin_identify_and_change_media; - rc_client_external_begin_load_game_func_t begin_change_media; - - rc_client_external_create_achievement_list_func_t create_achievement_list; - rc_client_external_get_int_func_t has_achievements; - rc_client_external_get_achievement_info_func_t get_achievement_info; - - rc_client_external_create_leaderboard_list_func_t create_leaderboard_list; - rc_client_external_get_int_func_t has_leaderboards; - rc_client_external_get_leaderboard_info_func_t get_leaderboard_info; - rc_client_external_begin_fetch_leaderboard_entries_func_t begin_fetch_leaderboard_entries; - rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t begin_fetch_leaderboard_entries_around_user; - - rc_client_external_copy_string_func_t get_rich_presence_message; - rc_client_external_get_int_func_t has_rich_presence; - - rc_client_external_action_func_t do_frame; - rc_client_external_action_func_t idle; - rc_client_external_get_int_func_t is_processing_required; - rc_client_external_can_pause_func_t can_pause; - rc_client_external_action_func_t reset; - - rc_client_external_progress_size_func_t progress_size; - rc_client_external_serialize_progress_func_t serialize_progress; - rc_client_external_deserialize_progress_func_t deserialize_progress; - - /* VERSION 2 */ - rc_client_external_add_game_hash_func_t add_game_hash; - rc_client_external_set_string_func_t load_unknown_game; - - /* VERSION 3 */ - rc_client_external_get_user_info_func_t get_user_info_v3; - rc_client_external_get_game_info_func_t get_game_info_v3; - rc_client_external_get_subset_info_func_t get_subset_info_v3; - rc_client_external_get_achievement_info_func_t get_achievement_info_v3; - rc_client_external_create_achievement_list_func_t create_achievement_list_v3; - - /* VERSION 4 */ - rc_client_external_set_int_func_t set_allow_background_memory_reads; - - /* VERSION 5 */ - rc_client_external_get_user_game_summary_func_t get_user_game_summary_v5; - rc_client_external_get_user_subset_summary_func_t get_user_subset_summary; - - /* VERSION 6 */ - rc_client_external_create_subset_list_func_t create_subset_list; - -} rc_client_external_t; - -#define RC_CLIENT_EXTERNAL_VERSION 5 - -void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); -void rc_client_load_unknown_game(rc_client_t* client, const char* hash); - -/* conversion support */ - -struct rc_client_external_conversions_t; -const rc_client_user_t* rc_client_external_convert_v1_user(const rc_client_t* client, const rc_client_user_t* v1_user); -const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* client, const rc_client_game_t* v1_game); -const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset); -const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement); -rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list); - -RC_END_C_DECLS - -#endif /* RC_CLIENT_EXTERNAL_H */ diff --git a/src/rcheevos/src/rc_client_external_versions.h b/src/rcheevos/src/rc_client_external_versions.h deleted file mode 100644 index 90f6f68960..0000000000 --- a/src/rcheevos/src/rc_client_external_versions.h +++ /dev/null @@ -1,171 +0,0 @@ -#ifndef RC_CLIENT_EXTERNAL_CONVERSIONS_H -#define RC_CLIENT_EXTERNAL_CONVERSIONS_H - -#include "rc_client_internal.h" - -RC_BEGIN_C_DECLS - -/* user */ - -typedef struct v1_rc_client_user_t { - const char* display_name; - const char* username; - const char* token; - uint32_t score; - uint32_t score_softcore; - uint32_t num_unread_messages; -} v1_rc_client_user_t; - -typedef struct v3_rc_client_user_t { - const char* display_name; - const char* username; - const char* token; - uint32_t score; - uint32_t score_softcore; - uint32_t num_unread_messages; - const char* avatar_url; -} v3_rc_client_user_t; - -/* game */ - -typedef struct v1_rc_client_game_t { - uint32_t id; - uint32_t console_id; - const char* title; - const char* hash; - const char* badge_name; -} v1_rc_client_game_t; - -typedef struct v3_rc_client_game_t { - uint32_t id; - uint32_t console_id; - const char* title; - const char* hash; - const char* badge_name; - const char* badge_url; -} v3_rc_client_game_t; - -/* subset */ - -typedef struct v1_rc_client_subset_t { - uint32_t id; - const char* title; - char badge_name[16]; - uint32_t num_achievements; - uint32_t num_leaderboards; -} v1_rc_client_subset_t; - -typedef struct v3_rc_client_subset_t { - uint32_t id; - const char* title; - char badge_name[16]; - uint32_t num_achievements; - uint32_t num_leaderboards; - const char* badge_url; -} v3_rc_client_subset_t; - -/* achievement */ - -typedef struct v1_rc_client_achievement_t { - const char* title; - const char* description; - char badge_name[8]; - char measured_progress[24]; - float measured_percent; - uint32_t id; - uint32_t points; - time_t unlock_time; - uint8_t state; - uint8_t category; - uint8_t bucket; - uint8_t unlocked; - float rarity; - float rarity_hardcore; - uint8_t type; -} v1_rc_client_achievement_t; - -typedef struct v3_rc_client_achievement_t { - const char* title; - const char* description; - char badge_name[8]; - char measured_progress[24]; - float measured_percent; - uint32_t id; - uint32_t points; - time_t unlock_time; - uint8_t state; - uint8_t category; - uint8_t bucket; - uint8_t unlocked; - float rarity; - float rarity_hardcore; - uint8_t type; - const char* badge_url; - const char* badge_locked_url; -} v3_rc_client_achievement_t; - -/* achievement list */ - -typedef struct v1_rc_client_achievement_bucket_t { - v1_rc_client_achievement_t** achievements; - uint32_t num_achievements; - - const char* label; - uint32_t subset_id; - uint8_t bucket_type; -} v1_rc_client_achievement_bucket_t; - -typedef struct v1_rc_client_achievement_list_t { - v1_rc_client_achievement_bucket_t* buckets; - uint32_t num_buckets; -} v1_rc_client_achievement_list_t; - -typedef struct v1_rc_client_achievement_list_info_t { - v1_rc_client_achievement_list_t public_; - rc_client_destroy_achievement_list_func_t destroy_func; -} v1_rc_client_achievement_list_info_t; - -typedef struct v3_rc_client_achievement_bucket_t { - const v3_rc_client_achievement_t** achievements; - uint32_t num_achievements; - - const char* label; - uint32_t subset_id; - uint8_t bucket_type; -} v3_rc_client_achievement_bucket_t; - -typedef struct v3_rc_client_achievement_list_t { - const v3_rc_client_achievement_bucket_t* buckets; - uint32_t num_buckets; -} v3_rc_client_achievement_list_t; - -typedef struct v3_rc_client_achievement_list_info_t { - v3_rc_client_achievement_list_t public_; - rc_client_destroy_achievement_list_func_t destroy_func; -} v3_rc_client_achievement_list_info_t; - -/* user_game_summary */ - -typedef struct v1_rc_client_user_game_summary_t { - uint32_t num_core_achievements; - uint32_t num_unofficial_achievements; - uint32_t num_unlocked_achievements; - uint32_t num_unsupported_achievements; - uint32_t points_core; - uint32_t points_unlocked; -} v1_rc_client_user_game_summary_t; - -typedef struct v5_rc_client_user_game_summary_t { - uint32_t num_core_achievements; - uint32_t num_unofficial_achievements; - uint32_t num_unlocked_achievements; - uint32_t num_unsupported_achievements; - uint32_t points_core; - uint32_t points_unlocked; - time_t beaten_time; - time_t completed_time; -} v5_rc_client_user_game_summary_t; - -RC_END_C_DECLS - -#endif /* RC_CLIENT_EXTERNAL_CONVERSIONS_H */ diff --git a/src/rcheevos/src/rc_client_raintegration.c b/src/rcheevos/src/rc_client_raintegration.c deleted file mode 100644 index e89d3de168..0000000000 --- a/src/rcheevos/src/rc_client_raintegration.c +++ /dev/null @@ -1,566 +0,0 @@ -#include "rc_client_raintegration_internal.h" - -#include "rc_client_internal.h" - -#include "rapi/rc_api_common.h" - -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - -/* ===== natvis extensions ===== */ - -typedef struct __rc_client_raintegration_event_enum_t { uint8_t value; } __rc_client_raintegration_event_enum_t; -static void rc_client_raintegration_natvis_helper(void) -{ - struct natvis_extensions { - __rc_client_raintegration_event_enum_t raintegration_event_type; - } natvis; - - natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE; - natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED; - natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED; - natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_PAUSE; - natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED; -} - -/* ============================= */ - -static void rc_client_raintegration_load_dll(rc_client_t* client, - const wchar_t* search_directory, rc_client_callback_t callback, void* callback_userdata) -{ - wchar_t sPath[_MAX_PATH]; - const int nPathSize = sizeof(sPath) / sizeof(sPath[0]); - rc_client_raintegration_t* raintegration; - int sPathIndex = 0; - DWORD dwAttrib; - HINSTANCE hDLL; - - if (search_directory) { - sPathIndex = swprintf_s(sPath, nPathSize, L"%s\\", search_directory); - if (sPathIndex > nPathSize - 22) { - callback(RC_INVALID_STATE, "search_directory too long", client, callback_userdata); - return; - } - } - -#if defined(_M_X64) || defined(__amd64__) - wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration-x64.dll"); - dwAttrib = GetFileAttributesW(sPath); - if (dwAttrib == INVALID_FILE_ATTRIBUTES) { - wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); - dwAttrib = GetFileAttributesW(sPath); - } -#else - wcscpy_s(&sPath[sPathIndex], nPathSize - sPathIndex, L"RA_Integration.dll"); - dwAttrib = GetFileAttributesW(sPath); -#endif - - if (dwAttrib == INVALID_FILE_ATTRIBUTES) { - callback(RC_MISSING_VALUE, "RA_Integration.dll not found in search directory", client, callback_userdata); - return; - } - - hDLL = LoadLibraryW(sPath); - if (hDLL == NULL) { - char error_message[512]; - const DWORD last_error = GetLastError(); - int offset = snprintf(error_message, sizeof(error_message), "Failed to load RA_Integration.dll (%u)", last_error); - - if (last_error != 0) { - LPSTR messageBuffer = NULL; - const DWORD size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); - - snprintf(&error_message[offset], sizeof(error_message) - offset, ": %.*s", size, messageBuffer); - - LocalFree(messageBuffer); - } - - callback(RC_ABORTED, error_message, client, callback_userdata); - return; - } - - raintegration = (rc_client_raintegration_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_raintegration_t)); - memset(raintegration, 0, sizeof(*raintegration)); - raintegration->hDLL = hDLL; - - raintegration->get_version = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_IntegrationVersion"); - raintegration->get_host_url = (rc_client_raintegration_get_string_func_t)GetProcAddress(hDLL, "_RA_HostUrl"); - raintegration->init_client = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitClient"); - raintegration->init_client_offline = (rc_client_raintegration_init_client_func_t)GetProcAddress(hDLL, "_RA_InitOffline"); - raintegration->set_console_id = (rc_client_raintegration_set_int_func_t)GetProcAddress(hDLL, "_RA_SetConsoleID"); - raintegration->shutdown = (rc_client_raintegration_action_func_t)GetProcAddress(hDLL, "_RA_Shutdown"); - - raintegration->update_main_window_handle = (rc_client_raintegration_hwnd_action_func_t)GetProcAddress(hDLL, "_RA_UpdateHWnd"); - - raintegration->get_external_client = (rc_client_raintegration_get_external_client_func_t)GetProcAddress(hDLL, "_Rcheevos_GetExternalClient"); - raintegration->get_menu = (rc_client_raintegration_get_menu_func_t)GetProcAddress(hDLL, "_Rcheevos_RAIntegrationGetMenu"); - raintegration->activate_menu_item = (rc_client_raintegration_activate_menuitem_func_t)GetProcAddress(hDLL, "_Rcheevos_ActivateRAIntegrationMenuItem"); - raintegration->set_write_memory_function = (rc_client_raintegration_set_write_memory_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationWriteMemoryFunction"); - raintegration->set_get_game_name_function = (rc_client_raintegration_set_get_game_name_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationGetGameNameFunction"); - raintegration->set_event_handler = (rc_client_raintegration_set_event_handler_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationEventHandler"); - raintegration->has_modifications = (rc_client_raintegration_get_int_func_t)GetProcAddress(hDLL, "_Rcheevos_HasModifications"); - raintegration->get_achievement_state = (rc_client_raintegration_get_achievement_state_func_t)GetProcAddress(hDLL, "_Rcheevos_GetAchievementState"); - - if (!raintegration->get_version || - !raintegration->init_client || - !raintegration->get_external_client) { - FreeLibrary(hDLL); - - callback(RC_ABORTED, "One or more required exports was not found in RA_Integration.dll", client, callback_userdata); - - /* dummy reference to natvis helper to ensure extensions get compiled in. */ - raintegration->shutdown = rc_client_raintegration_natvis_helper; - } - else { - rc_mutex_lock(&client->state.mutex); - client->state.raintegration = raintegration; - rc_mutex_unlock(&client->state.mutex); - - RC_CLIENT_LOG_INFO_FORMATTED(client, "RA_Integration.dll %s loaded", client->state.raintegration->get_version()); - } -} - -typedef struct rc_client_version_validation_callback_data_t { - rc_client_t* client; - rc_client_callback_t callback; - void* callback_userdata; - HWND main_window_handle; - char* client_name; - char* client_version; - rc_client_async_handle_t async_handle; -} rc_client_version_validation_callback_data_t; - -int rc_client_version_less(const char* left, const char* right) -{ - do { - int left_len = 0; - int right_len = 0; - while (*left && *left == '0') - ++left; - while (left[left_len] && left[left_len] != '.') - ++left_len; - while (*right && *right == '0') - ++right; - while (right[right_len] && right[right_len] != '.') - ++right_len; - - if (left_len != right_len) - return (left_len < right_len); - - while (left_len--) { - if (*left != *right) - return (*left < *right); - ++left; - ++right; - } - - if (*left == '.') - ++left; - if (*right == '.') - ++right; - } while (*left || *right); - - return 0; -} - -static void rc_client_init_raintegration(rc_client_t* client, - rc_client_version_validation_callback_data_t* version_validation_callback_data) -{ - rc_client_raintegration_init_client_func_t init_func = client->state.raintegration->init_client; - - if (client->state.raintegration->get_host_url) { - const char* host_url = client->state.raintegration->get_host_url(); - if (host_url) { - if (strcmp(host_url, "OFFLINE") != 0) { - if (strcmp(host_url, rc_api_default_host()) != 0) - rc_client_set_host(client, host_url); - } - else if (client->state.raintegration->init_client_offline) { - init_func = client->state.raintegration->init_client_offline; - RC_CLIENT_LOG_INFO(client, "Initializing in offline mode"); - } - } - } - - if (!init_func || !init_func(version_validation_callback_data->main_window_handle, - version_validation_callback_data->client_name, - version_validation_callback_data->client_version)) { - const char* error_message = "RA_Integration initialization failed"; - - rc_client_unload_raintegration(client); - - RC_CLIENT_LOG_ERR(client, error_message); - version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); - } - else { - rc_client_external_t* external_client = (rc_client_external_t*) - rc_buffer_alloc(&client->state.buffer, sizeof(*external_client)); - memset(external_client, 0, sizeof(*external_client)); - - if (!client->state.raintegration->get_external_client(external_client, RC_CLIENT_EXTERNAL_VERSION)) { - const char* error_message = "RA_Integration external client export failed"; - - rc_client_unload_raintegration(client); - - RC_CLIENT_LOG_ERR(client, error_message); - version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); - } - else { - /* copy state to the external client */ - if (external_client->enable_logging) - external_client->enable_logging(client, client->state.log_level, client->callbacks.log_call); - - if (external_client->set_event_handler) - external_client->set_event_handler(client, client->callbacks.event_handler); - if (external_client->set_read_memory) - external_client->set_read_memory(client, client->callbacks.read_memory); - - if (external_client->set_hardcore_enabled) - external_client->set_hardcore_enabled(rc_client_get_hardcore_enabled(client)); - if (external_client->set_unofficial_enabled) - external_client->set_unofficial_enabled(rc_client_get_unofficial_enabled(client)); - if (external_client->set_encore_mode_enabled) - external_client->set_encore_mode_enabled(rc_client_get_encore_mode_enabled(client)); - if (external_client->set_spectator_mode_enabled) - external_client->set_spectator_mode_enabled(rc_client_get_spectator_mode_enabled(client)); - if (external_client->set_allow_background_memory_reads) - external_client->set_allow_background_memory_reads(client->state.allow_background_memory_reads); - - /* attach the external client and call the callback */ - client->state.external_client = external_client; - - client->state.raintegration->hMainWindow = version_validation_callback_data->main_window_handle; - client->state.raintegration->bIsInited = 1; - - version_validation_callback_data->callback(RC_OK, NULL, - client, version_validation_callback_data->callback_userdata); - } - } -} - -static void rc_client_version_validation_callback(const rc_api_server_response_t* server_response, void* callback_data) -{ - rc_client_version_validation_callback_data_t* version_validation_callback_data = - (rc_client_version_validation_callback_data_t*)callback_data; - rc_client_t* client = version_validation_callback_data->client; - - if (rc_client_async_handle_aborted(client, &version_validation_callback_data->async_handle)) { - RC_CLIENT_LOG_VERBOSE(client, "Version validation aborted"); - } - else { - rc_api_response_t response; - int result; - const char* current_version; - const char* minimum_version = ""; - - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("MinimumVersion"), - }; - - memset(&response, 0, sizeof(response)); - rc_buffer_init(&response.buffer); - - result = rc_json_parse_server_response(&response, server_response, fields, sizeof(fields) / sizeof(fields[0])); - if (result == RC_OK) { - if (!rc_json_get_required_string(&minimum_version, &response, &fields[2], "MinimumVersion")) - result = RC_MISSING_VALUE; - } - - if (result != RC_OK) { - RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to fetch latest integration version: %.*s", server_response->body_length, server_response->body); - - rc_client_unload_raintegration(client); - - version_validation_callback_data->callback(result, rc_error_str(result), - client, version_validation_callback_data->callback_userdata); - } - else { - current_version = client->state.raintegration->get_version(); - - if (rc_client_version_less(current_version, minimum_version)) { - char error_message[256]; - - rc_client_unload_raintegration(client); - - snprintf(error_message, sizeof(error_message), - "RA_Integration version %s is lower than minimum version %s", current_version, minimum_version); - RC_CLIENT_LOG_WARN(client, error_message); - version_validation_callback_data->callback(RC_ABORTED, error_message, client, version_validation_callback_data->callback_userdata); - } - else { - RC_CLIENT_LOG_INFO_FORMATTED(client, "Validated RA_Integration version %s (minimum %s)", current_version, minimum_version); - - rc_client_init_raintegration(client, version_validation_callback_data); - } - } - - rc_buffer_destroy(&response.buffer); - } - - free(version_validation_callback_data->client_name); - free(version_validation_callback_data->client_version); - free(version_validation_callback_data); -} - -rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client, - const wchar_t* search_directory, HWND main_window_handle, - const char* client_name, const char* client_version, - rc_client_callback_t callback, void* callback_userdata) -{ - rc_client_version_validation_callback_data_t* callback_data; - rc_api_url_builder_t builder; - rc_api_request_t request; - - if (!client) { - callback(RC_INVALID_STATE, "client is required", client, callback_userdata); - return NULL; - } - - if (!client_name) { - callback(RC_INVALID_STATE, "client_name is required", client, callback_userdata); - return NULL; - } - - if (!client_version) { - callback(RC_INVALID_STATE, "client_version is required", client, callback_userdata); - return NULL; - } - - if (client->state.user != RC_CLIENT_USER_STATE_NONE) { - callback(RC_INVALID_STATE, "Cannot initialize RAIntegration after login", client, callback_userdata); - return NULL; - } - - if (!client->state.raintegration) { - if (!main_window_handle) { - callback(RC_INVALID_STATE, "main_window_handle is required", client, callback_userdata); - return NULL; - } - - rc_client_raintegration_load_dll(client, search_directory, callback, callback_userdata); - if (!client->state.raintegration) - return NULL; - } - - if (client->state.raintegration->get_host_url) { - const char* host_url = client->state.raintegration->get_host_url(); - if (host_url && strcmp(host_url, rc_api_default_host()) != 0 && - strcmp(host_url, "OFFLINE") != 0) { - /* if the DLL specifies a custom host, use it */ - rc_client_set_host(client, host_url); - } - } - - memset(&request, 0, sizeof(request)); - rc_api_url_build_dorequest_url(&request, &client->state.host); - rc_url_builder_init(&builder, &request.buffer, 48); - rc_url_builder_append_str_param(&builder, "r", "latestintegration"); - request.post_data = rc_url_builder_finalize(&builder); - - callback_data = calloc(1, sizeof(*callback_data)); - if (!callback_data) { - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - - callback_data->client = client; - callback_data->callback = callback; - callback_data->callback_userdata = callback_userdata; - callback_data->client_name = strdup(client_name); - callback_data->client_version = strdup(client_version); - callback_data->main_window_handle = main_window_handle; - - client->callbacks.server_call(&request, rc_client_version_validation_callback, callback_data, client); - return &callback_data->async_handle; -} - -void rc_client_raintegration_update_main_window_handle(rc_client_t* client, HWND main_window_handle) -{ - if (client && client->state.raintegration) { - client->state.raintegration->hMainWindow = main_window_handle; - - if (client->state.raintegration->bIsInited && - client->state.raintegration->update_main_window_handle) { - client->state.raintegration->update_main_window_handle(main_window_handle); - } - } -} - -void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler) -{ - if (client && client->state.raintegration && client->state.raintegration->set_write_memory_function) - client->state.raintegration->set_write_memory_function(client, handler); -} - -void rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler) -{ - if (client && client->state.raintegration && client->state.raintegration->set_get_game_name_function) - client->state.raintegration->set_get_game_name_function(client, handler); -} - -void rc_client_raintegration_set_event_handler(rc_client_t* client, - rc_client_raintegration_event_handler_t handler) -{ - if (client && client->state.raintegration && client->state.raintegration->set_event_handler) - client->state.raintegration->set_event_handler(client, handler); -} - -const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_client_t* client) -{ - if (!client || !client->state.raintegration || - !client->state.raintegration->bIsInited || - !client->state.raintegration->get_menu) { - return NULL; - } - - return client->state.raintegration->get_menu(); -} - -void rc_client_raintegration_set_console_id(rc_client_t* client, uint32_t console_id) -{ - if (client && client->state.raintegration && client->state.raintegration->set_console_id) - client->state.raintegration->set_console_id(console_id); -} - -int rc_client_raintegration_has_modifications(const rc_client_t* client) -{ - if (!client || !client->state.raintegration || - !client->state.raintegration->bIsInited || - !client->state.raintegration->has_modifications) { - return 0; - } - - return client->state.raintegration->has_modifications(); -} - -int rc_client_raintegration_get_achievement_state(const rc_client_t* client, uint32_t achievement_id) -{ - if (!client || !client->state.raintegration || - !client->state.raintegration->bIsInited || - !client->state.raintegration->get_achievement_state) { - return RC_CLIENT_RAINTEGRATION_ACHIEVEMENT_STATE_NONE; - } - - return client->state.raintegration->get_achievement_state(achievement_id); -} - -void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu) -{ - HMENU hPopupMenu = NULL; - const rc_client_raintegration_menu_t* menu; - - if (!client || !client->state.raintegration) - return; - - /* destroy the existing menu */ - if (client->state.raintegration->hPopupMenu) - DestroyMenu(client->state.raintegration->hPopupMenu); - - /* create the popup menu */ - hPopupMenu = CreatePopupMenu(); - - menu = rc_client_raintegration_get_menu(client); - if (menu && menu->num_items) - { - const rc_client_raintegration_menu_item_t* menuitem = menu->items; - const rc_client_raintegration_menu_item_t* stop = menu->items + menu->num_items; - - for (; menuitem < stop; ++menuitem) - { - if (menuitem->id == 0) - AppendMenuA(hPopupMenu, MF_SEPARATOR, 0U, NULL); - else - { - UINT flags = MF_STRING; - if (menuitem->checked) - flags |= MF_CHECKED; - if (!menuitem->enabled) - flags |= MF_GRAYED; - - AppendMenuA(hPopupMenu, flags, menuitem->id, menuitem->label); - } - } - } - - /* add/update the item containing the popup menu */ - { - int nIndex = GetMenuItemCount(hMenu); - const char* menuText = "&RetroAchievements"; - char buffer[64]; - - UINT flags = MF_POPUP | MF_STRING; - if (!menu || !menu->num_items) - flags |= MF_GRAYED; - - while (--nIndex >= 0) - { - if (GetMenuStringA(hMenu, nIndex, buffer, sizeof(buffer) - 1, MF_BYPOSITION)) - { - if (strcmp(buffer, menuText) == 0) - break; - } - } - - if (nIndex == -1) - AppendMenuA(hMenu, flags, (UINT_PTR)hPopupMenu, menuText); - else - ModifyMenuA(hMenu, nIndex, flags | MF_BYPOSITION, (UINT_PTR)hPopupMenu, menuText); - - if (client->state.raintegration->hMainWindow && GetMenu(client->state.raintegration->hMainWindow) == hMenu) - DrawMenuBar(client->state.raintegration->hMainWindow); - } - - client->state.raintegration->hPopupMenu = hPopupMenu; -} - -void rc_client_raintegration_update_menu_item(const rc_client_t* client, const rc_client_raintegration_menu_item_t* menuitem) -{ - if (client && client->state.raintegration && client->state.raintegration->hPopupMenu) - { - UINT flags = MF_STRING; - if (menuitem->checked) - flags |= MF_CHECKED; - - CheckMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); - - flags = (menuitem->enabled) ? MF_ENABLED : MF_GRAYED; - EnableMenuItem(client->state.raintegration->hPopupMenu, menuitem->id, flags | MF_BYCOMMAND); - } -} - -int rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t menu_item_id) -{ - if (!client || !client->state.raintegration || !client->state.raintegration->activate_menu_item) - return 0; - - return client->state.raintegration->activate_menu_item(menu_item_id); -} - -void rc_client_unload_raintegration(rc_client_t* client) -{ - HINSTANCE hDLL; - - if (!client || !client->state.raintegration) - return; - - RC_CLIENT_LOG_INFO(client, "Unloading RA_Integration") - - if (client->state.external_client && client->state.external_client->destroy) - client->state.external_client->destroy(); - - if (client->state.raintegration->shutdown) - client->state.raintegration->shutdown(); - - rc_mutex_lock(&client->state.mutex); - hDLL = client->state.raintegration->hDLL; - client->state.raintegration = NULL; - client->state.external_client = NULL; - rc_mutex_unlock(&client->state.mutex); - - if (hDLL) - FreeLibrary(hDLL); -} - -#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ diff --git a/src/rcheevos/src/rc_client_raintegration_internal.h b/src/rcheevos/src/rc_client_raintegration_internal.h deleted file mode 100644 index ce3a99dda5..0000000000 --- a/src/rcheevos/src/rc_client_raintegration_internal.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef RC_CLIENT_RAINTEGRATION_INTERNAL_H -#define RC_CLIENT_RAINTEGRATION_INTERNAL_H - -#include "rc_client_raintegration.h" - -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - -#include "rc_client_external.h" -#include "rc_compat.h" - -RC_BEGIN_C_DECLS - -/* RAIntegration follows the same calling convention as rcheevos */ - -typedef void (RC_CCONV* rc_client_raintegration_action_func_t)(void); -typedef const char* (RC_CCONV* rc_client_raintegration_get_string_func_t)(void); -typedef int (RC_CCONV* rc_client_raintegration_init_client_func_t)(HWND hMainWnd, const char* sClientName, const char* sClientVersion); -typedef int (RC_CCONV* rc_client_raintegration_get_external_client_func_t)(rc_client_external_t* pClient, int nVersion); -typedef void (RC_CCONV* rc_client_raintegration_hwnd_action_func_t)(HWND hWnd); -typedef int (RC_CCONV* rc_client_raintegration_get_achievement_state_func_t)(uint32_t nMenuItemId); -typedef const rc_client_raintegration_menu_t* (RC_CCONV* rc_client_raintegration_get_menu_func_t)(void); -typedef int (RC_CCONV* rc_client_raintegration_activate_menuitem_func_t)(uint32_t nMenuItemId); -typedef void (RC_CCONV* rc_client_raintegration_set_write_memory_func_t)(rc_client_t* pClient, rc_client_raintegration_write_memory_func_t handler); -typedef void (RC_CCONV* rc_client_raintegration_set_get_game_name_func_t)(rc_client_t* pClient, rc_client_raintegration_get_game_name_func_t handler); -typedef void (RC_CCONV* rc_client_raintegration_set_event_handler_func_t)(rc_client_t* pClient, rc_client_raintegration_event_handler_t handler); -typedef void (RC_CCONV* rc_client_raintegration_set_int_func_t)(int); -typedef int (RC_CCONV* rc_client_raintegration_get_int_func_t)(void); - -typedef struct rc_client_raintegration_t -{ - HINSTANCE hDLL; - HWND hMainWindow; - HMENU hPopupMenu; - uint8_t bIsInited; - - rc_client_raintegration_get_string_func_t get_version; - rc_client_raintegration_get_string_func_t get_host_url; - rc_client_raintegration_init_client_func_t init_client; - rc_client_raintegration_init_client_func_t init_client_offline; - rc_client_raintegration_set_int_func_t set_console_id; - rc_client_raintegration_action_func_t shutdown; - - rc_client_raintegration_hwnd_action_func_t update_main_window_handle; - - rc_client_raintegration_set_write_memory_func_t set_write_memory_function; - rc_client_raintegration_set_get_game_name_func_t set_get_game_name_function; - rc_client_raintegration_set_event_handler_func_t set_event_handler; - rc_client_raintegration_get_menu_func_t get_menu; - rc_client_raintegration_activate_menuitem_func_t activate_menu_item; - rc_client_raintegration_get_int_func_t has_modifications; - rc_client_raintegration_get_achievement_state_func_t get_achievement_state; - - rc_client_raintegration_get_external_client_func_t get_external_client; - -} rc_client_raintegration_t; - -RC_END_C_DECLS - -#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ - -#endif /* RC_CLIENT_RAINTEGRATION_INTERNAL_H */ diff --git a/src/rcheevos/src/rc_client_types.natvis b/src/rcheevos/src/rc_client_types.natvis deleted file mode 100644 index 397d400c35..0000000000 --- a/src/rcheevos/src/rc_client_types.natvis +++ /dev/null @@ -1,396 +0,0 @@ - - - - - - {{display_name={display_name,s} score={score}}} - - - {{title={title,s} id={id}}} - - - {{title={title,s} id={id}}} - - - {RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE} - {RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE} - {RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED} - {RC_CLIENT_ACHIEVEMENT_STATE_DISABLED} - unknown ({value}) - - - {RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE} - {RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE} - {RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL} - {RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL} - unknown ({value}) - - - {RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD} - {RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE} - {RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION} - {RC_CLIENT_ACHIEVEMENT_TYPE_WIN} - unknown ({value}) - - - {RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN} - {RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED} - {RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED} - {RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED} - {RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL} - {RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED} - {RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE} - {RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE} - {RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED} - unknown ({value}) - - - {RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE} - {RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE} - {RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE} - {RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH} - unknown ({value}) - - - {{title={title,s} id={id}}} - - title - description - points - id - *((__rc_client_achievement_state_enum_t*)&state) - *((__rc_client_achievement_type_enum_t*)&state) - *((__rc_client_achievement_category_enum_t*)&category) - *((__rc_client_achievement_state_enum_t*)&bucket) - *((__rc_client_achievement_unlocked_enum_t*)&unlocked) - - - - {{label={label,s} count={num_achievements}}} - - - num_achievements - achievements[$i] - - - - - {{count={num_buckets}}} - - - num_buckets - buckets[$i] - - - - - {RC_CLIENT_LEADERBOARD_STATE_INACTIVE} - {RC_CLIENT_LEADERBOARD_STATE_ACTIVE} - {RC_CLIENT_LEADERBOARD_STATE_TRACKING} - {RC_CLIENT_LEADERBOARD_STATE_DISABLED} - unknown ({value}) - - - {RC_CLIENT_LEADERBOARD_FORMAT_TIME} - {RC_CLIENT_LEADERBOARD_FORMAT_SCORE} - {RC_CLIENT_LEADERBOARD_FORMAT_VALUE} - unknown ({value}) - - - {{title={title,s} id={id}}} - - title - description - tracker_value - id - *((__rc_client_leaderboard_state_enum_t*)&state) - *((__rc_client_leaderboard_format_enum_t*)&format) - *((__rc_bool_enum_t*)&lower_is_better) - - - - {{label={label,s} count={num_leaderboards}}} - - - num_leaderboards - leaderboards[$i] - - - - - {{count={num_buckets}}} - - - num_buckets - buckets[$i] - - - - - {{rank={rank} score={score,s} username={username}}} - - - {{leaderboard_id={leaderboard_id} num_entries={num_entries}}} - - leaderboard_id - submitted_score - best_score - new_rank - num_entries - - num_top_entries - top_entries[$i] - - - - - {RC_CLIENT_EVENT_TYPE_NONE} - {RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED} - {RC_CLIENT_EVENT_LEADERBOARD_STARTED} - {RC_CLIENT_EVENT_LEADERBOARD_FAILED} - {RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED} - {RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW} - {RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE} - {RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW} - {RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE} - {RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE} - {RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW} - {RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE} - {RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE} - {RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD} - {RC_CLIENT_EVENT_RESET} - {RC_CLIENT_EVENT_GAME_COMPLETED} - {RC_CLIENT_EVENT_SERVER_ERROR} - {RC_CLIENT_EVENT_DISCONNECTED} - {RC_CLIENT_EVENT_RECONNECTED} - {RC_CLIENT_EVENT_SUBSET_COMPLETED} - unknown ({value}) - - - {{type={*((__rc_client_event_type_enum_t*)&type)}}} - - *((__rc_client_event_type_enum_t*)&type) - *achievement - *leaderboard - *leaderboard - *leaderboard - *achievement - *achievement - *achievement - *achievement - *achievement - *leaderboard_tracker - *leaderboard_tracker - *leaderboard_tracker - *leaderboard_scoreboard - *leaderboard - *server_error - *subset - - - - {{count={info.public_.num_achievements}}} - - - info.public_.num_achievements - info.achievements[$i] - - - - - {{count={info.public_.num_leaderboards}}} - - - info.public_.num_leaderboards - info.leaderboards[$i] - - - - - {RC_CLIENT_MASTERY_STATE_NONE} - {RC_CLIENT_MASTERY_STATE_PENDING} - {RC_CLIENT_MASTERY_STATE_SHOWN} - unknown ({value}) - - - {{title={public_.title,s} id={public_.id}}} - - public_ - *((__rc_bool_enum_t*)&active) - *((__rc_client_mastery_state_enum_t*)&mastery) - *((__rc_client_subset_info_achievements_list_t*)this) - *((__rc_client_subset_info_leaderboards_list_t*)this) - - - - {{NULL}} - {(void**)&first,na} - - - first - next - this - - - - - {{NULL}} - {(void**)&first,na} - - - first - next - this - - - - - {{NULL}} - {(void**)&first,na} - - - first - next - this - - - - - {{title={public_.title,s} id={public_.id}}} - - - {{title={public_.title,s} id={public_.id}}} - - - {{title={public_.title,s} id={public_.id}}} - - public_ - *((__rc_client_subset_info_list_t*)&subsets) - *((__rc_client_media_hash_list_t*)&media_hash) - *((__rc_client_leaderboard_tracker_list_t*)&leaderboard_trackers) - progress_tracker - runtime - - - - {{hash={hash,s} game_id={game_id}}} - - - {client.hashes} - - - client.hashes - next - *this - - - - - {RC_CLIENT_LOAD_GAME_STATE_NONE} - {RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME} - {RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN} - {RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION} - {RC_CLIENT_LOAD_GAME_STATE_DONE} - {RC_CLIENT_LOAD_GAME_STATE_ABORTED} - unknown ({value}) - - - - *((__rc_client_load_game_state_enum_t*)&progress) - *game - subset - *hash - pending_media - start_session_response - (int)outstanding_requests - - - - {{when={when} callback={callback,na}}} - - - {state.scheduled_callbacks} - - - state.scheduled_callbacks - next - *this - - - - - {RC_CLIENT_LOG_LEVEL_NONE} - {RC_CLIENT_LOG_LEVEL_ERROR} - {RC_CLIENT_LOG_LEVEL_WARN} - {RC_CLIENT_LOG_LEVEL_INFO} - {RC_CLIENT_LOG_LEVEL_VERBOSE} - unknown ({value}) - - - {RC_CLIENT_USER_STATE_NONE} - {RC_CLIENT_USER_STATE_LOGIN_REQUESTED} - {RC_CLIENT_USER_STATE_LOGGED_IN} - unknown ({value}) - - - {RC_CLIENT_SPECTATOR_MODE_OFF} - {RC_CLIENT_SPECTATOR_MODE_ON} - {RC_CLIENT_SPECTATOR_MODE_LOCKED} - unknown ({value}) - - - {RC_CLIENT_DISCONNECT_HIDDEN} - {RC_CLIENT_DISCONNECT_VISIBLE} - {RC_CLIENT_DISCONNECT_SHOW_PENDING} - {RC_CLIENT_DISCONNECT_HIDE_PENDING} - {RC_CLIENT_DISCONNECT_VISIBLE|RC_CLIENT_DISCONNECT_HIDE_PENDING} - unknown ({value}) - - - - *((__rc_bool_enum_t*)&hardcore) - *((__rc_bool_enum_t*)&unofficial_enabled) - *((__rc_bool_enum_t*)&encore_mode) - *((__rc_client_spectator_mode_enum_t*)&spectator_mode) - *((__rc_client_disconnect_enum_t*)&disconnect) - *((__rc_client_log_level_enum_t*)&log_level) - *((__rc_client_user_state_enum_t*)&user) - *((__rc_client_scheduled_callback_list_t*)this) - host - load - - - - - game - *((__rc_client_game_hash_list_t*)this) - user - callbacks - state - - - - {{count={num_items}}} - - - num_items - items[$i] - - - - - {RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE} - {RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED} - {RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED} - {RC_CLIENT_RAINTEGRATION_EVENT_PAUSE} - {RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED} - unknown ({value}) - - - {{type={*((__rc_client_raintegration_event_enum_t*)&type)}}} - - *((__rc_client_raintegration_event_enum_t*)&type) - menu_item - - - diff --git a/src/rcheevos/src/rc_libretro.c b/src/rcheevos/src/rc_libretro.c deleted file mode 100644 index e04c8d2f4f..0000000000 --- a/src/rcheevos/src/rc_libretro.c +++ /dev/null @@ -1,915 +0,0 @@ -/* This file provides a series of functions for integrating RetroAchievements with libretro. - * These functions will be called by a libretro frontend to validate certain expected behaviors - * and simplify mapping core data to the RAIntegration DLL. - * - * Originally designed to be shared between RALibretro and RetroArch, but will simplify - * integrating with any other frontends. - */ - -#include "rc_libretro.h" - -#include "rc_consoles.h" -#include "rc_compat.h" -#include "rhash/rc_hash_internal.h" - -#include -#include - -static rc_libretro_message_callback rc_libretro_verbose_message_callback = NULL; - -/* a value that starts with a comma is a CSV. - * if it starts with an exclamation point, it's everything but the provided value. - * if it starts with an exclamntion point followed by a comma, it's everything but the CSV values. - * values are case-insensitive */ -typedef struct rc_disallowed_core_settings_t -{ - const char* library_name; - const rc_disallowed_setting_t* disallowed_settings; -} rc_disallowed_core_settings_t; - - -static const rc_disallowed_setting_t _rc_disallowed_beetle_psx_settings[] = { - { "beetle_psx_cpu_freq_scale", "<100" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_beetle_psx_hw_settings[] = { - { "beetle_psx_hw_cpu_freq_scale", "<100" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { - { "bsnes_region", "pal" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_cap32_settings[] = { - { "cap32_autorun", "disabled" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = { - { "dolphin_cheats_enabled", "enabled" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_dosbox_pure_settings[] = { - { "dosbox_pure_strict_mode", "false" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = { - { "duckstation_CDROM.LoadImagePatches", "true" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { - { "ecwolf-invulnerability", "enabled" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { - { "fbneo-allow-patched-romsets", "enabled" }, - { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, - { "fbneo-cpu-speed-adjust", "??%" }, /* disallow speeds under 100% */ - { "fbneo-dipswitch-*", "Universe BIOS*" }, - { "fbneo-neogeo-mode", "UNIBIOS" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { - { "fceumm_game_genie", "!disabled" }, - { "fceumm_region", ",PAL,Dendy" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_flycast_settings[] = { - { "reicast_sh4clock", "<200" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { - { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, - { "genesis_plus_gx_region_detect", "pal" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_gpgx_wide_settings[] = { - { "genesis_plus_gx_wide_lock_on", ",action replay (pro),game genie" }, - { "genesis_plus_gx_wide_region_detect", "pal" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_mesen_settings[] = { - { "mesen_region", ",PAL,Dendy" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_mesen_s_settings[] = { - { "mesen-s_region", "PAL" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_neocd_settings[] = { - { "neocd_bios", "uni-bios*" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { - { "pcsx_rearmed_psxclock", ",!auto,<55" }, - { "pcsx_rearmed_region", "pal" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_picodrive_settings[] = { - { "picodrive_region", ",Europe,Japan PAL" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = { - { "ppsspp_cheats", "enabled" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_quasi88_settings[] = { - { "q88_cpu_clock", ",1,2" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_smsplus_settings[] = { - { "smsplus_region", "pal" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { - { "snes9x_gfx_clip", "disabled" }, - { "snes9x_gfx_transp", "disabled" }, - { "snes9x_layer_*", "disabled" }, - { "snes9x_region", "pal" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_swanstation_settings[] = { - { "swanstation_CPU_Overclock", "<100" }, - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { - { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ - { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ - { NULL, NULL } -}; - -static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = { - { "virtualjaguar_pal", "enabled" }, - { NULL, NULL } -}; - -static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { - { "Beetle PSX", _rc_disallowed_beetle_psx_settings }, - { "Beetle PSX HW", _rc_disallowed_beetle_psx_hw_settings }, - { "bsnes-mercury", _rc_disallowed_bsnes_settings }, - { "cap32", _rc_disallowed_cap32_settings }, - { "dolphin-emu", _rc_disallowed_dolphin_settings }, - { "DOSBox-pure", _rc_disallowed_dosbox_pure_settings }, - { "DuckStation", _rc_disallowed_duckstation_settings }, - { "ecwolf", _rc_disallowed_ecwolf_settings }, - { "FCEUmm", _rc_disallowed_fceumm_settings }, - { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, - { "Flycast", _rc_disallowed_flycast_settings }, - { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, - { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, - { "Mesen", _rc_disallowed_mesen_settings }, - { "Mesen-S", _rc_disallowed_mesen_s_settings }, - { "NeoCD", _rc_disallowed_neocd_settings }, - { "PPSSPP", _rc_disallowed_ppsspp_settings }, - { "PCSX-ReARMed", _rc_disallowed_pcsx_rearmed_settings }, - { "PicoDrive", _rc_disallowed_picodrive_settings }, - { "QUASI88", _rc_disallowed_quasi88_settings }, - { "SMS Plus GX", _rc_disallowed_smsplus_settings }, - { "Snes9x", _rc_disallowed_snes9x_settings }, - { "SwanStation", _rc_disallowed_swanstation_settings }, - { "VICE x64", _rc_disallowed_vice_settings }, - { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, - { NULL, NULL } -}; - -static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* match) { - char c1, c2; - while ((c1 = *test++)) { - if (tolower(c1) != tolower(c2 = *match++) && c2 != '?') - return (c2 == '*'); - } - - return (*match == '\0'); -} - -static int rc_libretro_numeric_less_than(const char* test, const char* value) { - int test_num = atoi(test); - int value_num = atoi(value); - return (test_num < value_num); -} - -static int rc_libretro_match_token(const char* val, const char* token, size_t size, int* result) { - if (*token == '!') { - /* !X => if X is a match, it's explicitly allowed. match with result = false */ - if (rc_libretro_match_token(val, token + 1, size - 1, result)) { - *result = 0; - return 1; - } - } - - if (*token == '<') { - /* if val < token, match with result = true */ - char buffer[128]; - memcpy(buffer, token + 1, size - 1); - buffer[size - 1] = '\0'; - if (rc_libretro_numeric_less_than(val, buffer)) { - *result = 1; - return 1; - } - } - - if (memcmp(token, val, size) == 0 && val[size] == 0) { - /* exact match, match with result = true */ - *result = 1; - return 1; - } - else { - /* check for case insensitive match */ - char buffer[128]; - memcpy(buffer, token, size); - buffer[size] = '\0'; - if (rc_libretro_string_equal_nocase_wildcard(val, buffer)) { - /* case insensitive match, match with result = true */ - *result = 1; - return 1; - } - } - - /* no match */ - return 0; -} - -static int rc_libretro_match_value(const char* val, const char* match) { - int result = 0; - - /* if value starts with a comma, it's a CSV list of potential matches */ - if (*match == ',') { - do { - const char* ptr = ++match; - size_t size; - - while (*match && *match != ',') - ++match; - - size = match - ptr; - if (rc_libretro_match_token(val, ptr, size, &result)) - return result; - - } while (*match == ','); - } - else { - /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ - if (*match == '!') - return !rc_libretro_match_value(val, &match[1]); - - /* just a single value, attempt to match it */ - if (rc_libretro_match_token(val, match, strlen(match), &result)) - return result; - } - - /* value did not match filters, assume it's allowed */ - return 0; -} - -int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { - const char* key; - size_t key_len; - - for (; disallowed_settings->setting; ++disallowed_settings) { - key = disallowed_settings->setting; - key_len = strlen(key); - - if (key[key_len - 1] == '*') { - if (memcmp(setting, key, key_len - 1) == 0) { - if (rc_libretro_match_value(value, disallowed_settings->value)) - return 0; - } - } - else { - if (memcmp(setting, key, key_len + 1) == 0) { - if (rc_libretro_match_value(value, disallowed_settings->value)) - return 0; - } - } - } - - return 1; -} - -const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name) { - const rc_disallowed_core_settings_t* core_filter = rc_disallowed_core_settings; - size_t library_name_length; - - if (!library_name || !library_name[0]) - return NULL; - - library_name_length = strlen(library_name) + 1; - while (core_filter->library_name) { - if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) - return core_filter->disallowed_settings; - - ++core_filter; - } - - return NULL; -} - -typedef struct rc_disallowed_core_systems_t -{ - const char* library_name; - const uint32_t disallowed_consoles[4]; -} rc_disallowed_core_systems_t; - -static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { - /* https://github.com/libretro/Mesen-S/issues/8 */ - { "Mesen-S", { RC_CONSOLE_GAMEBOY, RC_CONSOLE_GAMEBOY_COLOR, 0 }}, - { NULL, { 0 } } -}; - -int rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id) { - const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; - size_t library_name_length; - size_t i; - - if (!library_name || !library_name[0]) - return 1; - - library_name_length = strlen(library_name) + 1; - while (core_filter->library_name) { - if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) { - for (i = 0; i < sizeof(core_filter->disallowed_consoles) / sizeof(core_filter->disallowed_consoles[0]); ++i) { - if (core_filter->disallowed_consoles[i] == console_id) - return 0; - } - break; - } - - ++core_filter; - } - - return 1; -} - -uint8_t* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail) { - uint32_t i; - - for (i = 0; i < regions->count; ++i) { - const size_t size = regions->size[i]; - if (address < size) { - if (regions->data[i] == NULL) - break; - - if (avail) - *avail = (uint32_t)(size - address); - - return ®ions->data[i][address]; - } - - address -= (uint32_t)size; - } - - if (avail) - *avail = 0; - - return NULL; -} - -uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address) { - return rc_libretro_memory_find_avail(regions, address, NULL); -} - -uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, - uint8_t* buffer, uint32_t num_bytes) { - uint32_t bytes_read = 0; - uint32_t avail; - uint32_t i; - - for (i = 0; i < regions->count; ++i) { - const size_t size = regions->size[i]; - if (address >= size) { - /* address is not in this block, adjust and look at next block */ - address -= (unsigned)size; - continue; - } - - if (regions->data[i] == NULL) /* no memory associated to this block. abort */ - break; - - avail = (unsigned)(size - address); - if (avail >= num_bytes) { - /* requested memory is fully within this block, copy and return it */ - memcpy(buffer, ®ions->data[i][address], num_bytes); - bytes_read += num_bytes; - return bytes_read; - } - - /* copy whatever is available in this block, and adjust for the next block */ - memcpy(buffer, ®ions->data[i][address], avail); - buffer += avail; - bytes_read += avail; - num_bytes -= avail; - address = 0; - } - - return bytes_read; -} - -void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { - rc_libretro_verbose_message_callback = callback; -} - -static void rc_libretro_verbose(const char* message) { - if (rc_libretro_verbose_message_callback) - rc_libretro_verbose_message_callback(message); -} - -static const char* rc_memory_type_str(int type) { - switch (type) - { - case RC_MEMORY_TYPE_SAVE_RAM: - return "SRAM"; - case RC_MEMORY_TYPE_VIDEO_RAM: - return "VRAM"; - case RC_MEMORY_TYPE_UNUSED: - return "UNUSED"; - default: - break; - } - - return "SYSTEM RAM"; -} - -static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, - uint8_t* data, size_t size, const char* description) { - if (size == 0) - return; - - if (regions->count == (sizeof(regions->size) / sizeof(regions->size[0]))) { - rc_libretro_verbose("Too many memory memory regions to register"); - return; - } - - if (!data && regions->count > 0 && !regions->data[regions->count - 1]) { - /* extend null region */ - regions->size[regions->count - 1] += size; - } - else if (data && regions->count > 0 && - data == (regions->data[regions->count - 1] + regions->size[regions->count - 1])) { - /* extend non-null region */ - regions->size[regions->count - 1] += size; - } - else { - /* create new region */ - regions->data[regions->count] = data; - regions->size[regions->count] = size; - ++regions->count; - } - - regions->total_size += size; - - if (rc_libretro_verbose_message_callback) { - char message[128]; - snprintf(message, sizeof(message), "Registered 0x%04X bytes of %s at $%06X (%s)", (unsigned)size, - rc_memory_type_str(type), (unsigned)(regions->total_size - size), description); - rc_libretro_verbose_message_callback(message); - } -} - -static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t* regions, - rc_libretro_get_core_memory_info_func get_core_memory_info) { - /* no regions specified, assume system RAM followed by save RAM */ - char description[64]; - rc_libretro_core_memory_info_t info; - - snprintf(description, sizeof(description), "offset 0x%06x", 0); - - get_core_memory_info(RETRO_MEMORY_SYSTEM_RAM, &info); - if (info.size) - rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SYSTEM_RAM, info.data, info.size, description); - - get_core_memory_info(RETRO_MEMORY_SAVE_RAM, &info); - if (info.size) - rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); -} - -static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, uint32_t real_address, size_t* offset) -{ - const struct retro_memory_descriptor* desc = mmap->descriptors; - const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; - - for (; desc < end; desc++) { - if (desc->select == 0) { - /* if select is 0, attempt to explcitly match the address */ - if (real_address >= desc->start && real_address < desc->start + desc->len) { - *offset = real_address - desc->start; - return desc; - } - } - else { - /* otherwise, attempt to match the address by matching the select bits */ - /* address is in the block if (addr & select) == (start & select) */ - if (((desc->start ^ real_address) & desc->select) == 0) { - /* get the relative offset of the address from the start of the memory block */ - uint32_t reduced_address = real_address - (unsigned)desc->start; - - /* remove any bits from the reduced_address that correspond to the bits in the disconnect - * mask and collapse the remaining bits. this code was copied from the mmap_reduce function - * in RetroArch. i'm not exactly sure how it works, but it does. */ - uint32_t disconnect_mask = (unsigned)desc->disconnect; - while (disconnect_mask) { - const uint32_t tmp = (disconnect_mask - 1) & ~disconnect_mask; - reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); - disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; - } - - /* calculate the offset within the descriptor */ - *offset = reduced_address; - - /* sanity check - make sure the descriptor is large enough to hold the target address */ - if (reduced_address < desc->len) - return desc; - } - } - } - - *offset = 0; - return NULL; -} - -static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, - const rc_memory_regions_t* console_regions) { - char description[64]; - uint32_t i; - uint8_t* region_start; - uint8_t* desc_start; - size_t desc_size; - size_t offset; - - for (i = 0; i < console_regions->num_regions; ++i) { - const rc_memory_region_t* console_region = &console_regions->region[i]; - size_t console_region_size = console_region->end_address - console_region->start_address + 1; - uint32_t real_address = console_region->real_address; - uint32_t disconnect_size = 0; - - while (console_region_size > 0) { - const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); - if (!desc) { - if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { - snprintf(description, sizeof(description), "Could not map region starting at $%06X", - (unsigned)(real_address - console_region->real_address + console_region->start_address)); - rc_libretro_verbose(description); - } - - if (disconnect_size && console_region_size > disconnect_size) { - rc_libretro_memory_register_region(regions, console_region->type, NULL, disconnect_size, "null filler"); - console_region_size -= disconnect_size; - real_address += disconnect_size; - disconnect_size = 0; - continue; - } - - rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); - break; - } - - snprintf(description, sizeof(description), "descriptor %u, offset 0x%06X%s", - (unsigned)(desc - mmap->descriptors) + 1, (int)offset, desc->ptr ? "" : " [no pointer]"); - - if (desc->ptr) { - desc_start = (uint8_t*)desc->ptr + desc->offset; - region_start = desc_start + offset; - } - else { - region_start = NULL; - } - - desc_size = desc->len - offset; - if (desc->disconnect && desc_size > desc->disconnect) { - /* if we need to extract a disconnect bit, the largest block we can read is up to - * the next time that bit flips */ - /* https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit */ - disconnect_size = (desc->disconnect & -((int)desc->disconnect)); - desc_size = disconnect_size - (real_address & (disconnect_size - 1)); - } - - if (console_region_size > desc_size) { - if (desc_size == 0) { - if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { - snprintf(description, sizeof(description), "Could not map region starting at $%06X", - (unsigned)(real_address - console_region->real_address + console_region->start_address)); - rc_libretro_verbose(description); - } - - rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); - console_region_size = 0; - } - else { - rc_libretro_memory_register_region(regions, console_region->type, region_start, desc_size, description); - console_region_size -= desc_size; - real_address += (unsigned)desc_size; - } - } - else { - rc_libretro_memory_register_region(regions, console_region->type, region_start, console_region_size, description); - console_region_size = 0; - } - } - } -} - -static uint32_t rc_libretro_memory_console_region_to_ram_type(uint8_t region_type) { - switch (region_type) - { - case RC_MEMORY_TYPE_SAVE_RAM: - return RETRO_MEMORY_SAVE_RAM; - case RC_MEMORY_TYPE_VIDEO_RAM: - return RETRO_MEMORY_VIDEO_RAM; - default: - break; - } - - return RETRO_MEMORY_SYSTEM_RAM; -} - -static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, - rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { - char description[64]; - uint32_t i, j; - rc_libretro_core_memory_info_t info; - size_t offset; - int found_aligning_padding = 0; - - for (i = 0; i < console_regions->num_regions; ++i) { - const rc_memory_region_t* console_region = &console_regions->region[i]; - const size_t console_region_size = console_region->end_address - console_region->start_address + 1; - const uint32_t type = rc_libretro_memory_console_region_to_ram_type(console_region->type); - uint32_t base_address = 0; - - if (console_region->type == RC_MEMORY_TYPE_UNUSED && console_region_size >= 0x10000 && !found_aligning_padding) { - if (console_regions->region[console_regions->num_regions - 1].end_address > 0x01000000) { - /* assume anything exposing more than 16MB of regions with at least one 64KB+ UNUSED region - * is padding so things align with real addresses. this indicates the memory is disjoint - * in the system, so we cannot expect it to be contiguous in the RETRO_SYSTEM_RAM. - * stop processing regions now, and just fill the remaining memory map with null filler. */ - found_aligning_padding = 1; - } - } - - for (j = 0; j <= i; ++j) { - const rc_memory_region_t* console_region2 = &console_regions->region[j]; - if (rc_libretro_memory_console_region_to_ram_type(console_region2->type) == type) { - base_address = console_region2->start_address; - break; - } - } - offset = console_region->start_address - base_address; - - if (!found_aligning_padding) { - get_core_memory_info(type, &info); - } - else { - info.data = NULL; - info.size = console_region_size; - } - - if (offset < info.size) { - info.size -= offset; - - if (info.data) { - snprintf(description, sizeof(description), "offset 0x%06X", (int)offset); - info.data += offset; - } - else { - snprintf(description, sizeof(description), "null filler"); - } - } - else { - if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { - snprintf(description, sizeof(description), "Could not map region starting at $%06X", (unsigned)console_region->start_address); - rc_libretro_verbose(description); - } - - info.data = NULL; - info.size = 0; - } - - if (console_region_size > info.size) { - /* want more than what is available, take what we can and null fill the rest */ - rc_libretro_memory_register_region(regions, console_region->type, info.data, info.size, description); - rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size - info.size, "null filler"); - } - else { - /* only take as much as we need */ - rc_libretro_memory_register_region(regions, console_region->type, info.data, console_region_size, description); - } - } -} - -int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, - rc_libretro_get_core_memory_info_func get_core_memory_info, uint32_t console_id) { - const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); - rc_libretro_memory_regions_t new_regions; - int has_valid_region = 0; - uint32_t i; - - if (!regions) - return 0; - - memset(&new_regions, 0, sizeof(new_regions)); - - if (console_regions == NULL || console_regions->num_regions == 0) - rc_libretro_memory_init_without_regions(&new_regions, get_core_memory_info); - else if (mmap && mmap->num_descriptors != 0) - rc_libretro_memory_init_from_memory_map(&new_regions, mmap, console_regions); - else - rc_libretro_memory_init_from_unmapped_memory(&new_regions, get_core_memory_info, console_regions); - - /* determine if any valid regions were found */ - for (i = 0; i < new_regions.count; i++) { - if (new_regions.data[i]) { - has_valid_region = 1; - break; - } - } - - memcpy(regions, &new_regions, sizeof(*regions)); - return has_valid_region; -} - -void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions) { - memset(regions, 0, sizeof(*regions)); -} - -void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, - const char* m3u_path, rc_libretro_get_image_path_func get_image_path, - const rc_hash_filereader_t* file_reader) { - char image_path[1024]; - char* m3u_contents; - char* ptr; - int64_t file_len; - void* file_handle; - int index = 0; - - memset(hash_set, 0, sizeof(*hash_set)); - - if (!rc_path_compare_extension(m3u_path, "m3u")) - return; - - file_handle = file_reader->open(m3u_path); - if (!file_handle) { - rc_hash_iterator_t iterator; - memset(&iterator, 0, sizeof(iterator)); - memcpy(&iterator.callbacks, &hash_set->callbacks, sizeof(hash_set->callbacks)); - rc_hash_iterator_error(&iterator, "Could not open playlist"); - return; - } - - file_reader->seek(file_handle, 0, SEEK_END); - file_len = file_reader->tell(file_handle); - file_reader->seek(file_handle, 0, SEEK_SET); - - m3u_contents = (char*)malloc((size_t)file_len + 1); - if (m3u_contents) { - file_reader->read(file_handle, m3u_contents, (int)file_len); - m3u_contents[file_len] = '\0'; - - ptr = m3u_contents; - do - { - /* ignore whitespace */ - while (isspace((int)*ptr)) - ++ptr; - - if (*ptr == '#') { - /* ignore comment unless it's the special SAVEDISK extension */ - if (memcmp(ptr, "#SAVEDISK:", 10) == 0) { - /* get the path to the save disk from the frontend, assign it a bogus hash so - * it doesn't get hashed later */ - if (get_image_path(index, image_path, sizeof(image_path))) { - const char save_disk_hash[33] = "[SAVE DISK]"; - rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); - ++index; - } - } - } - else { - /* non-empty line, tally a file */ - ++index; - } - - /* find the end of the line */ - while (*ptr && *ptr != '\n') - ++ptr; - - } while (*ptr); - - free(m3u_contents); - } - - if (file_reader->close) - file_reader->close(file_handle); - - if (hash_set->entries_count > 0) { - /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by - * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */ - if (!get_image_path(index - 1, image_path, sizeof(image_path))) - hash_set->entries_count = 0; - } -} - -void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { - if (hash_set->entries) - free(hash_set->entries); - memset(hash_set, 0, sizeof(*hash_set)); -} - -static uint32_t rc_libretro_djb2(const char* input) -{ - uint32_t result = 5381; - char c; - - while ((c = *input++) != '\0') - result = ((result << 5) + result) + c; /* result = result * 33 + c */ - - return result; -} - -void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, - const char* path, uint32_t game_id, const char hash[33]) { - const uint32_t path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; - struct rc_libretro_hash_entry_t* entry = NULL; - struct rc_libretro_hash_entry_t* scan; - struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; - - if (path_djb2) { - /* attempt to match the path */ - for (scan = hash_set->entries; scan < stop; ++scan) { - if (scan->path_djb2 == path_djb2) { - entry = scan; - break; - } - } - } - - if (!entry) - { - /* entry not found, allocate a new one */ - if (hash_set->entries_size == 0) { - hash_set->entries_size = 4; - hash_set->entries = (struct rc_libretro_hash_entry_t*) - malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); - } - else if (hash_set->entries_count == hash_set->entries_size) { - hash_set->entries_size += 4; - hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries, - hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); - } - - if (hash_set->entries == NULL) /* unexpected, but better than crashing */ - return; - - entry = hash_set->entries + hash_set->entries_count++; - } - - /* update the entry */ - entry->path_djb2 = path_djb2; - entry->game_id = game_id; - memcpy(entry->hash, hash, sizeof(entry->hash)); -} - -const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) -{ - const uint32_t path_djb2 = rc_libretro_djb2(path); - struct rc_libretro_hash_entry_t* scan = hash_set->entries; - struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; - for (; scan < stop; ++scan) { - if (scan->path_djb2 == path_djb2) - return scan->hash; - } - - return NULL; -} - -int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash) -{ - struct rc_libretro_hash_entry_t* scan = hash_set->entries; - struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; - for (; scan < stop; ++scan) { - if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0) - return scan->game_id; - } - - return 0; -} diff --git a/src/rcheevos/src/rc_libretro.h b/src/rcheevos/src/rc_libretro.h deleted file mode 100644 index 8821303218..0000000000 --- a/src/rcheevos/src/rc_libretro.h +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef RC_LIBRETRO_H -#define RC_LIBRETRO_H - -#include "rc_export.h" - -#include "rc_hash.h" - -/* this file comes from the libretro repository, which is not an explicit submodule. - * the integration must set up paths appropriately to find it. */ -#include - -#include -#include - -RC_BEGIN_C_DECLS - -/*****************************************************************************\ -| Disallowed Settings | -\*****************************************************************************/ - -typedef struct rc_disallowed_setting_t -{ - const char* setting; - const char* value; -} rc_disallowed_setting_t; - -RC_EXPORT const rc_disallowed_setting_t* RC_CCONV rc_libretro_get_disallowed_settings(const char* library_name); -RC_EXPORT int RC_CCONV rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); -RC_EXPORT int RC_CCONV rc_libretro_is_system_allowed(const char* library_name, uint32_t console_id); - -/*****************************************************************************\ -| Memory Mapping | -\*****************************************************************************/ - -/* specifies a function to call for verbose logging */ -typedef void (RC_CCONV *rc_libretro_message_callback)(const char*); -RC_EXPORT void RC_CCONV rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback); - -#define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 -typedef struct rc_libretro_memory_regions_t -{ - uint8_t* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; - size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; - size_t total_size; - uint32_t count; -} rc_libretro_memory_regions_t; - -typedef struct rc_libretro_core_memory_info_t -{ - uint8_t* data; - size_t size; -} rc_libretro_core_memory_info_t; - -typedef void (RC_CCONV *rc_libretro_get_core_memory_info_func)(uint32_t id, rc_libretro_core_memory_info_t* info); - -RC_EXPORT int RC_CCONV rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, - rc_libretro_get_core_memory_info_func get_core_memory_info, uint32_t console_id); -RC_EXPORT void RC_CCONV rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); - -RC_EXPORT uint8_t* RC_CCONV rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, uint32_t address); -RC_EXPORT uint8_t* RC_CCONV rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, uint32_t address, uint32_t* avail); -RC_EXPORT uint32_t RC_CCONV rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address, uint8_t* buffer, uint32_t num_bytes); - -/*****************************************************************************\ -| Disk Identification | -\*****************************************************************************/ - -typedef struct rc_libretro_hash_entry_t -{ - uint32_t path_djb2; - uint32_t game_id; - char hash[33]; -} rc_libretro_hash_entry_t; - -typedef struct rc_libretro_hash_set_t -{ - struct rc_libretro_hash_entry_t* entries; - uint16_t entries_count; - uint16_t entries_size; - - rc_hash_callbacks_t callbacks; -} rc_libretro_hash_set_t; - -typedef int (RC_CCONV *rc_libretro_get_image_path_func)(uint32_t index, char* buffer, size_t buffer_size); - -RC_EXPORT void RC_CCONV rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, - const char* m3u_path, rc_libretro_get_image_path_func get_image_path, - const rc_hash_filereader_t* file_reader); -RC_EXPORT void RC_CCONV rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); - -RC_EXPORT void RC_CCONV rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, - const char* path, uint32_t game_id, const char hash[33]); -RC_EXPORT const char* RC_CCONV rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); -RC_EXPORT int RC_CCONV rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); - -RC_END_C_DECLS - -#endif /* RC_LIBRETRO_H */ diff --git a/src/rcheevos/src/rcheevos/rc_runtime_types.natvis b/src/rcheevos/src/rcheevos/rc_runtime_types.natvis deleted file mode 100644 index 25529c5525..0000000000 --- a/src/rcheevos/src/rcheevos/rc_runtime_types.natvis +++ /dev/null @@ -1,541 +0,0 @@ - - - - - - {value.u32} (RC_VALUE_TYPE_UNSIGNED) - {value.f32} (RC_VALUE_TYPE_FLOAT) - {value.i32} (RC_VALUE_TYPE_SIGNED) - none (RC_VALUE_TYPE_NONE) - {value.i32} (unknown) - - - false - true - true ({value}) - - - {RC_MEMSIZE_8_BITS} - {RC_MEMSIZE_16_BITS} - {RC_MEMSIZE_24_BITS} - {RC_MEMSIZE_32_BITS} - {RC_MEMSIZE_LOW} - {RC_MEMSIZE_HIGH} - {RC_MEMSIZE_BIT_0} - {RC_MEMSIZE_BIT_1} - {RC_MEMSIZE_BIT_2} - {RC_MEMSIZE_BIT_3} - {RC_MEMSIZE_BIT_4} - {RC_MEMSIZE_BIT_5} - {RC_MEMSIZE_BIT_6} - {RC_MEMSIZE_BIT_7} - {RC_MEMSIZE_BITCOUNT} - {RC_MEMSIZE_16_BITS_BE} - {RC_MEMSIZE_24_BITS_BE} - {RC_MEMSIZE_32_BITS_BE} - {RC_MEMSIZE_FLOAT} - {RC_MEMSIZE_MBF32} - {RC_MEMSIZE_MBF32_LE} - {RC_MEMSIZE_FLOAT_BE} - {RC_MEMSIZE_DOUBLE32} - {RC_MEMSIZE_DOUBLE32_BE} - {RC_MEMSIZE_VARIABLE} - unknown ({value}) - - - {RC_VALUE_TYPE_NONE} - {RC_VALUE_TYPE_UNSIGNED} - {RC_VALUE_TYPE_SIGNED} - {RC_VALUE_TYPE_FLOAT} - unknown ({value}) - - - {RC_MEMREF_TYPE_MEMREF} - {RC_MEMREF_TYPE_MODIFIED_MEMREF} - RC_MEMREF_TYPE_VALUE - unknown ({value}) - - - byte - word - tbyte - dword - lower4 - upper4 - bit0 - bit1 - bit2 - bit3 - bit4 - bit5 - bit6 - bit7 - bitcount - word_be - tbyte_be - dword_be - float - mbf32 - mbf32_le - float_be - double32 - double32_be - var - unknown - - - {{value={(float)*((float*)&value)} prior={(float)*((float*)&prior)}}} - {{value={(int)value} prior={(int)prior}}} - {{value={value,x} prior={prior,x}}} - - value - prior - *((__rc_memsize_enum_t*)&size) - *((__rc_bool_enum_t*)&changed) - *((__rc_value_type_enum_t*)&type) - *((__rc_memref_type_enum_t*)&memref_type) - - - - {*(rc_modified_memref_t*)&value} - {*(rc_value_t*)&value} - var - {*((__rc_memsize_enum_func_t*)&value.size)}({address,x}) - - (rc_modified_memref_t*)&value - value - address - - - - {{count = {count}}} - - next - count - capacity - - count - items - - - - - ... {*((__rc_memsize_enum_func_t*)&operand.size)} {((rc_modified_memref_t*)operand.value.memref)->parent,na} - value {((rc_value_t*)operand.value.memref)->name,s} - {*((__rc_memsize_enum_func_t*)&operand.size)} {operand.value.memref->address,x} - - (rc_modified_memref_t*)&operand.value - operand.value.memref->value - operand.value.memref->address - - - - {RC_OPERAND_ADDRESS} - {RC_OPERAND_DELTA} - {RC_OPERAND_CONST} - {RC_OPERAND_FP} - {RC_OPERAND_FUNC} - {RC_OPERAND_PRIOR} - {RC_OPERAND_BCD} - {RC_OPERAND_INVERTED} - {RC_OPERAND_RECALL} - RC_OPERAND_NONE (255) - unknown ({value}) - - - {{{*(__rc_operand_memref_t*)&value.memref}}} - {{delta {*(__rc_operand_memref_t*)&value.memref}}} - {{prior {*(__rc_operand_memref_t*)&value.memref}}} - {{bcd {*(__rc_operand_memref_t*)&value.memref}}} - {{inverted {*(__rc_operand_memref_t*)&value.memref}}} - {{value {(int)value.num}}} - {{value {value.num,x}}} - {{value {value.num}}} - {{value {value.dbl}}} - {{recall}} - {{func @{value}}} - {{none}} - {{unknown}} - - value.num - value.dbl - value.num - value.dbl - value.memref - value.memref - (rc_modified_memref_t*)value.memref - *((__rc_operand_enum_t*)&type) - *((__rc_memsize_enum_t*)&size) - *((__rc_operand_enum_t*)&memref_access_type) - - - - {RC_CONDITION_STANDARD} - {RC_CONDITION_PAUSE_IF} - {RC_CONDITION_RESET_IF} - {RC_CONDITION_MEASURED_IF} - {RC_CONDITION_TRIGGER} - {RC_CONDITION_MEASURED} - {RC_CONDITION_ADD_SOURCE} - {RC_CONDITION_SUB_SOURCE} - {RC_CONDITION_ADD_ADDRESS} - {RC_CONDITION_REMEMBER} - {RC_CONDITION_ADD_HITS} - {RC_CONDITION_SUB_HITS} - {RC_CONDITION_RESET_NEXT_IF} - {RC_CONDITION_AND_NEXT} - {RC_CONDITION_OR_NEXT} - unknown ({value}) - - - - PauseIf - ResetIf - MeasuredIf - Trigger - Measured - AddSource - SubSource - AddAddress - Remember - AddHits - SubHits - ResetNextIf - AndNext - OrNext - {value} - - - {RC_OPERATOR_EQ} - {RC_OPERATOR_LT} - {RC_OPERATOR_LE} - {RC_OPERATOR_GT} - {RC_OPERATOR_GE} - {RC_OPERATOR_NE} - {RC_OPERATOR_NONE} - {RC_OPERATOR_MULT} - {RC_OPERATOR_DIV} - {RC_OPERATOR_AND} - {RC_OPERATOR_XOR} - {RC_OPERATOR_MOD} - {RC_OPERATOR_ADD} - {RC_OPERATOR_SUB} - {RC_OPERATOR_SUB_PARENT} - {RC_OPERATOR_INDIRECT_READ} - unknown ({value}) - - - == - < - <= - > - >= - != - - * - / - & - ^ - % - + - - - subtracted from - $ - unknown ({value}) - - - {*((__rc_condition_enum_str_t*)&type)} {operand1} - {*((__rc_condition_enum_str_t*)&type)} {operand1} ({required_hits}) - {*((__rc_condition_enum_str_t*)&type)} {operand1} {*((__rc_operator_enum_str_t*)&oper)} {operand2} - {*((__rc_condition_enum_str_t*)&type)} {operand1} {*((__rc_operator_enum_str_t*)&oper)} {operand2} ({required_hits}) - - *((__rc_condition_enum_t*)&type) - operand1 - *((__rc_operator_enum_t*)&oper) - operand2 - required_hits - current_hits - next - - - - {{count={num_pause_conditions+num_reset_conditions+num_hittarget_conditions+num_measured_conditions+num_other_conditions+num_indirect_conditions}}} - - - conditions - next - this - - - - - {{}} - - - first_condset - next - this - - - - - $({parent} + {modifier}) - ({modifier} - {parent}) - ({parent} {*((__rc_operator_enum_str_t*)&modifier_type)} {modifier}) - - memref.value - parent - *((__rc_operator_enum_t*)&modifier_type) - modifier - - - - {{count = {count}}} - - next - count - capacity - - count - items - - - - - {RC_TRIGGER_STATE_INACTIVE} - {RC_TRIGGER_STATE_WAITING} - {RC_TRIGGER_STATE_ACTIVE} - {RC_TRIGGER_STATE_PAUSED} - {RC_TRIGGER_STATE_RESET} - {RC_TRIGGER_STATE_TRIGGERED} - {RC_TRIGGER_STATE_PRIMED} - {RC_TRIGGER_STATE_DISABLED} - unknown ({value}) - - - - *((__rc_trigger_state_enum_t*)&state) - *((__rc_bool_enum_t*)&has_hits) - *((__rc_bool_enum_t*)&measured_as_percent) - requirement - *((__rc_condset_list_t*)&alternative) - - - - {value} {name,s} - - value - conditions - name - - - - {{count = {count}}} - - count - capacity - - count - items - - - - - {RC_LBOARD_STATE_INACTIVE} - {RC_LBOARD_STATE_WAITING} - {RC_LBOARD_STATE_ACTIVE} - {RC_LBOARD_STATE_STARTED} - {RC_LBOARD_STATE_CANCELED} - {RC_LBOARD_STATE_TRIGGERED} - {RC_LBOARD_STATE_DISABLED} - unknown ({value}) - - - - *((__rc_lboard_state_enum_t*)&state) - start - submit - cancel - value - - - - {RC_FORMAT_FRAMES} - {RC_FORMAT_SECONDS} - {RC_FORMAT_CENTISECS} - {RC_FORMAT_SCORE} - {RC_FORMAT_VALUE} - {RC_FORMAT_MINUTES} - {RC_FORMAT_SECONDS_AS_MINUTES} - {RC_FORMAT_FLOAT1} - {RC_FORMAT_FLOAT2} - {RC_FORMAT_FLOAT3} - {RC_FORMAT_FLOAT4} - {RC_FORMAT_FLOAT5} - {RC_FORMAT_FLOAT6} - {RC_FORMAT_FIXED1} - {RC_FORMAT_FIXED2} - {RC_FORMAT_FIXED3} - {RC_FORMAT_TENS} - {RC_FORMAT_HUNDREDS} - {RC_FORMAT_THOUSANDS} - {RC_FORMAT_UNSIGNED_VALUE} - {RC_FORMAT_UNFORMATTED} - RC_FORMAT_STRING (101) - RC_FORMAT_LOOKUP (102) - RC_FORMAT_UNKNOWN_MACRO (103) - RC_FORMAT_ASCIICHAR (104) - RC_FORMAT_UNICODECHAR (105) - unknown ({value}) - - - {text,s} - [Unknown macro]{text,sb} - @{text,sb}({value,na}) - @{lookup->name,sb}({value,na}) - - text - lookup - value - *((__rc_format_enum_t*)&display_type) - - - - {display,na} {display->next,na} {display->next->next,na} - {display,na} {display->next,na} - {display,na} - - - display - next - this - - - - - - *((__rc_richpresence_display_part_list_t*)&display) - trigger - - - - {{NULL}} - {(void*)&first_display,na} - - - first_display - next - this - - - - - {first}: {label,na} - {first}-{last}: {label,na} - - - {name,na} - - name - *((__rc_format_enum_t*)&format) - default_label - - root - left - right - this - - - - - {{NULL}} - {(void*)&first_lookup,na} - - - first_lookup - next - this - - - - - - ((__rc_richpresence_display_list_t*)&first_display) - ((__rc_richpresence_lookup_list_t*)&first_lookup) - - - - {{NULL}} - {(void*)&first_value,na} - - - first_value - next - this - - - - - {{offset={offset} addsource_parent={addsource_parent} indirect_parent={indirect_parent}}} - - offset - memrefs - existing_memrefs - variables - ((__rc_value_list_t*)&variables) - addsource_parent - *((__rc_operator_enum_t*)&addsource_oper) - indirect_parent - remember - *((__rc_bool_enum_t*)&is_value) - *((__rc_bool_enum_t*)&has_required_hits) - *((__rc_bool_enum_t*)&measured_as_percent) - - - - {{used={write-start} size={end-start}}} - - end-start - write-start - end-write - next - - - - - - - &chunk - next - this - - - - - {{count={runtime.trigger_count}}} - - - runtime.trigger_count - runtime.triggers[$i] - - - - - {{count={runtime.lboard_count}}} - - - runtime.lboard_count - runtime.lboards[$i] - - - - - {{trigger_count={trigger_count} lboard_count={lboard_count}}} - - *((__rc_runtime_trigger_list_t*)this) - *((__rc_runtime_lboard_list_t*)this) - richpresence - memrefs - - - diff --git a/src/rcheevos/src/rhash/aes.c b/src/rcheevos/src/rhash/aes.c deleted file mode 100644 index 54ed10bce9..0000000000 --- a/src/rcheevos/src/rhash/aes.c +++ /dev/null @@ -1,480 +0,0 @@ -/* This file is sourced from https://github.com/kokke/tiny-AES-c, with unused code excised. - * This code is licensed under the Unlicense license, effectively public domain. - * https://github.com/kokke/tiny-AES-c/blob/f06ac37/unlicense.txt - */ - -/* - -This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. -Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. - -The implementation is verified against the test vectors in: - National Institute of Standards and Technology Special Publication 800-38A 2001 ED - -ECB-AES128 ----------- - - plain-text: - 6bc1bee22e409f96e93d7e117393172a - ae2d8a571e03ac9c9eb76fac45af8e51 - 30c81c46a35ce411e5fbc1191a0a52ef - f69f2445df4f9b17ad2b417be66c3710 - - key: - 2b7e151628aed2a6abf7158809cf4f3c - - resulting cipher - 3ad77bb40d7a3660a89ecaf32466ef97 - f5d3d58503b9699de785895a96fdbaaf - 43b1cd7f598ece23881b00e3ed030688 - 7b0c785e27e8ad3f8223207104725dd4 - - -NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0) - You should pad the end of the string with zeros if this is not the case. - For AES192/256 the key size is proportionally larger. - -*/ - - -/*****************************************************************************/ -/* Includes: */ -/*****************************************************************************/ -#include /* CBC mode, for memset */ -#include "aes.h" - -/*****************************************************************************/ -/* Defines: */ -/*****************************************************************************/ - -/* The number of columns comprising a state in AES. This is a constant in AES. Value=4 */ -#define Nb 4 - -#define Nk 4 /* The number of 32 bit words in a key. */ -#define Nr 10 /* The number of rounds in AES Cipher. */ - -/*****************************************************************************/ -/* Private variables: */ -/*****************************************************************************/ - -/* state - array holding the intermediate results during decryption. */ -typedef uint8_t state_t[4][4]; - -/* The lookup-tables are marked const so they can be placed in read-only storage instead of RAM - * The numbers below can be computed dynamically trading ROM for RAM - - * This can be useful in (embedded) bootloader applications, where ROM is often limited. - */ -static const uint8_t sbox[256] = { - /*0 1 2 3 4 5 6 7 8 9 A B C D E F */ - 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, - 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, - 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, - 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, - 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, - 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, - 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, - 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, - 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, - 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, - 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, - 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, - 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, - 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, - 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, - 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; - -static const uint8_t rsbox[256] = { - 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, - 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, - 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, - 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, - 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, - 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, - 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, - 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, - 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, - 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, - 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, - 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, - 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, - 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, - 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, - 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d }; - -/* The round constant word array, Rcon[i], contains the values given by - * x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8) - */ -static const uint8_t Rcon[11] = { - 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 }; - -/* - * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES-C/pull/12), - * that you can remove most of the elements in the Rcon array, because they are unused. - * - * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon - * - * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed), - * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm." - */ - - -/*****************************************************************************/ -/* Private functions: */ -/*****************************************************************************/ - -#define getSBoxValue(num) (sbox[(num)]) - -/* This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states. */ -static void KeyExpansion(uint8_t RoundKey[AES_keyExpSize], const uint8_t Key[AES_KEYLEN]) -{ - unsigned i, j, k; - uint8_t tempa[4]; /* Used for the column/row operations */ - - /* The first round key is the key itself. */ - for (i = 0; i < Nk; ++i) - { - RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; - RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; - RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; - RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; - } - - /* All other round keys are found from the previous round keys. */ - for (i = Nk; i < Nb * (Nr + 1); ++i) - { - { - k = (i - 1) * 4; - tempa[0]=RoundKey[k + 0]; - tempa[1]=RoundKey[k + 1]; - tempa[2]=RoundKey[k + 2]; - tempa[3]=RoundKey[k + 3]; - - } - - if (i % Nk == 0) - { - /* This function shifts the 4 bytes in a word to the left once. */ - /* [a0,a1,a2,a3] becomes [a1,a2,a3,a0] */ - - /* Function RotWord() */ - { - const uint8_t u8tmp = tempa[0]; - tempa[0] = tempa[1]; - tempa[1] = tempa[2]; - tempa[2] = tempa[3]; - tempa[3] = u8tmp; - } - - /* SubWord() is a function that takes a four-byte input word and - * applies the S-box to each of the four bytes to produce an output word. - */ - - /* Function Subword() */ - { - tempa[0] = getSBoxValue(tempa[0]); - tempa[1] = getSBoxValue(tempa[1]); - tempa[2] = getSBoxValue(tempa[2]); - tempa[3] = getSBoxValue(tempa[3]); - } - - tempa[0] = tempa[0] ^ Rcon[i/Nk]; - } - - j = i * 4; k=(i - Nk) * 4; - RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; - RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; - RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; - RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; - } -} - -void AES_init_ctx(struct AES_ctx* ctx, const uint8_t key[AES_KEYLEN]) -{ - KeyExpansion(ctx->RoundKey, key); -} - -void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t key[AES_KEYLEN], const uint8_t iv[AES_BLOCKLEN]) -{ - KeyExpansion(ctx->RoundKey, key); - memcpy (ctx->Iv, iv, AES_BLOCKLEN); -} - -void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t iv[AES_BLOCKLEN]) -{ - memcpy (ctx->Iv, iv, AES_BLOCKLEN); -} - -/* This function adds the round key to state. - * The round key is added to the state by an XOR function. - */ -static void AddRoundKey(uint8_t round, state_t* state, const uint8_t RoundKey[AES_keyExpSize]) -{ - uint8_t i,j; - for (i = 0; i < 4; ++i) - { - for (j = 0; j < 4; ++j) - { - (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; - } - } -} - -/* The SubBytes Function Substitutes the values in the - * state matrix with values in an S-box. - */ -static void SubBytes(state_t* state) -{ - uint8_t i, j; - for (i = 0; i < 4; ++i) - { - for (j = 0; j < 4; ++j) - { - (*state)[j][i] = getSBoxValue((*state)[j][i]); - } - } -} - -/* The ShiftRows() function shifts the rows in the state to the left. - * Each row is shifted with different offset. - * Offset = Row number. So the first row is not shifted. - */ -static void ShiftRows(state_t* state) -{ - uint8_t temp; - - /* Rotate first row 1 columns to left */ - temp = (*state)[0][1]; - (*state)[0][1] = (*state)[1][1]; - (*state)[1][1] = (*state)[2][1]; - (*state)[2][1] = (*state)[3][1]; - (*state)[3][1] = temp; - - /* Rotate second row 2 columns to left */ - temp = (*state)[0][2]; - (*state)[0][2] = (*state)[2][2]; - (*state)[2][2] = temp; - - temp = (*state)[1][2]; - (*state)[1][2] = (*state)[3][2]; - (*state)[3][2] = temp; - - /* Rotate third row 3 columns to left */ - temp = (*state)[0][3]; - (*state)[0][3] = (*state)[3][3]; - (*state)[3][3] = (*state)[2][3]; - (*state)[2][3] = (*state)[1][3]; - (*state)[1][3] = temp; -} - -static uint8_t xtime(uint8_t x) -{ - return ((x<<1) ^ (((x>>7) & 1) * 0x1b)); -} - -/* MixColumns function mixes the columns of the state matrix */ -static void MixColumns(state_t* state) -{ - uint8_t i; - uint8_t Tmp, Tm, t; - for (i = 0; i < 4; ++i) - { - t = (*state)[i][0]; - Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ; - Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ; - Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ; - Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ; - Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ; - } -} - -/* Multiply is used to multiply numbers in the field GF(2^8) - * Note: The last call to xtime() is unneeded, but often ends up generating a smaller binary - * The compiler seems to be able to vectorize the operation better this way. - * See https://github.com/kokke/tiny-AES-c/pull/34 - */ - -#define Multiply(x, y) \ - ( ((y & 1) * x) ^ \ - ((y>>1 & 1) * xtime(x)) ^ \ - ((y>>2 & 1) * xtime(xtime(x))) ^ \ - ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \ - ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \ - -#define getSBoxInvert(num) (rsbox[(num)]) - -/* MixColumns function mixes the columns of the state matrix. - * The method used to multiply may be difficult to understand for the inexperienced. - * Please use the references to gain more information. - */ -static void InvMixColumns(state_t* state) -{ - int i; - uint8_t a, b, c, d; - for (i = 0; i < 4; ++i) - { - a = (*state)[i][0]; - b = (*state)[i][1]; - c = (*state)[i][2]; - d = (*state)[i][3]; - - (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09); - (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d); - (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b); - (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e); - } -} - - -/* The SubBytes Function Substitutes the values in the - * state matrix with values in an S-box. - */ -static void InvSubBytes(state_t* state) -{ - uint8_t i, j; - for (i = 0; i < 4; ++i) - { - for (j = 0; j < 4; ++j) - { - (*state)[j][i] = getSBoxInvert((*state)[j][i]); - } - } -} - -static void InvShiftRows(state_t* state) -{ - uint8_t temp; - - /* Rotate first row 1 columns to right */ - temp = (*state)[3][1]; - (*state)[3][1] = (*state)[2][1]; - (*state)[2][1] = (*state)[1][1]; - (*state)[1][1] = (*state)[0][1]; - (*state)[0][1] = temp; - - /* Rotate second row 2 columns to right */ - temp = (*state)[0][2]; - (*state)[0][2] = (*state)[2][2]; - (*state)[2][2] = temp; - - temp = (*state)[1][2]; - (*state)[1][2] = (*state)[3][2]; - (*state)[3][2] = temp; - - /* Rotate third row 3 columns to right */ - temp = (*state)[0][3]; - (*state)[0][3] = (*state)[1][3]; - (*state)[1][3] = (*state)[2][3]; - (*state)[2][3] = (*state)[3][3]; - (*state)[3][3] = temp; -} - -/* Cipher is the main function that encrypts the PlainText. */ -static void Cipher(state_t* state, const uint8_t RoundKey[AES_keyExpSize]) -{ - uint8_t round = 0; - - /* Add the First round key to the state before starting the rounds. */ - AddRoundKey(0, state, RoundKey); - - /* There will be Nr rounds. - * The first Nr-1 rounds are identical. - * These Nr rounds are executed in the loop below. - * Last one without MixColumns() - */ - for (round = 1; ; ++round) - { - SubBytes(state); - ShiftRows(state); - if (round == Nr) { - break; - } - MixColumns(state); - AddRoundKey(round, state, RoundKey); - } - /* Add round key to last round */ - AddRoundKey(Nr, state, RoundKey); -} - -static void InvCipher(state_t* state, const uint8_t RoundKey[AES_keyExpSize]) -{ - uint8_t round = 0; - - /* Add the First round key to the state before starting the rounds. */ - AddRoundKey(Nr, state, RoundKey); - - /* There will be Nr rounds. - * The first Nr-1 rounds are identical. - * These Nr rounds are executed in the loop below. - * Last one without InvMixColumn() - */ - for (round = (Nr - 1); ; --round) - { - InvShiftRows(state); - InvSubBytes(state); - AddRoundKey(round, state, RoundKey); - if (round == 0) { - break; - } - InvMixColumns(state); - } -} - -/*****************************************************************************/ -/* Public functions: */ -/*****************************************************************************/ - -static void XorWithIv(uint8_t* buf, const uint8_t Iv[AES_BLOCKLEN]) -{ - uint8_t i; - for (i = 0; i < AES_BLOCKLEN; ++i) /* The block in AES is always 128bit no matter the key size */ - { - buf[i] ^= Iv[i]; - } -} - -void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) -{ - size_t i; - uint8_t storeNextIv[AES_BLOCKLEN]; - for (i = 0; i < length; i += AES_BLOCKLEN) - { - memcpy(storeNextIv, buf, AES_BLOCKLEN); - InvCipher((state_t*)buf, ctx->RoundKey); - XorWithIv(buf, ctx->Iv); - memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN); - buf += AES_BLOCKLEN; - } -} - -/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ -void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) -{ - uint8_t buffer[AES_BLOCKLEN]; - - size_t i; - int bi; - for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) - { - if (bi == AES_BLOCKLEN) /* we need to regen xor compliment in buffer */ - { - memcpy(buffer, ctx->Iv, AES_BLOCKLEN); - Cipher((state_t*)buffer, ctx->RoundKey); - - /* Increment Iv and handle overflow */ - for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) - { - /* inc will overflow */ - if (ctx->Iv[bi] == 255) - { - ctx->Iv[bi] = 0; - continue; - } - ctx->Iv[bi] += 1; - break; - } - bi = 0; - } - - buf[i] = (buf[i] ^ buffer[bi]); - } -} diff --git a/src/rcheevos/test/Makefile b/src/rcheevos/test/Makefile deleted file mode 100644 index 2abac55f3d..0000000000 --- a/src/rcheevos/test/Makefile +++ /dev/null @@ -1,224 +0,0 @@ -# supported parameters -# ARCH architecture - "x86" or "x64" [detected if not set] -# BUILD use flags for specified upstream consumer - "c89" or "retroarch" [default to "c89"] -# DEBUG if set to anything, builds with DEBUG symbols -# HAVE_HASH if set to 0, excludes all hash functionality -# HAVE_HASH_ROM if set to 0, excludes rom hash generation -# HAVE_HASH_DISC if set to 0, excludes disc hash generation -# HAVE_HASH_ZIP if set to 0, excludes zip hash generation -# HAVE_HASH_ENCRYPTED if set to 0, excludes encrypted hash generation - -RC_SRC=../src -RC_CHEEVOS_SRC=$(RC_SRC)/rcheevos -RC_HASH_SRC=$(RC_SRC)/rhash -RC_API_SRC=$(RC_SRC)/rapi - -# default parameter values -ifeq ($(ARCH),) - UNAME := $(shell uname -s) - ifeq ($(findstring MINGW64, $(UNAME)), MINGW64) - ARCH=x64 - else ifeq ($(findstring MINGW32, $(UNAME)), MINGW32) - ARCH=x86 - else - $(error Could not determine ARCH) - endif -endif - -ifeq ($(BUILD),) - BUILD=c89 -endif - -# OS specific stuff -ifeq ($(OS),Windows_NT) - EXE=.exe -else ifeq ($(findstring mingw, $(CC)), mingw) - EXE=.exe -else - EXE= -endif - -# source files -OBJ=$(RC_SRC)/rc_compat.o \ - $(RC_SRC)/rc_client.o \ - $(RC_SRC)/rc_util.o \ - $(RC_SRC)/rc_version.o \ - $(RC_CHEEVOS_SRC)/alloc.o \ - $(RC_CHEEVOS_SRC)/condition.o \ - $(RC_CHEEVOS_SRC)/condset.o \ - $(RC_CHEEVOS_SRC)/consoleinfo.o \ - $(RC_CHEEVOS_SRC)/format.o \ - $(RC_CHEEVOS_SRC)/lboard.o \ - $(RC_CHEEVOS_SRC)/memref.o \ - $(RC_CHEEVOS_SRC)/operand.o \ - $(RC_CHEEVOS_SRC)/rc_validate.o \ - $(RC_CHEEVOS_SRC)/richpresence.o \ - $(RC_CHEEVOS_SRC)/runtime.o \ - $(RC_CHEEVOS_SRC)/runtime_progress.o \ - $(RC_CHEEVOS_SRC)/trigger.o \ - $(RC_CHEEVOS_SRC)/value.o \ - $(RC_HASH_SRC)/md5.o \ - $(RC_API_SRC)/rc_api_common.o \ - $(RC_API_SRC)/rc_api_editor.o \ - $(RC_API_SRC)/rc_api_info.o \ - $(RC_API_SRC)/rc_api_runtime.o \ - $(RC_API_SRC)/rc_api_user.o \ - rcheevos/test_condition.o \ - rcheevos/test_condset.o \ - rcheevos/test_consoleinfo.o \ - rcheevos/test_format.o \ - rcheevos/test_lboard.o \ - rcheevos/test_memref.o \ - rcheevos/test_operand.o \ - rcheevos/test_rc_validate.o \ - rcheevos/test_richpresence.o \ - rcheevos/test_runtime.o \ - rcheevos/test_runtime_progress.o \ - rcheevos/test_timing.o \ - rcheevos/test_trigger.o \ - rcheevos/test_value.o \ - rapi/test_rc_api_common.o \ - rapi/test_rc_api_editor.o \ - rapi/test_rc_api_info.o \ - rapi/test_rc_api_runtime.o \ - rapi/test_rc_api_user.o \ - test_rc_client.o \ - test.o - -# compile flags -CFLAGS=-Wall -Werror -Wno-long-long -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -INCLUDES=-I../include -I$(RC_CHEEVOS_SRC) -I. - -ifeq ($(ARCH), x86) - CFLAGS += -m32 - LDFLAGS += -m32 -else ifeq ($(ARCH), x64) - CFLAGS += -m64 - LDFLAGS += -m64 -else - $(error unknown ARCH "$(ARCH)") -endif - -EXTRA= -ifdef DEBUG - CFLAGS += -O0 -g - EXTRA += DEBUG -else - CFLAGS += -O3 -endif - -# more strict validation for source files to eliminate warnings/errors in upstream consumers -# -Wextra includes -Wsign-compare -Wtype-limits -Wimplicit-fallthrough=3 -Wunused-parameter and more -# -pedantic issues errors when using GNU extensions or other things that are not strictly ISO C -SRC_CFLAGS=-Wextra -Wshadow -pedantic -# 3DS build (retroarch) doesn't support signed char -SRC_CFLAGS += -fno-signed-char - -ifeq ($(BUILD), c89) - CFLAGS += -std=c89 -else ifeq ($(BUILD), c99) - CFLAGS += -std=c99 -else ifeq ($(BUILD), retroarch) - # RetroArch builds with gcc8 and gnu99 which adds some extra warning validations - SRC_CFLAGS += -std=gnu99 -D_GNU_SOURCE -Wall -Warray-bounds=2 -Wincompatible-pointer-types - # Also include the RC_CLIENT_SUPPORTS_EXTERNAL flag to ensure we do the full level of validation on the - # most code. The c89 build will ensure things still build without the RC_CLIENT_SUPPORTS_EXTERNAL flag. - CFLAGS += -DRC_CLIENT_SUPPORTS_EXTERNAL - OBJ += $(RC_SRC)/rc_client_external.o test_rc_client_external.o -else - $(error unknown BUILD "$(BUILD)") -endif - -# support for building without hash subdirectory -ifeq ($(HAVE_HASH), 0) - EXTRA += |NO_HASH -else - CFLAGS += -DRC_CLIENT_SUPPORTS_HASH - OBJ += $(RC_HASH_SRC)/hash.o \ - $(RC_SRC)/rc_libretro.o \ - rhash/data.o \ - rhash/mock_filereader.o \ - rhash/test_hash.o \ - test_rc_libretro.o - - ifeq ($(HAVE_HASH_ROM), 0) - EXTRA += |NO_HASH_ROM - CFLAGS += -DRC_HASH_NO_ROM - else - OBJ += $(RC_HASH_SRC)/hash_rom.o \ - rhash/test_hash_rom.o - endif - - ifeq ($(HAVE_HASH_DISC), 0) - EXTRA += |NO_HASH_DISC - CFLAGS += -DRC_HASH_NO_DISC - else - OBJ += $(RC_HASH_SRC)/cdreader.o \ - $(RC_HASH_SRC)/hash_disc.o \ - rhash/test_cdreader.o \ - rhash/test_hash_disc.o - endif - - ifeq ($(HAVE_HASH_ZIP), 0) - EXTRA += |NO_HASH_ZIP - CFLAGS += -DRC_HASH_NO_ZIP - else - OBJ += $(RC_HASH_SRC)/hash_zip.o \ - rhash/test_hash_zip.o - endif - - ifeq ($(HAVE_HASH_ENCRYPTED), 0) - EXTRA += |NO_HASH_ENCRYPTED - CFLAGS += -DRC_HASH_NO_ENCRYPTED - else - OBJ += $(RC_HASH_SRC)/hash_encrypted.o \ - $(RC_HASH_SRC)/aes.o - endif -endif - -# recipes -$(info ==== rcheevos test [$(BUILD)/$(ARCH)$(EXTRA)] ====) - -all: test - -$(RC_SRC)/%.o: $(RC_SRC)/%.c - $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ - -$(RC_CHEEVOS_SRC)/%.o: $(RC_CHEEVOS_SRC)/%.c - $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ - -$(RC_HASH_SRC)/%.o: $(RC_HASH_SRC)/%.c - $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ - -$(RC_API_SRC)/%.o: $(RC_API_SRC)/%.c - $(CC) $(CFLAGS) $(SRC_CFLAGS) $(INCLUDES) -c $< -o $@ - -%.o: %.c - $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ - -test: $(OBJ) - $(CC) $(LDFLAGS) -o $@$(EXE) $+ -lm - @echo --------------------------------- - -check_ctype: - @if grep -rnI "isalpha([^(u]" ../src/*; \ - then echo "*** Error: isalpha without unsigned char cast" && false; \ - fi - @if grep -rnI "isalnum([^(u]" ../src/*; \ - then echo "*** Error: isalnum without unsigned char cast" && false; \ - fi - @if grep -rnI "isdigit([^(u]" ../src/*; \ - then echo "*** Error: isdigit without unsigned char cast" && false; \ - fi - @if grep -rnI "isspace([^(u]" ../src/*; \ - then echo "*** Error: isspace without unsigned char cast" && false; \ - fi - -runtests: test - @./test$(EXE) - -valgrind: test - @valgrind --leak-check=full --error-exitcode=1 ./test$(EXE) - -clean: - rm -f test$(EXE) $(OBJ) diff --git a/src/rcheevos/test/libretro.h b/src/rcheevos/test/libretro.h deleted file mode 100644 index 30009f5056..0000000000 --- a/src/rcheevos/test/libretro.h +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Provides copies of structures and constants from - * https://github.com/libretro/RetroArch/blob/master/libretro-common/include/libretro.h - * for unit testing without pulling in the entire libretro project. - */ - -#ifndef LIBRETRO_H__ -#define LIBRETRO_H__ - -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* Regular save RAM. This RAM is usually found on a game cartridge, - * backed up by a battery. - * If save game data is too complex for a single memory buffer, - * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment - * callback can be used. */ -#define RETRO_MEMORY_SAVE_RAM 0 - -/* Some games have a built-in clock to keep track of time. - * This memory is usually just a couple of bytes to keep track of time. - */ -#define RETRO_MEMORY_RTC 1 - -/* System ram lets a frontend peek into a game systems main RAM. */ -#define RETRO_MEMORY_SYSTEM_RAM 2 - -/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */ -#define RETRO_MEMORY_VIDEO_RAM 3 - - -#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ -#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ -#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */ -#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */ -#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */ -#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ -#define RETRO_MEMDESC_ALIGN_4 (2 << 16) -#define RETRO_MEMDESC_ALIGN_8 (3 << 16) -#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ -#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) -#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) -struct retro_memory_descriptor -{ - uint64_t flags; - - /* Pointer to the start of the relevant ROM or RAM chip. - * It's strongly recommended to use 'offset' if possible, rather than - * doing math on the pointer. - * - * If the same byte is mapped my multiple descriptors, their descriptors - * must have the same pointer. - * If 'start' does not point to the first byte in the pointer, put the - * difference in 'offset' instead. - * - * May be NULL if there's nothing usable here (e.g. hardware registers and - * open bus). No flags should be set if the pointer is NULL. - * It's recommended to minimize the number of descriptors if possible, - * but not mandatory. */ - void *ptr; - size_t offset; - - /* This is the location in the emulated address space - * where the mapping starts. */ - size_t start; - - /* Which bits must be same as in 'start' for this mapping to apply. - * The first memory descriptor to claim a certain byte is the one - * that applies. - * A bit which is set in 'start' must also be set in this. - * Can be zero, in which case each byte is assumed mapped exactly once. - * In this case, 'len' must be a power of two. */ - size_t select; - - /* If this is nonzero, the set bits are assumed not connected to the - * memory chip's address pins. */ - size_t disconnect; - - /* This one tells the size of the current memory area. - * If, after start+disconnect are applied, the address is higher than - * this, the highest bit of the address is cleared. - * - * If the address is still too high, the next highest bit is cleared. - * Can be zero, in which case it's assumed to be infinite (as limited - * by 'select' and 'disconnect'). */ - size_t len; - - /* To go from emulated address to physical address, the following - * order applies: - * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */ - - /* The address space name must consist of only a-zA-Z0-9_-, - * should be as short as feasible (maximum length is 8 plus the NUL), - * and may not be any other address space plus one or more 0-9A-F - * at the end. - * However, multiple memory descriptors for the same address space is - * allowed, and the address space name can be empty. NULL is treated - * as empty. - * - * Address space names are case sensitive, but avoid lowercase if possible. - * The same pointer may exist in multiple address spaces. - * - * Examples: - * blank+blank - valid (multiple things may be mapped in the same namespace) - * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace) - * 'A'+'B' - valid (neither is a prefix of each other) - * 'S'+blank - valid ('S' is not in 0-9A-F) - * 'a'+blank - valid ('a' is not in 0-9A-F) - * 'a'+'A' - valid (neither is a prefix of each other) - * 'AR'+blank - valid ('R' is not in 0-9A-F) - * 'ARB'+blank - valid (the B can't be part of the address either, because - * there is no namespace 'AR') - * blank+'B' - not valid, because it's ambigous which address space B1234 - * would refer to. - * The length can't be used for that purpose; the frontend may want - * to append arbitrary data to an address, without a separator. */ - const char *addrspace; - - /* TODO: When finalizing this one, add a description field, which should be - * "WRAM" or something roughly equally long. */ - - /* TODO: When finalizing this one, replace 'select' with 'limit', which tells - * which bits can vary and still refer to the same address (limit = ~select). - * TODO: limit? range? vary? something else? */ - - /* TODO: When finalizing this one, if 'len' is above what 'select' (or - * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len' - * and 'select' != 0, and the mappings don't tell how the system switches the - * banks. */ - - /* TODO: When finalizing this one, fix the 'len' bit removal order. - * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00. - * Algorithm: Take bits highest to lowest, but if it goes above len, clear - * the most recent addition and continue on the next bit. - * TODO: Can the above be optimized? Is "remove the lowest bit set in both - * pointer and 'len'" equivalent? */ - - /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing - * the emulated memory in 32-bit chunks, native endian. But that's nothing - * compared to Darek Mihocka - * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE - * RAM backwards! I'll want to represent both of those, via some flags. - * - * I suspect MAME either didn't think of that idea, or don't want the #ifdef. - * Not sure which, nor do I really care. */ - - /* TODO: Some of those flags are unused and/or don't really make sense. Clean - * them up. */ -}; - -/* The frontend may use the largest value of 'start'+'select' in a - * certain namespace to infer the size of the address space. - * - * If the address space is larger than that, a mapping with .ptr=NULL - * should be at the end of the array, with .select set to all ones for - * as long as the address space is big. - * - * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags): - * SNES WRAM: - * .start=0x7E0000, .len=0x20000 - * (Note that this must be mapped before the ROM in most cases; some of the - * ROM mappers - * try to claim $7E0000, or at least $7E8000.) - * SNES SPC700 RAM: - * .addrspace="S", .len=0x10000 - * SNES WRAM mirrors: - * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000 - * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000 - * SNES WRAM mirrors, alternate equivalent descriptor: - * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF - * (Various similar constructions can be created by combining parts of - * the above two.) - * SNES LoROM (512KB, mirrored a couple of times): - * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024 - * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024 - * SNES HiROM (4MB): - * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024 - * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024 - * SNES ExHiROM (8MB): - * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024 - * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024 - * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024 - * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024 - * Clarify the size of the address space: - * .ptr=NULL, .select=0xFFFFFF - * .len can be implied by .select in many of them, but was included for clarity. - */ - -struct retro_memory_map -{ - const struct retro_memory_descriptor *descriptors; - unsigned num_descriptors; -}; - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/rcheevos/test/rapi/test_rc_api_common.c b/src/rcheevos/test/rapi/test_rc_api_common.c deleted file mode 100644 index d054718247..0000000000 --- a/src/rcheevos/test/rapi/test_rc_api_common.c +++ /dev/null @@ -1,941 +0,0 @@ -#include "../rapi/rc_api_common.h" - -#include "rc_api_runtime.h" /* for rc_fetch_image */ - -#include "../rc_compat.h" - -#include "../test_framework.h" - -#define IMAGEREQUEST_URL "https://media.retroachievements.org" - -static void _assert_json_parse_response(rc_api_response_t* response, rc_json_field_t* field, const char* json, int expected_result) { - int result; - rc_api_server_response_t server_response; - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("Test") - }; - rc_buffer_init(&response->buffer); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = json; - server_response.body_length = strlen(json); - - result = rc_json_parse_server_response(response, &server_response, fields, sizeof(fields)/sizeof(fields[0])); - ASSERT_NUM_EQUALS(result, expected_result); - - ASSERT_NUM_EQUALS(response->succeeded, 1); - ASSERT_PTR_NULL(response->error_message); - - memcpy(field, &fields[2], sizeof(fields[2])); -} -#define assert_json_parse_response(response, field, json, expected_result) ASSERT_HELPER(_assert_json_parse_response(response, field, json, expected_result), "assert_json_parse_operand") - -static void _assert_field_value(rc_json_field_t* field, const char* expected_value) { - char buffer[256]; - - ASSERT_PTR_NOT_NULL(field->value_start); - ASSERT_PTR_NOT_NULL(field->value_end); - ASSERT_NUM_LESS(field->value_end - field->value_start, sizeof(buffer)); - - memcpy(buffer, field->value_start, field->value_end - field->value_start); - buffer[field->value_end - field->value_start] = '\0'; - ASSERT_STR_EQUALS(buffer, expected_value); -} -#define assert_field_value(field, expected_value) ASSERT_HELPER(_assert_field_value(field, expected_value), "assert_field_value") - -static void test_json_parse_response_empty() { - rc_api_response_t response; - rc_json_field_t field; - - assert_json_parse_response(&response, &field, "{}", RC_OK); - - ASSERT_STR_EQUALS(field.name, "Test"); - ASSERT_PTR_NULL(field.value_start); - ASSERT_PTR_NULL(field.value_end); -} - -static void test_json_parse_response_field(const char* json, const char* value) { - rc_api_response_t response; - rc_json_field_t field; - - assert_json_parse_response(&response, &field, json, RC_OK); - - ASSERT_STR_EQUALS(field.name, "Test"); - assert_field_value(&field, value); -} - -static void test_json_parse_response_non_json() { - int result; - rc_api_server_response_t server_response; - rc_api_response_t response; - const char* error_message = "This is an error."; - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("Test") - }; - rc_buffer_init(&response.buffer); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = error_message; - server_response.body_length = strlen(error_message); - - result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); - ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "This is an error."); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_parse_response_non_json_bounded() { - int result; - rc_api_server_response_t server_response; - rc_api_response_t response; - const char* error_message = "This is an error.\r\nShould not be seen"; - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("Test") - }; - rc_buffer_init(&response.buffer); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = error_message; - server_response.body_length = 16; /* "This is an error" (no period, newline, etc) */ - - result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); - ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "This is an error"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_parse_response_451() { - int result; - rc_api_server_response_t server_response; - rc_api_response_t response; - const char* error_message = "" - "Unavailable For Legal Reasons" - "" - "

Unavailable For Legal Reasons

" - "

This request may not be serviced in the Roman Province" - "of Judea due to the Lex Julia Majestatis, which disallows" - "access to resources hosted on servers deemed to be" - "operated by the People's Front of Judea.

" - "" - ""; - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("Test") - }; - rc_buffer_init(&response.buffer); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = error_message; - server_response.body_length = strlen(error_message); - server_response.http_status_code = 451; - - result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); - ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Unavailable For Legal Reasons"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_parse_response_error_from_server() { - int result; - rc_api_server_response_t server_response; - rc_api_response_t response; - const char* json; - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("Test") - }; - rc_buffer_init(&response.buffer); - - json = "{\"Success\":false,\"Error\":\"Oops\"}"; - memset(&server_response, 0, sizeof(server_response)); - server_response.body = json; - server_response.body_length = strlen(json); - - result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields)/sizeof(fields[0])); - ASSERT_NUM_EQUALS(result, RC_OK); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Oops"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_parse_response_incorrect_size() { - int result; - rc_api_server_response_t server_response; - rc_api_response_t response; - const char* json; - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("Test") - }; - rc_buffer_init(&response.buffer); - - json = "{\"Success\":false,\"Error\":\"Oops\"}"; - memset(&server_response, 0, sizeof(server_response)); - server_response.body = json; - server_response.body_length = strlen(json) - 1; - - result = rc_json_parse_server_response(&response, &server_response, fields, sizeof(fields) / sizeof(fields[0])); - ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); - - /* the error message was found before the parser failed */ - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Oops"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_get_string(const char* escaped, const char* expected) { - rc_api_response_t response; - rc_json_field_t field; - char buffer[256]; - const char *value = NULL; - snprintf(buffer, sizeof(buffer), "{\"Test\":\"%s\"}", escaped); - - assert_json_parse_response(&response, &field, buffer, RC_OK); - - ASSERT_TRUE(rc_json_get_string(&value, &response.buffer, &field, "Test")); - ASSERT_PTR_NOT_NULL(value); - ASSERT_STR_EQUALS(value, expected); -} - -static void test_json_get_optional_string() { - rc_api_response_t response; - rc_json_field_t field; - const char *value = NULL; - - assert_json_parse_response(&response, &field, "{\"Test\":\"Value\"}", RC_OK); - - rc_json_get_optional_string(&value, &response, &field, "Test", "Default"); - ASSERT_PTR_NOT_NULL(value); - ASSERT_STR_EQUALS(value, "Value"); - - assert_json_parse_response(&response, &field, "{\"Test2\":\"Value\"}", RC_OK); - - rc_json_get_optional_string(&value, &response, &field, "Test", "Default"); - ASSERT_PTR_NOT_NULL(value); - ASSERT_STR_EQUALS(value, "Default"); -} - -static void test_json_get_required_string() { - rc_api_response_t response; - rc_json_field_t field; - const char *value = NULL; - - assert_json_parse_response(&response, &field, "{\"Test\":\"Value\"}", RC_OK); - - ASSERT_TRUE(rc_json_get_required_string(&value, &response, &field, "Test")); - ASSERT_PTR_NOT_NULL(value); - ASSERT_STR_EQUALS(value, "Value"); - - ASSERT_PTR_NULL(response.error_message); - ASSERT_NUM_EQUALS(response.succeeded, 1); - - assert_json_parse_response(&response, &field, "{\"Test2\":\"Value\"}", RC_OK); - - ASSERT_FALSE(rc_json_get_required_string(&value, &response, &field, "Test")); - ASSERT_PTR_NULL(value); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_get_num(const char* input, int expected) { - rc_api_response_t response; - rc_json_field_t field; - char buffer[64]; - int value = 0; - snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); - - assert_json_parse_response(&response, &field, buffer, RC_OK); - - if (expected) { - ASSERT_TRUE(rc_json_get_num(&value, &field, "Test")); - } - else { - ASSERT_FALSE(rc_json_get_num(&value, &field, "Test")); - } - - ASSERT_NUM_EQUALS(value, expected); -} - -static void test_json_get_optional_num() { - rc_api_response_t response; - rc_json_field_t field; - int value = 0; - - assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); - - rc_json_get_optional_num(&value, &field, "Test", 9999); - ASSERT_NUM_EQUALS(value, 12345678); - - assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); - - rc_json_get_optional_num(&value, &field, "Test", 9999); - ASSERT_NUM_EQUALS(value, 9999); -} - -static void test_json_get_required_num() { - rc_api_response_t response; - rc_json_field_t field; - int value = 0; - - assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); - - ASSERT_TRUE(rc_json_get_required_num(&value, &response, &field, "Test")); - ASSERT_NUM_EQUALS(value, 12345678); - - ASSERT_PTR_NULL(response.error_message); - ASSERT_NUM_EQUALS(response.succeeded, 1); - - assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); - - ASSERT_FALSE(rc_json_get_required_num(&value, &response, &field, "Test")); - ASSERT_NUM_EQUALS(value, 0); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_get_unum(const char* input, uint32_t expected) { - rc_api_response_t response; - rc_json_field_t field; - char buffer[64]; - uint32_t value = 0; - snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); - - assert_json_parse_response(&response, &field, buffer, RC_OK); - - if (expected) { - ASSERT_TRUE(rc_json_get_unum(&value, &field, "Test")); - } - else { - ASSERT_FALSE(rc_json_get_unum(&value, &field, "Test")); - } - - ASSERT_NUM_EQUALS(value, expected); -} - -static void test_json_get_optional_unum() { - rc_api_response_t response; - rc_json_field_t field; - uint32_t value = 0; - - assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); - - rc_json_get_optional_unum(&value, &field, "Test", 9999); - ASSERT_NUM_EQUALS(value, 12345678); - - assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); - - rc_json_get_optional_unum(&value, &field, "Test", 9999); - ASSERT_NUM_EQUALS(value, 9999); -} - -static void test_json_get_required_unum() { - rc_api_response_t response; - rc_json_field_t field; - uint32_t value = 0; - - assert_json_parse_response(&response, &field, "{\"Test\":12345678}", RC_OK); - - ASSERT_TRUE(rc_json_get_required_unum(&value, &response, &field, "Test")); - ASSERT_NUM_EQUALS(value, 12345678); - - ASSERT_PTR_NULL(response.error_message); - ASSERT_NUM_EQUALS(response.succeeded, 1); - - assert_json_parse_response(&response, &field, "{\"Test2\":12345678}", RC_OK); - - ASSERT_FALSE(rc_json_get_required_unum(&value, &response, &field, "Test")); - ASSERT_NUM_EQUALS(value, 0); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_get_float(const char* input, float expected) -{ - rc_api_response_t response; - rc_json_field_t field; - char buffer[64]; - float value = 0.0; - snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); - - assert_json_parse_response(&response, &field, buffer, RC_OK); - - if (expected) - { - ASSERT_TRUE(rc_json_get_float(&value, &field, "Test")); - } - else - { - ASSERT_FALSE(rc_json_get_float(&value, &field, "Test")); - } - - ASSERT_FLOAT_EQUALS(value, expected); -} - -static void test_json_get_optional_float() -{ - rc_api_response_t response; - rc_json_field_t field; - float value = 0.0; - - assert_json_parse_response(&response, &field, "{\"Test\":1.5}", RC_OK); - - rc_json_get_optional_float(&value, &field, "Test", 9999); - ASSERT_FLOAT_EQUALS(value, 1.5); - - assert_json_parse_response(&response, &field, "{\"Test2\":1.5}", RC_OK); - - rc_json_get_optional_float(&value, &field, "Test", 2.5); - ASSERT_FLOAT_EQUALS(value, 2.5); -} - -static void test_json_get_required_float() -{ - rc_api_response_t response; - rc_json_field_t field; - float value = 0.0; - - assert_json_parse_response(&response, &field, "{\"Test\":1.5}", RC_OK); - - ASSERT_TRUE(rc_json_get_required_float(&value, &response, &field, "Test")); - ASSERT_FLOAT_EQUALS(value, 1.5f); - - ASSERT_PTR_NULL(response.error_message); - ASSERT_NUM_EQUALS(response.succeeded, 1); - - assert_json_parse_response(&response, &field, "{\"Test2\":1.5}", RC_OK); - - ASSERT_FALSE(rc_json_get_required_float(&value, &response, &field, "Test")); - ASSERT_FLOAT_EQUALS(value, 0.0f); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_get_bool(const char* input, int expected) { - rc_api_response_t response; - rc_json_field_t field; - char buffer[64]; - int value = 2; - snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); - - assert_json_parse_response(&response, &field, buffer, RC_OK); - - if (expected != -1) { - ASSERT_TRUE(rc_json_get_bool(&value, &field, "Test")); - ASSERT_NUM_EQUALS(value, expected); - } - else { - ASSERT_FALSE(rc_json_get_bool(&value, &field, "Test")); - ASSERT_NUM_EQUALS(value, 0); - } -} - -static void test_json_get_optional_bool() { - rc_api_response_t response; - rc_json_field_t field; - int value = 3; - - assert_json_parse_response(&response, &field, "{\"Test\":true}", RC_OK); - - rc_json_get_optional_bool(&value, &field, "Test", 2); - ASSERT_NUM_EQUALS(value, 1); - - assert_json_parse_response(&response, &field, "{\"Test2\":true}", RC_OK); - - rc_json_get_optional_bool(&value, &field, "Test", 2); - ASSERT_NUM_EQUALS(value, 2); -} - -static void test_json_get_required_bool() { - rc_api_response_t response; - rc_json_field_t field; - int value = 3; - - assert_json_parse_response(&response, &field, "{\"Test\":true}", RC_OK); - - ASSERT_TRUE(rc_json_get_required_bool(&value, &response, &field, "Test")); - ASSERT_NUM_EQUALS(value, 1); - - ASSERT_PTR_NULL(response.error_message); - ASSERT_NUM_EQUALS(response.succeeded, 1); - - assert_json_parse_response(&response, &field, "{\"Test2\":True}", RC_OK); - - ASSERT_FALSE(rc_json_get_required_bool(&value, &response, &field, "Test")); - ASSERT_NUM_EQUALS(value, 0); - - ASSERT_PTR_NOT_NULL(response.error_message); - ASSERT_STR_EQUALS(response.error_message, "Test not found in response"); - ASSERT_NUM_EQUALS(response.succeeded, 0); -} - -static void test_json_get_datetime(const char* input, int expected) { - rc_api_response_t response; - rc_json_field_t field; - char buffer[64]; - time_t value = 2; - snprintf(buffer, sizeof(buffer), "{\"Test\":\"%s\"}", input); - - assert_json_parse_response(&response, &field, buffer, RC_OK); - - if (expected != -1) { - ASSERT_TRUE(rc_json_get_datetime(&value, &field, "Test")); - ASSERT_NUM_EQUALS(value, (time_t)expected); - } - else { - ASSERT_FALSE(rc_json_get_datetime(&value, &field, "Test")); - ASSERT_NUM_EQUALS(value, 0); - } -} - -static void test_json_get_unum_array(const char* input, uint32_t expected_count, int expected_result) { - rc_api_response_t response; - rc_json_field_t field; - int result; - uint32_t count = 0xFFFFFFFF; - uint32_t*values; - char buffer[128]; - - snprintf(buffer, sizeof(buffer), "{\"Test\":%s}", input); - assert_json_parse_response(&response, &field, buffer, RC_OK); - - result = rc_json_get_required_unum_array(&values, &count, &response, &field, "Test"); - ASSERT_NUM_EQUALS(result, expected_result); - ASSERT_NUM_EQUALS(count, expected_count); - - rc_buffer_destroy(&response.buffer); -} - -static void test_json_get_unum_array_trailing_comma() { - rc_api_response_t response; - rc_json_field_t field; - - assert_json_parse_response(&response, &field, "{\"Test\":[1,2,3,]}", RC_INVALID_JSON); -} - -static void test_url_build_dorequest_url_default_host() { - rc_api_request_t request; - rc_api_fetch_image_request_t api_params; - - rc_api_url_build_dorequest_url(&request, NULL); - ASSERT_STR_EQUALS(request.url, "https://retroachievements.org/dorequest.php"); - rc_api_destroy_request(&request); - - api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; - api_params.image_name = "12345"; - rc_api_init_fetch_image_request_hosted(&request, &api_params, NULL); - ASSERT_STR_EQUALS(request.url, "https://media.retroachievements.org/Badge/12345.png"); - rc_api_destroy_request(&request); -} - -static void test_url_build_dorequest_url_default_host_nonssl() { - rc_api_request_t request; - rc_api_fetch_image_request_t api_params; - rc_api_host_t host; - - memset(&host, 0, sizeof(host)); - host.host = "http://retroachievements.org"; - - rc_api_url_build_dorequest_url(&request, &host); - ASSERT_STR_EQUALS(request.url, "http://retroachievements.org/dorequest.php"); - rc_api_destroy_request(&request); - - api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; - api_params.image_name = "12345"; - rc_api_init_fetch_image_request_hosted(&request, &api_params, &host); - ASSERT_STR_EQUALS(request.url, "http://media.retroachievements.org/Badge/12345.png"); - rc_api_destroy_request(&request); -} - -static void test_url_build_dorequest_url_custom_host() { - rc_api_request_t request; - rc_api_fetch_image_request_t api_params; - rc_api_host_t host; - - memset(&host, 0, sizeof(host)); - host.host = "http://localhost"; - - rc_api_url_build_dorequest_url(&request, &host); - ASSERT_STR_EQUALS(request.url, "http://localhost/dorequest.php"); - rc_api_destroy_request(&request); - - api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; - api_params.image_name = "12345"; - rc_api_init_fetch_image_request_hosted(&request, &api_params, &host); - ASSERT_STR_EQUALS(request.url, "http://localhost/Badge/12345.png"); - rc_api_destroy_request(&request); -} - -static void test_url_build_dorequest_url_custom_host_no_protocol() { - rc_api_request_t request; - rc_api_fetch_image_request_t api_params; - rc_api_host_t host; - - memset(&host, 0, sizeof(host)); - host.host = "my.host"; - - rc_api_url_build_dorequest_url(&request, &host); - ASSERT_STR_EQUALS(request.url, "http://my.host/dorequest.php"); - rc_api_destroy_request(&request); - - api_params.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; - api_params.image_name = "12345"; - rc_api_init_fetch_image_request_hosted(&request, &api_params, &host); - ASSERT_STR_EQUALS(request.url, "http://my.host/Badge/12345.png"); - rc_api_destroy_request(&request); -} - -static void test_url_builder_append_encoded_str(const char* input, const char* expected) { - rc_api_url_builder_t builder; - rc_buffer_t buffer; - const char* output; - - rc_buffer_init(&buffer); - rc_url_builder_init(&builder, &buffer, 128); - rc_url_builder_append_encoded_str(&builder, input); - output = rc_url_builder_finalize(&builder); - - ASSERT_STR_EQUALS(output, expected); - - rc_buffer_destroy(&buffer); -} - -static void test_url_builder_append_str_param() { - rc_api_url_builder_t builder; - rc_buffer_t buffer; - const char* output; - - rc_buffer_init(&buffer); - rc_url_builder_init(&builder, &buffer, 64); - rc_url_builder_append_str_param(&builder, "a", "Apple"); - rc_url_builder_append_str_param(&builder, "b", "Banana"); - rc_url_builder_append_str_param(&builder, "t", "Test 1"); - output = rc_url_builder_finalize(&builder); - - ASSERT_STR_EQUALS(output, "a=Apple&b=Banana&t=Test+1"); - - rc_buffer_destroy(&buffer); -} - -static void test_url_builder_append_unum_param() { - rc_api_url_builder_t builder; - rc_buffer_t buffer; - const char* output; - - rc_buffer_init(&buffer); - rc_url_builder_init(&builder, &buffer, 32); - rc_url_builder_append_unum_param(&builder, "a", 0); - rc_url_builder_append_unum_param(&builder, "b", 123456); - rc_url_builder_append_unum_param(&builder, "t", (unsigned)-1); - output = rc_url_builder_finalize(&builder); - - ASSERT_STR_EQUALS(output, "a=0&b=123456&t=4294967295"); - - rc_buffer_destroy(&buffer); -} - -static void test_url_builder_append_num_param() { - rc_api_url_builder_t builder; - rc_buffer_t buffer; - const char* output; - - rc_buffer_init(&buffer); - rc_url_builder_init(&builder, &buffer, 32); - rc_url_builder_append_num_param(&builder, "a", 0); - rc_url_builder_append_num_param(&builder, "b", 123456); - rc_url_builder_append_num_param(&builder, "t", -1); - output = rc_url_builder_finalize(&builder); - - ASSERT_STR_EQUALS(output, "a=0&b=123456&t=-1"); - - rc_buffer_destroy(&buffer); -} - -static void test_init_fetch_image_request_game() { - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "0123324"; - fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); - ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Images/0123324.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_achievement() { - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "135764"; - fetch_image_request.image_type = RC_IMAGE_TYPE_ACHIEVEMENT; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); - ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Badge/135764.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_achievement_locked() { - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "135764"; - fetch_image_request.image_type = RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); - ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/Badge/135764_lock.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_user() { - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "Username"; - fetch_image_request.image_type = RC_IMAGE_TYPE_USER; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_OK); - ASSERT_STR_EQUALS(request.url, IMAGEREQUEST_URL "/UserPic/Username.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_unknown() { - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "12345"; - fetch_image_request.image_type = -1; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request(&request, &fetch_image_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_custom_host() -{ - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - rc_api_host_t host; - - memset(&host, 0, sizeof(host)); - host.host = "http://localhost"; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "0123324"; - fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request_hosted(&request, &fetch_image_request, &host), RC_OK); - ASSERT_STR_EQUALS(request.url, "http://localhost/Images/0123324.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_explicit_default_host() -{ - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - rc_api_host_t host; - - memset(&host, 0, sizeof(host)); - host.host = rc_api_default_host(); - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "0123324"; - fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request_hosted(&request, &fetch_image_request, &host), RC_OK); - ASSERT_STR_EQUALS(request.url, "https://media.retroachievements.org/Images/0123324.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_image_request_explicit_nonssl_host() -{ - rc_api_fetch_image_request_t fetch_image_request; - rc_api_request_t request; - rc_api_host_t host; - - memset(&host, 0, sizeof(host)); - host.host = "http://retroachievements.org"; - - memset(&fetch_image_request, 0, sizeof(fetch_image_request)); - fetch_image_request.image_name = "0123324"; - fetch_image_request.image_type = RC_IMAGE_TYPE_GAME; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_image_request_hosted(&request, &fetch_image_request, &host), RC_OK); - ASSERT_STR_EQUALS(request.url, "http://media.retroachievements.org/Images/0123324.png"); - ASSERT_PTR_NULL(request.post_data); - - rc_api_destroy_request(&request); -} - -void test_rapi_common(void) { - TEST_SUITE_BEGIN(); - - /* rc_json_parse_response */ - TEST(test_json_parse_response_empty); - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":\"Test\"}", "\"Test\""); /* string */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":\"Te\\\"st\"}", "\"Te\\\"st\""); /* escaped string */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":12345678}", "12345678"); /* integer */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":+12345678}", "+12345678"); /* positive integer */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":-12345678}", "-12345678"); /* negatvie integer */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":1234.5678}", "1234.5678"); /* decimal */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":+1234.5678}", "+1234.5678"); /* positive decimal */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":-1234.5678}", "-1234.5678"); /* negatvie decimal */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":[1,2,3]}", "[1,2,3]"); /* array */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":{\"Foo\":1}}", "{\"Foo\":1}"); /* object */ - TEST_PARAMS2(test_json_parse_response_field, "{\"Test\":null}", "null"); /* null */ - TEST_PARAMS2(test_json_parse_response_field, "{ \"Test\" : 0 }", "0"); /* ignore whitespace */ - TEST_PARAMS2(test_json_parse_response_field, "{ \"Other\" : 1, \"Test\" : 2 }", "2"); /* preceding field */ - TEST_PARAMS2(test_json_parse_response_field, "{ \"Test\" : 1, \"Other\" : 2 }", "1"); /* trailing field */ - TEST(test_json_parse_response_non_json); - TEST(test_json_parse_response_non_json_bounded); - TEST(test_json_parse_response_451); - TEST(test_json_parse_response_error_from_server); - TEST(test_json_parse_response_incorrect_size); - - /* rc_json_get_string */ - TEST_PARAMS2(test_json_get_string, "", ""); - TEST_PARAMS2(test_json_get_string, "Banana", "Banana"); - TEST_PARAMS2(test_json_get_string, "A \\\"Quoted\\\" String", "A \"Quoted\" String"); - TEST_PARAMS2(test_json_get_string, "This\\r\\nThat", "This\r\nThat"); - TEST_PARAMS2(test_json_get_string, "This\\/That", "This/That"); - TEST_PARAMS2(test_json_get_string, "\\u0065", "e"); - TEST_PARAMS2(test_json_get_string, "\\u00a9", "\xc2\xa9"); - TEST_PARAMS2(test_json_get_string, "\\u2260", "\xe2\x89\xa0"); - TEST_PARAMS2(test_json_get_string, "\\ud83d\\udeb6", "\xf0\x9f\x9a\xb6"); /* surrogate pair */ - TEST_PARAMS2(test_json_get_string, "\\ud83d", "\xef\xbf\xbd"); /* surrogate lead with no tail */ - TEST_PARAMS2(test_json_get_string, "\\udeb6", "\xef\xbf\xbd"); /* surrogate tail with no lead */ - TEST(test_json_get_optional_string); - TEST(test_json_get_required_string); - - /* rc_json_get_num */ - TEST_PARAMS2(test_json_get_num, "Banana", 0); - TEST_PARAMS2(test_json_get_num, "True", 0); - TEST_PARAMS2(test_json_get_num, "2468", 2468); - TEST_PARAMS2(test_json_get_num, "+55", 55); - TEST_PARAMS2(test_json_get_num, "-16", -16); - TEST_PARAMS2(test_json_get_num, "3.14159", 3); - TEST(test_json_get_optional_num); - TEST(test_json_get_required_num); - - /* rc_json_get_unum */ - TEST_PARAMS2(test_json_get_unum, "Banana", 0); - TEST_PARAMS2(test_json_get_unum, "True", 0); - TEST_PARAMS2(test_json_get_unum, "2468", 2468); - TEST_PARAMS2(test_json_get_unum, "+55", 0); - TEST_PARAMS2(test_json_get_unum, "-16", 0); - TEST_PARAMS2(test_json_get_unum, "3.14159", 3); - TEST(test_json_get_optional_unum); - TEST(test_json_get_required_unum); - - /* rc_json_get_num */ - TEST_PARAMS2(test_json_get_float, "Banana", 0.0f); - TEST_PARAMS2(test_json_get_float, "True", 0.0f); - TEST_PARAMS2(test_json_get_float, "2468", 2468.0f); - TEST_PARAMS2(test_json_get_float, "+55", 55.0f); - TEST_PARAMS2(test_json_get_float, "-16", -16.0f); - TEST_PARAMS2(test_json_get_float, "3.14159", 3.14159f); - TEST_PARAMS2(test_json_get_float, "-6.7", -6.7f); - TEST(test_json_get_optional_float); - TEST(test_json_get_required_float); - - /* rc_json_get_bool */ - TEST_PARAMS2(test_json_get_bool, "true", 1); - TEST_PARAMS2(test_json_get_bool, "false", 0); - TEST_PARAMS2(test_json_get_bool, "TRUE", 1); - TEST_PARAMS2(test_json_get_bool, "True", 1); - TEST_PARAMS2(test_json_get_bool, "Banana", -1); - TEST_PARAMS2(test_json_get_bool, "1", 1); - TEST_PARAMS2(test_json_get_bool, "0", 0); - TEST(test_json_get_optional_bool); - TEST(test_json_get_required_bool); - - /* rc_json_get_datetime */ - TEST_PARAMS2(test_json_get_datetime, "", -1); - TEST_PARAMS2(test_json_get_datetime, "2015-01-01 08:15:00", 1420100100); - TEST_PARAMS2(test_json_get_datetime, "2016-02-29 20:01:47", 1456776107); - - /* rc_json_get_unum_array */ - TEST_PARAMS3(test_json_get_unum_array, "[]", 0, RC_OK); - TEST_PARAMS3(test_json_get_unum_array, "1", 0, RC_MISSING_VALUE); - TEST_PARAMS3(test_json_get_unum_array, "[1]", 1, RC_OK); - TEST_PARAMS3(test_json_get_unum_array, "[ 1 ]", 1, RC_OK); - TEST_PARAMS3(test_json_get_unum_array, "[1,2,3,4]", 4, RC_OK); - TEST_PARAMS3(test_json_get_unum_array, "[ 1 , 2 ]", 2, RC_OK); - TEST_PARAMS3(test_json_get_unum_array, "[1,1,1]", 3, RC_OK); - TEST_PARAMS3(test_json_get_unum_array, "[A,B,C]", 3, RC_MISSING_VALUE); - TEST(test_json_get_unum_array_trailing_comma); - - /* rc_api_url_build_dorequest_url / rc_api_url_build_dorequest_url_hosted */ - TEST(test_url_build_dorequest_url_default_host); - TEST(test_url_build_dorequest_url_default_host_nonssl); - TEST(test_url_build_dorequest_url_custom_host); - TEST(test_url_build_dorequest_url_custom_host_no_protocol); - - /* rc_api_url_builder_append_encoded_str */ - TEST_PARAMS2(test_url_builder_append_encoded_str, "", ""); - TEST_PARAMS2(test_url_builder_append_encoded_str, "Apple", "Apple"); - TEST_PARAMS2(test_url_builder_append_encoded_str, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"); - TEST_PARAMS2(test_url_builder_append_encoded_str, "Test 1", "Test+1"); - TEST_PARAMS2(test_url_builder_append_encoded_str, "Test+1", "Test%2b1"); - TEST_PARAMS2(test_url_builder_append_encoded_str, "Test%1", "Test%251"); - TEST_PARAMS2(test_url_builder_append_encoded_str, "%Test%", "%25Test%25"); - TEST_PARAMS2(test_url_builder_append_encoded_str, "%%", "%25%25"); - - /* rc_api_url_builder_append_param */ - TEST(test_url_builder_append_str_param); - TEST(test_url_builder_append_num_param); - TEST(test_url_builder_append_unum_param); - - /* fetch_image */ - TEST(test_init_fetch_image_request_game); - TEST(test_init_fetch_image_request_achievement); - TEST(test_init_fetch_image_request_achievement_locked); - TEST(test_init_fetch_image_request_user); - TEST(test_init_fetch_image_request_unknown); - TEST(test_init_fetch_image_request_custom_host); - TEST(test_init_fetch_image_request_explicit_default_host); - TEST(test_init_fetch_image_request_explicit_nonssl_host); - - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rapi/test_rc_api_editor.c b/src/rcheevos/test/rapi/test_rc_api_editor.c deleted file mode 100644 index e01adc6d59..0000000000 --- a/src/rcheevos/test/rapi/test_rc_api_editor.c +++ /dev/null @@ -1,931 +0,0 @@ -#include "rc_api_editor.h" -#include "rc_api_runtime.h" - -#include "../src/rapi/rc_api_common.h" -#include "../test_framework.h" -#include "../rc_compat.h" -#include "rc_consoles.h" - -#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" - -static void test_init_fetch_code_notes_request() -{ - rc_api_fetch_code_notes_request_t fetch_code_notes_request; - rc_api_request_t request; - - memset(&fetch_code_notes_request, 0, sizeof(fetch_code_notes_request)); - fetch_code_notes_request.game_id = 1234; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_code_notes_request(&request, &fetch_code_notes_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=codenotes2&g=1234"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_code_notes_request_no_game_id() -{ - rc_api_fetch_code_notes_request_t fetch_code_notes_request; - rc_api_request_t request; - - memset(&fetch_code_notes_request, 0, sizeof(fetch_code_notes_request)); - - ASSERT_NUM_EQUALS(rc_api_init_fetch_code_notes_request(&request, &fetch_code_notes_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_code_notes_response_empty_array() -{ - rc_api_fetch_code_notes_response_t fetch_code_notes_response; - const char* server_response = "{\"Success\":true,\"CodeNotes\":[]}"; - memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); - ASSERT_PTR_NULL(fetch_code_notes_response.notes); - ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 0); - - rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); -} - -static void test_init_fetch_code_notes_response_one_item() -{ - rc_api_fetch_code_notes_response_t fetch_code_notes_response; - const char* server_response = "{\"Success\":true,\"CodeNotes\":[" - "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}" - "]}"; - memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); - ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); - - rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); -} - -static void test_init_fetch_code_notes_response_several_items() -{ - rc_api_fetch_code_notes_response_t fetch_code_notes_response; - const char* server_response = "{\"Success\":true,\"CodeNotes\":[" - "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}," - "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," - "{\"User\":\"User2\",\"Address\":\"0x002002\",\"Note\":\"Sad\"}," - "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"Banana\\n0=a\"}" - "]}"; - memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 4); - ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[1].address, 0x2000); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].note, "Happy"); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[2].address, 0x2002); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].author, "User2"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].note, "Sad"); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[3].address, 0x2ABC); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].note, "Banana\n0=a"); - - rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); -} - -static void test_init_fetch_code_notes_response_deleted_items() -{ - rc_api_fetch_code_notes_response_t fetch_code_notes_response; - const char* server_response = "{\"Success\":true,\"CodeNotes\":[" - "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"\"}," - "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," - "{\"User\":\"User2\",\"Address\":\"0x002002\",\"Note\":\"''\"}," - "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"\"}" - "]}"; - memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); - ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x2000); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "Happy"); - - rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); -} - -static void test_init_fetch_code_notes_response_null_user() -{ - rc_api_fetch_code_notes_response_t fetch_code_notes_response; - const char* server_response = "{\"Success\":true,\"CodeNotes\":[" - "{\"User\":\"User\",\"Address\":\"0x001234\",\"Note\":\"01=true\"}," - "{\"User\":\"User\",\"Address\":\"0x002000\",\"Note\":\"Happy\"}," - "{\"User\":null,\"Address\":\"0x002002\",\"Note\":\"Sad\"}," - "{\"User\":\"User\",\"Address\":\"0x002ABC\",\"Note\":\"Banana\\n0=a\"}" - "]}"; - memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 4); - ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x1234); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[1].address, 0x2000); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[1].note, "Happy"); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[2].address, 0x2002); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].author, ""); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[2].note, "Sad"); - - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[3].address, 0x2ABC); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[3].note, "Banana\n0=a"); - - rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); -} - -static void test_init_fetch_code_notes_response_unsigned() -{ - rc_api_fetch_code_notes_response_t fetch_code_notes_response; - const char* server_response = "{\"Success\":true,\"CodeNotes\":[" - "{\"User\":\"User\",\"Address\":\"0x98765432\",\"Note\":\"01=true\"}" - "]}"; - memset(&fetch_code_notes_response, 0, sizeof(fetch_code_notes_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_code_notes_response(&fetch_code_notes_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_code_notes_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_code_notes_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_code_notes_response.num_notes, 1); - ASSERT_PTR_NOT_NULL(fetch_code_notes_response.notes); - ASSERT_NUM_EQUALS(fetch_code_notes_response.notes[0].address, 0x98765432); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].author, "User"); - ASSERT_STR_EQUALS(fetch_code_notes_response.notes[0].note, "01=true"); - - rc_api_destroy_fetch_code_notes_response(&fetch_code_notes_response); -} - -static void test_init_update_code_note_request() -{ - rc_api_update_code_note_request_t update_code_note_request; - rc_api_request_t request; - - memset(&update_code_note_request, 0, sizeof(update_code_note_request)); - update_code_note_request.username = "Dev"; - update_code_note_request.api_token = "API_TOKEN"; - update_code_note_request.game_id = 1234; - update_code_note_request.address = 0x1C00; - update_code_note_request.note = "flags\n1=first\n2=second"; - - ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168&n=flags%0a1%3dfirst%0a2%3dsecond"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_code_note_request_no_game_id() -{ - rc_api_update_code_note_request_t update_code_note_request; - rc_api_request_t request; - - memset(&update_code_note_request, 0, sizeof(update_code_note_request)); - update_code_note_request.username = "Dev"; - update_code_note_request.api_token = "API_TOKEN"; - update_code_note_request.address = 0x1C00; - update_code_note_request.note = "flags\n1=first\n2=second"; - - ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_update_code_note_request_no_note() -{ - rc_api_update_code_note_request_t update_code_note_request; - rc_api_request_t request; - - memset(&update_code_note_request, 0, sizeof(update_code_note_request)); - update_code_note_request.username = "Dev"; - update_code_note_request.api_token = "API_TOKEN"; - update_code_note_request.game_id = 1234; - update_code_note_request.address = 0x1C00; - update_code_note_request.note = NULL; - - ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_code_note_request_empty_note() -{ - rc_api_update_code_note_request_t update_code_note_request; - rc_api_request_t request; - - memset(&update_code_note_request, 0, sizeof(update_code_note_request)); - update_code_note_request.username = "Dev"; - update_code_note_request.api_token = "API_TOKEN"; - update_code_note_request.game_id = 1234; - update_code_note_request.address = 0x1C00; - update_code_note_request.note = ""; - - ASSERT_NUM_EQUALS(rc_api_init_update_code_note_request(&request, &update_code_note_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitcodenote&u=Dev&t=API_TOKEN&g=1234&m=7168"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_code_note_response() -{ - rc_api_update_code_note_response_t update_code_note_response; - const char* server_response = "{\"Success\":true,\"GameID\":1234,\"Address\":7168,\"Note\":\"test\"}"; - memset(&update_code_note_response, 0, sizeof(update_code_note_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_code_note_response(&update_code_note_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_code_note_response.response.succeeded, 1); - ASSERT_PTR_NULL(update_code_note_response.response.error_message); - - rc_api_destroy_update_code_note_response(&update_code_note_response); -} - -static void test_init_update_code_note_response_invalid_credentials() -{ - rc_api_update_code_note_response_t update_code_note_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - memset(&update_code_note_response, 0, sizeof(update_code_note_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_code_note_response(&update_code_note_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_code_note_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_code_note_response.response.error_message, "Credentials invalid (0)"); - - rc_api_destroy_update_code_note_response(&update_code_note_response); -} - -static void test_init_update_achievement_request() -{ - rc_api_update_achievement_request_t update_achievement_request; - rc_api_request_t request; - - memset(&update_achievement_request, 0, sizeof(update_achievement_request)); - update_achievement_request.username = "Dev"; - update_achievement_request.api_token = "API_TOKEN"; - update_achievement_request.game_id = 1234; - update_achievement_request.achievement_id = 5555; - update_achievement_request.title = "Title"; - update_achievement_request.description = "Description"; - update_achievement_request.badge = "123456"; - update_achievement_request.trigger = "0xH1234=1"; - update_achievement_request.points = 5; - update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; - - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=&h=7cd9d3f0bfdf84734968353b5a430cfd"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_achievement_request_new() -{ - rc_api_update_achievement_request_t update_achievement_request; - rc_api_request_t request; - - memset(&update_achievement_request, 0, sizeof(update_achievement_request)); - update_achievement_request.username = "Dev"; - update_achievement_request.api_token = "API_TOKEN"; - update_achievement_request.game_id = 1234; - update_achievement_request.title = "Title"; - update_achievement_request.description = "Description"; - update_achievement_request.badge = "123456"; - update_achievement_request.trigger = "0xH1234=1"; - update_achievement_request.points = 5; - update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL; - - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=5&b=123456&x=&h=10dd1fd6e0201f634b1b7536d4860ccb"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_achievement_request_no_game_id() -{ - rc_api_update_achievement_request_t update_achievement_request; - rc_api_request_t request; - - memset(&update_achievement_request, 0, sizeof(update_achievement_request)); - update_achievement_request.username = "Dev"; - update_achievement_request.api_token = "API_TOKEN"; - update_achievement_request.achievement_id = 5555; - update_achievement_request.title = "Title"; - update_achievement_request.description = "Description"; - update_achievement_request.badge = "123456"; - update_achievement_request.trigger = "0xH1234=1"; - update_achievement_request.points = 5; - update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; - - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_update_achievement_request_type() -{ - rc_api_update_achievement_request_t update_achievement_request; - rc_api_request_t request; - - memset(&update_achievement_request, 0, sizeof(update_achievement_request)); - update_achievement_request.username = "Dev"; - update_achievement_request.api_token = "API_TOKEN"; - update_achievement_request.game_id = 1234; - update_achievement_request.achievement_id = 5555; - update_achievement_request.title = "Title"; - update_achievement_request.description = "Description"; - update_achievement_request.badge = "123456"; - update_achievement_request.trigger = "0xH1234=1"; - update_achievement_request.points = 5; - update_achievement_request.category = RC_ACHIEVEMENT_CATEGORY_CORE; - update_achievement_request.type = RC_ACHIEVEMENT_TYPE_MISSABLE; - - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=missable&h=7cd9d3f0bfdf84734968353b5a430cfd"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - update_achievement_request.type = RC_ACHIEVEMENT_TYPE_PROGRESSION; - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=progression&h=7cd9d3f0bfdf84734968353b5a430cfd"); - - update_achievement_request.type = RC_ACHIEVEMENT_TYPE_WIN; - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=win_condition&h=7cd9d3f0bfdf84734968353b5a430cfd"); - - update_achievement_request.type = RC_ACHIEVEMENT_TYPE_STANDARD; - ASSERT_NUM_EQUALS(rc_api_init_update_achievement_request(&request, &update_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.post_data, "r=uploadachievement&u=Dev&t=API_TOKEN&a=5555&g=1234&n=Title&d=Description&m=0xH1234%3d1&z=5&f=3&b=123456&x=&h=7cd9d3f0bfdf84734968353b5a430cfd"); - - rc_api_destroy_request(&request); -} - -static void test_init_update_achievement_response() -{ - rc_api_update_achievement_response_t update_achievement_response; - const char* server_response = "{\"Success\":true,\"AchievementID\":1234}"; - memset(&update_achievement_response, 0, sizeof(update_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 1); - ASSERT_PTR_NULL(update_achievement_response.response.error_message); - ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 1234); - - rc_api_destroy_update_achievement_response(&update_achievement_response); -} - -static void test_init_update_achievement_response_invalid_credentials() -{ - rc_api_update_achievement_response_t update_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - memset(&update_achievement_response, 0, sizeof(update_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "Credentials invalid (0)"); - ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); - - rc_api_destroy_update_achievement_response(&update_achievement_response); -} - -static void test_init_update_achievement_response_invalid_perms() -{ - rc_api_update_achievement_response_t update_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; - memset(&update_achievement_response, 0, sizeof(update_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); - ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); - - rc_api_destroy_update_achievement_response(&update_achievement_response); -} - -static void test_init_update_leaderboard_request() -{ - rc_api_update_leaderboard_request_t update_leaderboard_request; - rc_api_request_t request; - - memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); - update_leaderboard_request.username = "Dev"; - update_leaderboard_request.api_token = "API_TOKEN"; - update_leaderboard_request.game_id = 1234; - update_leaderboard_request.leaderboard_id = 5555; - update_leaderboard_request.title = "Title"; - update_leaderboard_request.description = "Description"; - update_leaderboard_request.start_trigger = "0xH1234=1"; - update_leaderboard_request.submit_trigger = "0xH1234=2"; - update_leaderboard_request.cancel_trigger = "0xH1234=3"; - update_leaderboard_request.value_definition = "0xH2345"; - update_leaderboard_request.lower_is_better = 1; - update_leaderboard_request.format = "SCORE"; - - ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_leaderboard_request_new() -{ - rc_api_update_leaderboard_request_t update_leaderboard_request; - rc_api_request_t request; - - memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); - update_leaderboard_request.username = "Dev"; - update_leaderboard_request.api_token = "API_TOKEN"; - update_leaderboard_request.game_id = 1234; - update_leaderboard_request.title = "Title"; - update_leaderboard_request.description = "Description"; - update_leaderboard_request.start_trigger = "0xH1234=1"; - update_leaderboard_request.submit_trigger = "0xH1234=2"; - update_leaderboard_request.cancel_trigger = "0xH1234=3"; - update_leaderboard_request.value_definition = "0xH2345"; - update_leaderboard_request.lower_is_better = 1; - update_leaderboard_request.format = "SCORE"; - - ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=739e28608a9e93d7351103d2f43fc6dc"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_leaderboard_request_no_game_id() -{ - rc_api_update_leaderboard_request_t update_leaderboard_request; - rc_api_request_t request; - - memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); - update_leaderboard_request.username = "Dev"; - update_leaderboard_request.api_token = "API_TOKEN"; - update_leaderboard_request.title = "Title"; - update_leaderboard_request.description = "Description"; - update_leaderboard_request.start_trigger = "0xH1234=1"; - update_leaderboard_request.submit_trigger = "0xH1234=2"; - update_leaderboard_request.cancel_trigger = "0xH1234=3"; - update_leaderboard_request.value_definition = "0xH2345"; - update_leaderboard_request.lower_is_better = 1; - update_leaderboard_request.format = "SCORE"; - - ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_update_leaderboard_request_no_description() -{ - rc_api_update_leaderboard_request_t update_leaderboard_request; - rc_api_request_t request; - - memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); - update_leaderboard_request.username = "Dev"; - update_leaderboard_request.api_token = "API_TOKEN"; - update_leaderboard_request.game_id = 1234; - update_leaderboard_request.leaderboard_id = 5555; - update_leaderboard_request.title = "Title"; - update_leaderboard_request.description = ""; - update_leaderboard_request.start_trigger = "0xH1234=1"; - update_leaderboard_request.submit_trigger = "0xH1234=2"; - update_leaderboard_request.cancel_trigger = "0xH1234=3"; - update_leaderboard_request.value_definition = "0xH2345"; - update_leaderboard_request.lower_is_better = 1; - update_leaderboard_request.format = "SCORE"; - - ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_leaderboard_response() -{ - rc_api_update_leaderboard_response_t update_leaderboard_response; - const char* server_response = "{\"Success\":true,\"LeaderboardID\":1234}"; - memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 1); - ASSERT_PTR_NULL(update_leaderboard_response.response.error_message); - ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 1234); - - rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); -} - -static void test_init_update_leaderboard_response_invalid_credentials() -{ - rc_api_update_leaderboard_response_t update_leaderboard_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "Credentials invalid (0)"); - ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); - - rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); -} - -static void test_init_update_leaderboard_response_invalid_perms() -{ - rc_api_update_leaderboard_response_t update_leaderboard_response; - const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; - memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); - - ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); - ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); - - rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); -} - -static void test_init_update_rich_presence_request() -{ - rc_api_update_rich_presence_request_t update_rich_presence_request; - rc_api_request_t request; - - memset(&update_rich_presence_request, 0, sizeof(update_rich_presence_request)); - update_rich_presence_request.username = "Dev"; - update_rich_presence_request.api_token = "API_TOKEN"; - update_rich_presence_request.game_id = 1234; - update_rich_presence_request.script = "Display:\nThis is a test."; - - ASSERT_NUM_EQUALS(rc_api_init_update_rich_presence_request(&request, &update_rich_presence_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitrichpresence&u=Dev&t=API_TOKEN&g=1234&d=Display%3a%0aThis+is+a+test."); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_update_rich_presence_request_no_game_id() -{ - rc_api_update_rich_presence_request_t update_rich_presence_request; - rc_api_request_t request; - - memset(&update_rich_presence_request, 0, sizeof(update_rich_presence_request)); - update_rich_presence_request.username = "Dev"; - update_rich_presence_request.api_token = "API_TOKEN"; - update_rich_presence_request.script = "Display:\nThis is a test."; - - ASSERT_NUM_EQUALS(rc_api_init_update_rich_presence_request(&request, &update_rich_presence_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_update_rich_presence_request_no_script() -{ - rc_api_update_rich_presence_request_t update_rich_presence_request; - rc_api_request_t request; - - memset(&update_rich_presence_request, 0, sizeof(update_rich_presence_request)); - update_rich_presence_request.username = "Dev"; - update_rich_presence_request.api_token = "API_TOKEN"; - update_rich_presence_request.game_id = 1234; - - ASSERT_NUM_EQUALS(rc_api_init_update_rich_presence_request(&request, &update_rich_presence_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_update_rich_presence_response() -{ - rc_api_server_response_t response_obj; - rc_api_update_rich_presence_response_t update_rich_presence_response; - const char* server_response = "{\"Success\":true}"; - memset(&update_rich_presence_response, 0, sizeof(update_rich_presence_response)); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = strlen(server_response); - - ASSERT_NUM_EQUALS(rc_api_process_update_rich_presence_server_response(&update_rich_presence_response, &response_obj), RC_OK); - ASSERT_NUM_EQUALS(update_rich_presence_response.response.succeeded, 1); - ASSERT_PTR_NULL(update_rich_presence_response.response.error_message); - - rc_api_destroy_update_rich_presence_response(&update_rich_presence_response); -} - -static void test_init_update_rich_presence_response_invalid_credentials() -{ - rc_api_server_response_t response_obj; - rc_api_update_rich_presence_response_t update_rich_presence_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Invalid user/token combination.\",\"Code\":\"invalid_credentials\",\"Status\":401}"; - memset(&update_rich_presence_response, 0, sizeof(update_rich_presence_response)); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = strlen(server_response); - - ASSERT_NUM_EQUALS(rc_api_process_update_rich_presence_server_response(&update_rich_presence_response, &response_obj), RC_INVALID_CREDENTIALS); - ASSERT_NUM_EQUALS(update_rich_presence_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_rich_presence_response.response.error_message, "Invalid user/token combination."); - - rc_api_destroy_update_rich_presence_response(&update_rich_presence_response); -} - -static void test_init_update_rich_presence_response_invalid_perms() -{ - rc_api_server_response_t response_obj; - rc_api_update_rich_presence_response_t update_rich_presence_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Access denied.\",\"Code\":\"access_denied\",\"Status\":400}"; - memset(&update_rich_presence_response, 0, sizeof(update_rich_presence_response)); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = strlen(server_response); - - ASSERT_NUM_EQUALS(rc_api_process_update_rich_presence_server_response(&update_rich_presence_response, &response_obj), RC_ACCESS_DENIED); - ASSERT_NUM_EQUALS(update_rich_presence_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_rich_presence_response.response.error_message, "Access denied."); - - rc_api_destroy_update_rich_presence_response(&update_rich_presence_response); -} - -static void test_init_fetch_badge_range_request() -{ - rc_api_fetch_badge_range_request_t fetch_badge_range_request; - rc_api_request_t request; - - memset(&fetch_badge_range_request, 0, sizeof(fetch_badge_range_request)); - - ASSERT_NUM_EQUALS(rc_api_init_fetch_badge_range_request(&request, &fetch_badge_range_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=badgeiter"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_badge_range_response() -{ - rc_api_fetch_badge_range_response_t fetch_badge_range_response; - const char* server_response = "{\"Success\":true,\"FirstBadge\":12,\"NextBadge\":123456}"; - memset(&fetch_badge_range_response, 0, sizeof(fetch_badge_range_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_badge_range_response(&fetch_badge_range_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_badge_range_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_badge_range_response.response.error_message); - ASSERT_UNUM_EQUALS(fetch_badge_range_response.first_badge_id, 12); - ASSERT_UNUM_EQUALS(fetch_badge_range_response.next_badge_id, 123456); - - rc_api_destroy_fetch_badge_range_response(&fetch_badge_range_response); -} - -static void test_init_add_game_hash_request() -{ - rc_api_add_game_hash_request_t add_game_hash_request; - rc_api_request_t request; - - memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); - add_game_hash_request.username = "Dev"; - add_game_hash_request.api_token = "API_TOKEN"; - add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; - add_game_hash_request.game_id = 1234; - add_game_hash_request.title = "Game Name"; - add_game_hash_request.hash = "NEW_HASH"; - add_game_hash_request.hash_description = "Game Name [No Intro].nes"; - - ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&i=Game+Name&g=1234&d=Game+Name+%5bNo+Intro%5d.nes"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_add_game_hash_request_no_game_id() -{ - rc_api_add_game_hash_request_t add_game_hash_request; - rc_api_request_t request; - - memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); - add_game_hash_request.username = "Dev"; - add_game_hash_request.api_token = "API_TOKEN"; - add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; - add_game_hash_request.title = "Game Name"; - add_game_hash_request.hash = "NEW_HASH"; - add_game_hash_request.hash_description = "Game Name [No Intro].nes"; - - ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&i=Game+Name&d=Game+Name+%5bNo+Intro%5d.nes"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_add_game_hash_request_no_console_id() -{ - rc_api_add_game_hash_request_t add_game_hash_request; - rc_api_request_t request; - - memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); - add_game_hash_request.username = "Dev"; - add_game_hash_request.api_token = "API_TOKEN"; - add_game_hash_request.game_id = 1234; - add_game_hash_request.title = "Game Name"; - add_game_hash_request.hash = "NEW_HASH"; - add_game_hash_request.hash_description = "Game Name [No Intro].nes"; - - ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_add_game_hash_request_no_title() -{ - rc_api_add_game_hash_request_t add_game_hash_request; - rc_api_request_t request; - - memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); - add_game_hash_request.username = "Dev"; - add_game_hash_request.api_token = "API_TOKEN"; - add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; - add_game_hash_request.game_id = 1234; - add_game_hash_request.hash = "NEW_HASH"; - add_game_hash_request.hash_description = "Game Name [No Intro].nes"; - - /* title is not required when a game id is provided (at least at the client - * level - the server will generate an error, but that could change) */ - ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitgametitle&u=Dev&t=API_TOKEN&c=7&m=NEW_HASH&g=1234&d=Game+Name+%5bNo+Intro%5d.nes"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_add_game_hash_request_no_title_or_game_id() -{ - rc_api_add_game_hash_request_t add_game_hash_request; - rc_api_request_t request; - - memset(&add_game_hash_request, 0, sizeof(add_game_hash_request)); - add_game_hash_request.username = "Dev"; - add_game_hash_request.api_token = "API_TOKEN"; - add_game_hash_request.console_id = RC_CONSOLE_NINTENDO; - add_game_hash_request.hash = "NEW_HASH"; - add_game_hash_request.hash_description = "Game Name [No Intro].nes"; - - ASSERT_NUM_EQUALS(rc_api_init_add_game_hash_request(&request, &add_game_hash_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_add_game_hash_response() -{ - rc_api_add_game_hash_response_t add_game_hash_response; - const char* server_response = "{\"Success\":true,\"Response\":{\"GameID\":1234}}"; - memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); - - ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 1); - ASSERT_PTR_NULL(add_game_hash_response.response.error_message); - ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 1234); - - rc_api_destroy_add_game_hash_response(&add_game_hash_response); -} - -static void test_init_add_game_hash_response_error() -{ - rc_api_add_game_hash_response_t add_game_hash_response; - const char* server_response = "{\"Success\":false,\"Error\":\"The ROM you are trying to load is not in the database.\"}"; - memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); - - ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 0); - ASSERT_STR_EQUALS(add_game_hash_response.response.error_message, "The ROM you are trying to load is not in the database."); - ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 0); - - rc_api_destroy_add_game_hash_response(&add_game_hash_response); -} - -static void test_init_add_game_hash_response_invalid_credentials() -{ - rc_api_add_game_hash_response_t add_game_hash_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - memset(&add_game_hash_response, 0, sizeof(add_game_hash_response)); - - ASSERT_NUM_EQUALS(rc_api_process_add_game_hash_response(&add_game_hash_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(add_game_hash_response.response.succeeded, 0); - ASSERT_STR_EQUALS(add_game_hash_response.response.error_message, "Credentials invalid (0)"); - ASSERT_UNUM_EQUALS(add_game_hash_response.game_id, 0); - - rc_api_destroy_add_game_hash_response(&add_game_hash_response); -} - -void test_rapi_editor(void) { - TEST_SUITE_BEGIN(); - - /* fetch code notes */ - TEST(test_init_fetch_code_notes_request); - TEST(test_init_fetch_code_notes_request_no_game_id); - - TEST(test_init_fetch_code_notes_response_empty_array); - TEST(test_init_fetch_code_notes_response_one_item); - TEST(test_init_fetch_code_notes_response_several_items); - TEST(test_init_fetch_code_notes_response_deleted_items); - TEST(test_init_fetch_code_notes_response_null_user); - TEST(test_init_fetch_code_notes_response_unsigned); - - /* update code note */ - TEST(test_init_update_code_note_request); - TEST(test_init_update_code_note_request_no_game_id); - TEST(test_init_update_code_note_request_no_note); - TEST(test_init_update_code_note_request_empty_note); - - TEST(test_init_update_code_note_response); - TEST(test_init_update_code_note_response_invalid_credentials); - - /* update achievement */ - TEST(test_init_update_achievement_request); - TEST(test_init_update_achievement_request_new); - TEST(test_init_update_achievement_request_no_game_id); - TEST(test_init_update_achievement_request_type); - - TEST(test_init_update_achievement_response); - TEST(test_init_update_achievement_response_invalid_credentials); - TEST(test_init_update_achievement_response_invalid_perms); - - /* update leaderboard */ - TEST(test_init_update_leaderboard_request); - TEST(test_init_update_leaderboard_request_new); - TEST(test_init_update_leaderboard_request_no_game_id); - TEST(test_init_update_leaderboard_request_no_description); - - TEST(test_init_update_leaderboard_response); - TEST(test_init_update_leaderboard_response_invalid_credentials); - TEST(test_init_update_leaderboard_response_invalid_perms); - - /* update rich presence */ - TEST(test_init_update_rich_presence_request); - TEST(test_init_update_rich_presence_request_no_game_id); - TEST(test_init_update_rich_presence_request_no_script); - - TEST(test_init_update_rich_presence_response); - TEST(test_init_update_rich_presence_response_invalid_credentials); - TEST(test_init_update_rich_presence_response_invalid_perms); - - /* fetch badge range */ - TEST(test_init_fetch_badge_range_request); - - TEST(test_init_fetch_badge_range_response); - - /* add game hash */ - TEST(test_init_add_game_hash_request); - TEST(test_init_add_game_hash_request_no_game_id); - TEST(test_init_add_game_hash_request_no_console_id); - TEST(test_init_add_game_hash_request_no_title); - TEST(test_init_add_game_hash_request_no_title_or_game_id); - - TEST(test_init_add_game_hash_response); - TEST(test_init_add_game_hash_response_error); - TEST(test_init_add_game_hash_response_invalid_credentials); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rapi/test_rc_api_info.c b/src/rcheevos/test/rapi/test_rc_api_info.c deleted file mode 100644 index 82b4628541..0000000000 --- a/src/rcheevos/test/rapi/test_rc_api_info.c +++ /dev/null @@ -1,537 +0,0 @@ -#include "rc_api_info.h" - -#include "../src/rapi/rc_api_common.h" -#include "../test_framework.h" -#include "../rc_compat.h" - -#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" - -static void test_init_fetch_achievement_info_request() { - rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; - rc_api_request_t request; - - memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); - fetch_achievement_info_request.username = "Username"; - fetch_achievement_info_request.api_token = "API_TOKEN"; - fetch_achievement_info_request.achievement_id = 1234; - fetch_achievement_info_request.first_entry = 100; - fetch_achievement_info_request.count = 50; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&o=99&c=50"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_achievement_info_request_no_first() { - rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; - rc_api_request_t request; - - memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); - fetch_achievement_info_request.username = "Username"; - fetch_achievement_info_request.api_token = "API_TOKEN"; - fetch_achievement_info_request.achievement_id = 1234; - fetch_achievement_info_request.count = 50; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&c=50"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_achievement_info_request_one_first() { - rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; - rc_api_request_t request; - - memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); - fetch_achievement_info_request.username = "Username"; - fetch_achievement_info_request.api_token = "API_TOKEN"; - fetch_achievement_info_request.achievement_id = 1234; - fetch_achievement_info_request.first_entry = 1; - fetch_achievement_info_request.count = 50; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&c=50"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_achievement_info_request_friends_only() { - rc_api_fetch_achievement_info_request_t fetch_achievement_info_request; - rc_api_request_t request; - - memset(&fetch_achievement_info_request, 0, sizeof(fetch_achievement_info_request)); - fetch_achievement_info_request.username = "Username"; - fetch_achievement_info_request.api_token = "API_TOKEN"; - fetch_achievement_info_request.achievement_id = 1234; - fetch_achievement_info_request.count = 50; - fetch_achievement_info_request.friends_only = 1; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_achievement_info_request(&request, &fetch_achievement_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementwondata&u=Username&t=API_TOKEN&a=1234&f=1&c=50"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_achievement_info_response() { - rc_api_fetch_achievement_info_response_t fetch_achievement_info_response; - rc_api_achievement_awarded_entry_t* entry; - const char* server_response = "{\"Success\":true,\"AchievementID\":1234,\"Response\":{" - "\"NumEarned\":17,\"GameID\":2345,\"TotalPlayers\":25," - "\"RecentWinner\":[{\"User\":\"Player1\",\"DateAwarded\":1615654895,\"AvatarUrl\":\"http://host/UserPic/PLAYER1.png\"}," - "{\"User\":\"Player2\",\"DateAwarded\":1600604303}]" - "}}"; - - memset(&fetch_achievement_info_response, 0, sizeof(fetch_achievement_info_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_achievement_info_response(&fetch_achievement_info_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_achievement_info_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_achievement_info_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_achievement_info_response.id, 1234); - ASSERT_NUM_EQUALS(fetch_achievement_info_response.game_id, 2345); - ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_awarded, 17); - ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_players, 25); - ASSERT_NUM_EQUALS(fetch_achievement_info_response.num_recently_awarded, 2); - - entry = &fetch_achievement_info_response.recently_awarded[0]; - ASSERT_STR_EQUALS(entry->username, "Player1"); - ASSERT_NUM_EQUALS(entry->awarded, 1615654895); - ASSERT_STR_EQUALS(entry->avatar_url, "http://host/UserPic/PLAYER1.png"); - entry = &fetch_achievement_info_response.recently_awarded[1]; - ASSERT_STR_EQUALS(entry->username, "Player2"); - ASSERT_NUM_EQUALS(entry->awarded, 1600604303); - ASSERT_STR_EQUALS(entry->avatar_url, "https://media.retroachievements.org/UserPic/Player2.png"); - - rc_api_destroy_fetch_achievement_info_response(&fetch_achievement_info_response); -} - -static void test_init_fetch_leaderboard_info_request() { - rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; - rc_api_request_t request; - - memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); - fetch_leaderboard_info_request.leaderboard_id = 1234; - fetch_leaderboard_info_request.first_entry = 101; - fetch_leaderboard_info_request.count = 50; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&o=100&c=50"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_leaderboard_info_request_no_first() { - rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; - rc_api_request_t request; - - memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); - fetch_leaderboard_info_request.leaderboard_id = 1234; - fetch_leaderboard_info_request.count = 50; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&c=50"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_leaderboard_info_request_for_user() { - rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; - rc_api_request_t request; - - memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); - fetch_leaderboard_info_request.leaderboard_id = 1234; - fetch_leaderboard_info_request.username = "Username"; - fetch_leaderboard_info_request.count = 20; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&u=Username&c=20"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_leaderboard_info_request_for_user_with_offset() { - rc_api_fetch_leaderboard_info_request_t fetch_leaderboard_info_request; - rc_api_request_t request; - - memset(&fetch_leaderboard_info_request, 0, sizeof(fetch_leaderboard_info_request)); - fetch_leaderboard_info_request.leaderboard_id = 1234; - fetch_leaderboard_info_request.username = "Username"; - fetch_leaderboard_info_request.first_entry = 11; /* should be ignored */ - fetch_leaderboard_info_request.count = 20; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_leaderboard_info_request(&request, &fetch_leaderboard_info_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=lbinfo&i=1234&u=Username&c=20"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_leaderboard_info_response() { - rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; - rc_api_lboard_info_entry_t* entry; - const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":1234,\"GameID\":2345," - "\"LowerIsBetter\":1,\"LBTitle\":\"Title\",\"LBDesc\":\"Description\",\"LBFormat\":\"TIME\"," - "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":null," - "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":12," - "\"Entries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1615654895,\"AvatarUrl\":\"http://host/UserPic/PLAYER1.png\"}," - "{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2,\"Index\":6,\"DateSubmitted\":1600604303}]" - "}}"; - - memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 1234); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2345); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 1); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); - ASSERT_PTR_NULL(fetch_leaderboard_info_response.author); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.total_entries, 12); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1382307141); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1623658699); - - entry = &fetch_leaderboard_info_response.entries[0]; - ASSERT_NUM_EQUALS(entry->rank, 1); - ASSERT_NUM_EQUALS(entry->index, 5); - ASSERT_STR_EQUALS(entry->username, "Player1"); - ASSERT_NUM_EQUALS(entry->score, 8765); - ASSERT_NUM_EQUALS(entry->submitted, 1615654895); - ASSERT_STR_EQUALS(entry->avatar_url, "http://host/UserPic/PLAYER1.png"); - entry = &fetch_leaderboard_info_response.entries[1]; - ASSERT_NUM_EQUALS(entry->rank, 2); - ASSERT_NUM_EQUALS(entry->index, 6); - ASSERT_STR_EQUALS(entry->username, "Player2"); - ASSERT_NUM_EQUALS(entry->score, 7654); - ASSERT_NUM_EQUALS(entry->submitted, 1600604303); - ASSERT_STR_EQUALS(entry->avatar_url, "https://media.retroachievements.org/UserPic/Player2.png"); - - rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); -} - -static void test_process_fetch_leaderboard_info_response2() { - rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; - rc_api_lboard_info_entry_t* entry; - const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":9999,\"GameID\":2222," - "\"LowerIsBetter\":0,\"LBTitle\":\"Title2\",\"LBDesc\":\"Description2\",\"LBFormat\":\"SCORE\"," - "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":\"AuthorName\"," - "\"LBCreated\":\"2021-06-18 15:32:16\",\"LBUpdated\":\"2021-06-18 15:32:16\",\"TotalEntries\":12," - "\"Entries\":[{\"User\":\"Player1\",\"Score\":1013580,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1624055310}," - "{\"User\":\"Player2\",\"Score\":133340,\"Rank\":1,\"Index\":6,\"DateSubmitted\":1624166772}]" - "}}"; - - memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 9999); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2222); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 0); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title2"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description2"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.author, "AuthorName"); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.total_entries, 12); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1624030336); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1624030336); - - entry = &fetch_leaderboard_info_response.entries[0]; - ASSERT_NUM_EQUALS(entry->rank, 1); - ASSERT_NUM_EQUALS(entry->index, 5); - ASSERT_STR_EQUALS(entry->username, "Player1"); - ASSERT_NUM_EQUALS(entry->score, 1013580); - ASSERT_NUM_EQUALS(entry->submitted, 1624055310); - entry = &fetch_leaderboard_info_response.entries[1]; - ASSERT_NUM_EQUALS(entry->rank, 1); - ASSERT_NUM_EQUALS(entry->index, 6); - ASSERT_STR_EQUALS(entry->username, "Player2"); - ASSERT_NUM_EQUALS(entry->score, 133340); - ASSERT_NUM_EQUALS(entry->submitted, 1624166772); - - rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); -} - -static void test_process_fetch_leaderboard_info_response_iso8601() { - rc_api_fetch_leaderboard_info_response_t fetch_leaderboard_info_response; - rc_api_lboard_info_entry_t* entry; - const char* server_response = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":1234,\"GameID\":2345," - "\"LowerIsBetter\":1,\"LBTitle\":\"Title\",\"LBDesc\":\"Description\",\"LBFormat\":\"TIME\"," - "\"LBMem\":\"STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004\",\"LBAuthor\":null," - "\"LBCreated\":\"2013-10-20T22:12:21.000000Z\",\"LBUpdated\":\"2021-06-14T08:18:19.000000Z\",\"TotalEntries\":12," - "\"Entries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1,\"Index\":5,\"DateSubmitted\":1615654895}," - "{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2,\"Index\":6,\"DateSubmitted\":1600604303}]" - "}}"; - - memset(&fetch_leaderboard_info_response, 0, sizeof(fetch_leaderboard_info_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_leaderboard_info_response(&fetch_leaderboard_info_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_leaderboard_info_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.id, 1234); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.game_id, 2345); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.lower_is_better, 1); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.title, "Title"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.description, "Description"); - ASSERT_STR_EQUALS(fetch_leaderboard_info_response.definition, "STA:0xH0000=1::CAN:1=1::SUB:0xH0000=2::VAL:b0x 0004"); - ASSERT_PTR_NULL(fetch_leaderboard_info_response.author); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.num_entries, 2); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.total_entries, 12); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.created, 1382307141); - ASSERT_NUM_EQUALS(fetch_leaderboard_info_response.updated, 1623658699); - - entry = &fetch_leaderboard_info_response.entries[0]; - ASSERT_NUM_EQUALS(entry->rank, 1); - ASSERT_NUM_EQUALS(entry->index, 5); - ASSERT_STR_EQUALS(entry->username, "Player1"); - ASSERT_NUM_EQUALS(entry->score, 8765); - ASSERT_NUM_EQUALS(entry->submitted, 1615654895); - entry = &fetch_leaderboard_info_response.entries[1]; - ASSERT_NUM_EQUALS(entry->rank, 2); - ASSERT_NUM_EQUALS(entry->index, 6); - ASSERT_STR_EQUALS(entry->username, "Player2"); - ASSERT_NUM_EQUALS(entry->score, 7654); - ASSERT_NUM_EQUALS(entry->submitted, 1600604303); - - rc_api_destroy_fetch_leaderboard_info_response(&fetch_leaderboard_info_response); -} - -static void test_init_fetch_games_list_request() { - rc_api_fetch_games_list_request_t fetch_games_list_request; - rc_api_request_t request; - - memset(&fetch_games_list_request, 0, sizeof(fetch_games_list_request)); - fetch_games_list_request.console_id = 12; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_games_list_request(&request, &fetch_games_list_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=gameslist&c=12"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_games_list_response() { - rc_api_fetch_games_list_response_t fetch_games_list_response; - rc_api_game_list_entry_t* entry; - const char* server_response = "{\"Success\":true,\"Response\":{" - "\"1234\":\"Game Name 1\"," - "\"17\":\"Game Name 2\"," - "\"9923\":\"Game Name 3\"," - "\"12303\":\"Game Name 4\"," - "\"4338\":\"Game Name 5\"," - "\"5437\":\"Game Name 6\"" - "}}"; - - memset(&fetch_games_list_response, 0, sizeof(fetch_games_list_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_games_list_response(&fetch_games_list_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_games_list_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_games_list_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_games_list_response.num_entries, 6); - - entry = &fetch_games_list_response.entries[0]; - ASSERT_NUM_EQUALS(entry->id, 1234); - ASSERT_STR_EQUALS(entry->name, "Game Name 1"); - entry = &fetch_games_list_response.entries[1]; - ASSERT_NUM_EQUALS(entry->id, 17); - ASSERT_STR_EQUALS(entry->name, "Game Name 2"); - entry = &fetch_games_list_response.entries[2]; - ASSERT_NUM_EQUALS(entry->id, 9923); - ASSERT_STR_EQUALS(entry->name, "Game Name 3"); - entry = &fetch_games_list_response.entries[3]; - ASSERT_NUM_EQUALS(entry->id, 12303); - ASSERT_STR_EQUALS(entry->name, "Game Name 4"); - entry = &fetch_games_list_response.entries[4]; - ASSERT_NUM_EQUALS(entry->id, 4338); - ASSERT_STR_EQUALS(entry->name, "Game Name 5"); - entry = &fetch_games_list_response.entries[5]; - ASSERT_NUM_EQUALS(entry->id, 5437); - ASSERT_STR_EQUALS(entry->name, "Game Name 6"); - - rc_api_destroy_fetch_games_list_response(&fetch_games_list_response); -} - -static void test_init_fetch_game_titles_request() { - rc_api_fetch_game_titles_request_t fetch_game_titles_request; - rc_api_request_t request; - uint32_t game_ids[] = { 3, 4, 7, 10 }; - - memset(&fetch_game_titles_request, 0, sizeof(fetch_game_titles_request)); - fetch_game_titles_request.game_ids = game_ids; - fetch_game_titles_request.num_game_ids = 3; /* purposefully only ask for 3/4 */ - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_titles_request(&request, &fetch_game_titles_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=gameinfolist&g=3,4,7"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_game_titles_response() { - rc_api_fetch_game_titles_response_t fetch_game_titles_response; - rc_api_server_response_t fetch_game_titles_server_response; - rc_api_game_title_entry_t* entry; - const char* server_response = "{\"Success\":true,\"Response\":[" - "{\"ID\": 3, \"Title\":\"Game Name 3\", \"ImageIcon\": \"/Images/010003.png\"}," - "{\"ID\": 4, \"Title\":\"Game Name 4\", \"ImageIcon\": \"/Images/010004.png\"}," - "{\"ID\": 7, \"Title\":\"Game Name 7\", \"ImageIcon\": \"/Images/010007.png\"}" - "]}"; - - memset(&fetch_game_titles_response, 0, sizeof(fetch_game_titles_response)); - memset(&fetch_game_titles_server_response, 0, sizeof(fetch_game_titles_server_response)); - fetch_game_titles_server_response.body = server_response; - fetch_game_titles_server_response.body_length = strlen(server_response); - fetch_game_titles_server_response.http_status_code = 200; - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_titles_server_response(&fetch_game_titles_response, &fetch_game_titles_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_titles_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_titles_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_titles_response.num_entries, 3); - - entry = &fetch_game_titles_response.entries[0]; - ASSERT_NUM_EQUALS(entry->id, 3); - ASSERT_STR_EQUALS(entry->title, "Game Name 3"); - ASSERT_STR_EQUALS(entry->image_name, "010003"); - entry = &fetch_game_titles_response.entries[1]; - ASSERT_NUM_EQUALS(entry->id, 4); - ASSERT_STR_EQUALS(entry->title, "Game Name 4"); - ASSERT_STR_EQUALS(entry->image_name, "010004"); - entry = &fetch_game_titles_response.entries[2]; - ASSERT_NUM_EQUALS(entry->id, 7); - ASSERT_STR_EQUALS(entry->title, "Game Name 7"); - ASSERT_STR_EQUALS(entry->image_name, "010007"); - - rc_api_destroy_fetch_game_titles_response(&fetch_game_titles_response); -} - -static void test_init_fetch_hash_library_request() { - rc_api_fetch_hash_library_request_t fetch_hash_library_request; - rc_api_request_t request; - - memset(&fetch_hash_library_request, 0, sizeof(fetch_hash_library_request)); - fetch_hash_library_request.console_id = 1; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_hash_library_request(&request, &fetch_hash_library_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=hashlibrary&c=1"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_hash_library_response() { - rc_api_fetch_hash_library_response_t fetch_hash_library_response; - rc_api_server_response_t fetch_hash_library_server_response; - rc_api_hash_library_entry_t* entry; - const char* server_response = "{\"Success\":true,\"MD5List\":{" - "\"aabbccddeeff00112233445566778899\":1," - "\"99aabbccddeeff001122334455667788\":1," - "\"8899aabbccddeeff0011223344556677\":2" - "}}"; - - memset(&fetch_hash_library_server_response, 0, sizeof(fetch_hash_library_server_response)); - fetch_hash_library_server_response.body = server_response; - fetch_hash_library_server_response.body_length = strlen(server_response); - fetch_hash_library_server_response.http_status_code = 200; - - memset(&fetch_hash_library_response, 0, sizeof(fetch_hash_library_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_hash_library_server_response(&fetch_hash_library_response, &fetch_hash_library_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_hash_library_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_hash_library_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_hash_library_response.num_entries, 3); - - entry = &fetch_hash_library_response.entries[0]; - ASSERT_NUM_EQUALS(entry->game_id, 1); - ASSERT_STR_EQUALS(entry->hash, "aabbccddeeff00112233445566778899"); - entry = &fetch_hash_library_response.entries[1]; - ASSERT_NUM_EQUALS(entry->game_id, 1); - ASSERT_STR_EQUALS(entry->hash, "99aabbccddeeff001122334455667788"); - entry = &fetch_hash_library_response.entries[2]; - ASSERT_NUM_EQUALS(entry->game_id, 2); - ASSERT_STR_EQUALS(entry->hash, "8899aabbccddeeff0011223344556677"); - - rc_api_destroy_fetch_hash_library_response(&fetch_hash_library_response); -} - -static void test_process_fetch_hash_library_empty_response() { - rc_api_fetch_hash_library_response_t fetch_hash_library_response; - rc_api_server_response_t fetch_hash_library_server_response; - const char* server_response = "{\"Success\":true,\"MD5List\":[]}"; /* RAWeb returns a list instead of a map for invalid console */ - - memset(&fetch_hash_library_server_response, 0, sizeof(fetch_hash_library_server_response)); - fetch_hash_library_server_response.body = server_response; - fetch_hash_library_server_response.body_length = strlen(server_response); - fetch_hash_library_server_response.http_status_code = 200; - - memset(&fetch_hash_library_response, 0, sizeof(fetch_hash_library_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_hash_library_server_response(&fetch_hash_library_response, &fetch_hash_library_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_hash_library_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_hash_library_response.response.error_message); - ASSERT_PTR_NULL(fetch_hash_library_response.entries); - ASSERT_NUM_EQUALS(fetch_hash_library_response.num_entries, 0); - - rc_api_destroy_fetch_hash_library_response(&fetch_hash_library_response); -} - -void test_rapi_info(void) { - TEST_SUITE_BEGIN(); - - /* achievement info */ - TEST(test_init_fetch_achievement_info_request); - TEST(test_init_fetch_achievement_info_request_no_first); - TEST(test_init_fetch_achievement_info_request_one_first); - TEST(test_init_fetch_achievement_info_request_friends_only); - - TEST(test_process_fetch_achievement_info_response); - - /* leaderboard info */ - TEST(test_init_fetch_leaderboard_info_request); - TEST(test_init_fetch_leaderboard_info_request_no_first); - TEST(test_init_fetch_leaderboard_info_request_for_user); - TEST(test_init_fetch_leaderboard_info_request_for_user_with_offset); - - TEST(test_process_fetch_leaderboard_info_response); - TEST(test_process_fetch_leaderboard_info_response2); - TEST(test_process_fetch_leaderboard_info_response_iso8601); - - /* games list */ - TEST(test_init_fetch_games_list_request); - - TEST(test_process_fetch_games_list_response); - - /* game titles */ - TEST(test_init_fetch_game_titles_request); - - TEST(test_process_fetch_game_titles_response); - - /* hash library */ - TEST(test_init_fetch_hash_library_request); - - TEST(test_process_fetch_hash_library_response); - TEST(test_process_fetch_hash_library_empty_response); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rapi/test_rc_api_runtime.c b/src/rcheevos/test/rapi/test_rc_api_runtime.c deleted file mode 100644 index 64f137a058..0000000000 --- a/src/rcheevos/test/rapi/test_rc_api_runtime.c +++ /dev/null @@ -1,2213 +0,0 @@ -#include "rc_api_runtime.h" - -#include "rc_error.h" -#include "rc_runtime_types.h" - -#include "../src/rapi/rc_api_common.h" -#include "../test_framework.h" - -#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" - -static void init_server_response(rc_api_server_response_t* server_response, - int status_code, const char* body, size_t body_length) { - memset(server_response, 0, sizeof(*server_response)); - server_response->body = body; - server_response->body_length = body_length; - server_response->http_status_code = status_code; -} - -/* ----- resolve hash ----- */ - -static void test_init_resolve_hash_request() { - rc_api_resolve_hash_request_t resolve_hash_request; - rc_api_request_t request; - - memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); - resolve_hash_request.username = "Username"; /* credentials are ignored - turns out server doesn't validate this API */ - resolve_hash_request.api_token = "API_TOKEN"; - resolve_hash_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=gameid&m=ABCDEF0123456789"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_resolve_hash_request_no_credentials() { - rc_api_resolve_hash_request_t resolve_hash_request; - rc_api_request_t request; - - memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); - resolve_hash_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=gameid&m=ABCDEF0123456789"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_resolve_hash_request_no_hash() { - rc_api_resolve_hash_request_t resolve_hash_request; - rc_api_request_t request; - - memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); - - ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_resolve_hash_request_empty_hash() { - rc_api_resolve_hash_request_t resolve_hash_request; - rc_api_request_t request; - - memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); - resolve_hash_request.game_hash = ""; - - ASSERT_NUM_EQUALS(rc_api_init_resolve_hash_request(&request, &resolve_hash_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_process_resolve_hash_response_match() { - rc_api_resolve_hash_response_t resolve_hash_response; - const char* server_response = "{\"Success\":true,\"GameID\":1446}"; - - memset(&resolve_hash_response, 0, sizeof(resolve_hash_response)); - - ASSERT_NUM_EQUALS(rc_api_process_resolve_hash_response(&resolve_hash_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(resolve_hash_response.response.succeeded, 1); - ASSERT_PTR_NULL(resolve_hash_response.response.error_message); - ASSERT_NUM_EQUALS(resolve_hash_response.game_id, 1446); - - rc_api_destroy_resolve_hash_response(&resolve_hash_response); -} - -static void test_process_resolve_hash_response_no_match() { - rc_api_resolve_hash_response_t resolve_hash_response; - const char* server_response = "{\"Success\":true,\"GameID\":0}"; - - memset(&resolve_hash_response, 0, sizeof(resolve_hash_response)); - - ASSERT_NUM_EQUALS(rc_api_process_resolve_hash_response(&resolve_hash_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(resolve_hash_response.response.succeeded, 1); - ASSERT_PTR_NULL(resolve_hash_response.response.error_message); - ASSERT_NUM_EQUALS(resolve_hash_response.game_id, 0); - - rc_api_destroy_resolve_hash_response(&resolve_hash_response); -} - -/* ----- fetch game data ----- */ - -static void test_init_fetch_game_data_request() { - rc_api_fetch_game_data_request_t fetch_game_data_request; - rc_api_request_t request; - - memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); - fetch_game_data_request.username = "Username"; - fetch_game_data_request.api_token = "API_TOKEN"; - fetch_game_data_request.game_id = 1234; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&g=1234"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_game_data_request_by_hash() { - rc_api_fetch_game_data_request_t fetch_game_data_request; - rc_api_request_t request; - - memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); - fetch_game_data_request.username = "Username"; - fetch_game_data_request.api_token = "API_TOKEN"; - fetch_game_data_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&m=ABCDEF0123456789"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_game_data_request_by_id_and_hash() { - rc_api_fetch_game_data_request_t fetch_game_data_request; - rc_api_request_t request; - - memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); - fetch_game_data_request.username = "Username"; - fetch_game_data_request.api_token = "API_TOKEN"; - fetch_game_data_request.game_id = 1234; - fetch_game_data_request.game_hash = "ABCDEF0123456789"; /* ignored when game_id provided */ - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=patch&u=Username&t=API_TOKEN&g=1234"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_game_data_request_no_id() { - rc_api_fetch_game_data_request_t fetch_game_data_request; - rc_api_request_t request; - - memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); - fetch_game_data_request.username = "Username"; - fetch_game_data_request.api_token = "API_TOKEN"; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_game_data_response_empty() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":177,\"Title\":\"My Game\",\"ConsoleID\":23,\"ImageIcon\":\"/Images/012345.png\"," - "\"Achievements\":[],\"Leaderboards\":[]" - "}}"; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_data_response.id, 177); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "My Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 23); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "012345"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_invalid_credentials() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_game_data_response.response.error_message, "Credentials invalid (0)"); - ASSERT_NUM_EQUALS(fetch_game_data_response.id, 0); - ASSERT_PTR_NULL(fetch_game_data_response.title); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 0); - ASSERT_PTR_NULL(fetch_game_data_response.image_name); - ASSERT_PTR_NULL(fetch_game_data_response.rich_presence_script); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_not_found() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Unknown game\",\"Code\":\"not_found\",\"Status\":404}"; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_NOT_FOUND); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_game_data_response.response.error_message, "Unknown game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.id, 0); - ASSERT_PTR_NULL(fetch_game_data_response.title); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 0); - ASSERT_PTR_NULL(fetch_game_data_response.image_name); - ASSERT_PTR_NULL(fetch_game_data_response.rich_presence_script); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_achievements() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," - "\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\"," - "\"Created\":1376969412,\"Modified\":1376969412}," - "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," - "\"Created\":1504474554,\"Modified\":1504474554}" - "],\"Leaderboards\":[]" - "}}"; - rc_api_achievement_definition_t* achievement; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_data_response.id, 20); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); - achievement = fetch_game_data_response.achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_STR_EQUALS(achievement->title, "Ach1"); - ASSERT_STR_EQUALS(achievement->description, "Desc1"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_STR_EQUALS(achievement->title, "Ach2"); - ASSERT_STR_EQUALS(achievement->description, "Desc2"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 2); - ASSERT_STR_EQUALS(achievement->definition, "0=2"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00235"); - ASSERT_NUM_EQUALS(achievement->created, 1376970283); - ASSERT_NUM_EQUALS(achievement->updated, 1376970283); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5503); - ASSERT_STR_EQUALS(achievement->title, "Ach3"); - ASSERT_STR_EQUALS(achievement->description, "Desc3"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); - ASSERT_NUM_EQUALS(achievement->points, 0); - ASSERT_STR_EQUALS(achievement->definition, "0=3"); - ASSERT_STR_EQUALS(achievement->author, "User2"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->created, 1376969412); - ASSERT_NUM_EQUALS(achievement->updated, 1376969412); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5504); - ASSERT_STR_EQUALS(achievement->title, "Ach4"); - ASSERT_STR_EQUALS(achievement->description, "Desc4"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_achievement_types() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Type\":\"\"," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Type\":\"missable\"," - "\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Type\":\"progression\"," - "\"Created\":1376969412,\"Modified\":1376969412}," - "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"win_condition\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5505,\"Title\":\"Ach5 [m]\",\"Description\":\"Desc5\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5506,\"Title\":\"[m] Ach6\",\"Description\":\"Desc6\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}" - "],\"Leaderboards\":[]" - "}}"; - rc_api_achievement_definition_t* achievement; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 7); - - ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); - achievement = fetch_game_data_response.achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_STR_EQUALS(achievement->title, "Ach1"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_STR_EQUALS(achievement->title, "Ach2"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5503); - ASSERT_STR_EQUALS(achievement->title, "Ach3"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_PROGRESSION); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5504); - ASSERT_STR_EQUALS(achievement->title, "Ach4"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_WIN); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5505); - ASSERT_STR_EQUALS(achievement->title, "Ach5"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5506); - ASSERT_STR_EQUALS(achievement->title, "Ach6"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5507); - ASSERT_STR_EQUALS(achievement->title, "Ach7"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_achievement_rarity() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Rarity\":100.0,\"RarityHardcore\":66.67," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Rarity\":57.43,\"RarityHardcore\":57.43," - "\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Rarity\":6.8,\"RarityHardcore\":0," - "\"Created\":1376969412,\"Modified\":1376969412}," - "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\"," - "\"Created\":1376969412,\"Modified\":1376969412}" - "],\"Leaderboards\":[]" - "}}"; - rc_api_achievement_definition_t* achievement; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); - - ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); - achievement = fetch_game_data_response.achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_STR_EQUALS(achievement->title, "Ach1"); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.67f); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_STR_EQUALS(achievement->title, "Ach2"); - ASSERT_FLOAT_EQUALS(achievement->rarity, 57.43f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 57.43f); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5503); - ASSERT_STR_EQUALS(achievement->title, "Ach3"); - ASSERT_FLOAT_EQUALS(achievement->rarity, 6.8f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 0.0f); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5504); - ASSERT_STR_EQUALS(achievement->title, "Ach4"); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_achievement_null_author() -{ - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":20,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"BadgeURL\":\"http://host/Badge/00234.png\",\"BadgeLockedURL\":\"http://host/Badge/00234_lock.png\"," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0=2\",\"Author\":null,\"BadgeName\":\"00235\"," - "\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":null,\"BadgeName\":\"00236\"," - "\"Created\":1376969412,\"Modified\":1376969412}," - "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," - "\"Created\":1504474554,\"Modified\":1504474554}" - "],\"Leaderboards\":[]" - "}}"; - rc_api_achievement_definition_t* achievement; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_data_response.id, 20); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 4); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - ASSERT_PTR_NOT_NULL(fetch_game_data_response.achievements); - achievement = fetch_game_data_response.achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_STR_EQUALS(achievement->title, "Ach1"); - ASSERT_STR_EQUALS(achievement->description, "Desc1"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_STR_EQUALS(achievement->badge_url, "http://host/Badge/00234.png"); - ASSERT_STR_EQUALS(achievement->badge_locked_url, "http://host/Badge/00234_lock.png"); - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_STR_EQUALS(achievement->title, "Ach2"); - ASSERT_STR_EQUALS(achievement->description, "Desc2"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 2); - ASSERT_STR_EQUALS(achievement->definition, "0=2"); - ASSERT_STR_EQUALS(achievement->author, ""); - ASSERT_STR_EQUALS(achievement->badge_name, "00235"); - ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/00235.png"); - ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/00235_lock.png"); - ASSERT_NUM_EQUALS(achievement->created, 1376970283); - ASSERT_NUM_EQUALS(achievement->updated, 1376970283); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5503); - ASSERT_STR_EQUALS(achievement->title, "Ach3"); - ASSERT_STR_EQUALS(achievement->description, "Desc3"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); - ASSERT_NUM_EQUALS(achievement->points, 0); - ASSERT_STR_EQUALS(achievement->definition, "0=3"); - ASSERT_STR_EQUALS(achievement->author, ""); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/00236.png"); - ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/00236_lock.png"); - ASSERT_NUM_EQUALS(achievement->created, 1376969412); - ASSERT_NUM_EQUALS(achievement->updated, 1376969412); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5504); - ASSERT_STR_EQUALS(achievement->title, "Ach4"); - ASSERT_STR_EQUALS(achievement->description, "Desc4"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/00236.png"); - ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/00236_lock.png"); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_leaderboards() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":177,\"Title\":\"Another Amazing Game\",\"ConsoleID\":19,\"ImageIcon\":\"/Images/112233.png\"," - "\"Achievements\":[],\"Leaderboards\":[" - "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," - "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}," - "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," - "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}," - "{\"ID\":4403,\"Title\":\"Leaderboard3\",\"Description\":\"Desc3\"," - "\"Mem\":\"0=1\",\"Format\":\"UNKNOWN\",\"LowerIsBetter\":true,\"Hidden\":false}" - "]}}"; - rc_api_leaderboard_definition_t* leaderboard; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 3); - - ASSERT_PTR_NOT_NULL(fetch_game_data_response.leaderboards); - leaderboard = fetch_game_data_response.leaderboards; - - ASSERT_NUM_EQUALS(leaderboard->id, 4401); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); - ASSERT_NUM_EQUALS(leaderboard->hidden, 0); - - ++leaderboard; - ASSERT_NUM_EQUALS(leaderboard->id, 4402); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); - ASSERT_NUM_EQUALS(leaderboard->hidden, 1); - - ++leaderboard; - ASSERT_NUM_EQUALS(leaderboard->id, 4403); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard3"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc3"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_VALUE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); - ASSERT_NUM_EQUALS(leaderboard->hidden, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_rich_presence() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," - "\"ImageIconURL\":\"http://host/Images/000001.png\"," - "\"Achievements\":[],\"Leaderboards\":[]," - "\"RichPresencePatch\":\"Display:\\r\\nTest\\r\\n\"" - "}}"; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); - ASSERT_STR_EQUALS(fetch_game_data_response.image_url, "http://host/Images/000001.png"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, "Display:\r\nTest\r\n"); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_rich_presence_null() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," - "\"Achievements\":[],\"Leaderboards\":[]," - "\"RichPresencePatch\":null" - "}}"; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); - ASSERT_STR_EQUALS(fetch_game_data_response.image_url, "https://media.retroachievements.org/Images/000001.png"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -static void test_process_fetch_game_data_response_rich_presence_tab() { - rc_api_fetch_game_data_response_t fetch_game_data_response; - const char* server_response = "{\"Success\":true,\"PatchData\":{" - "\"ID\":177,\"Title\":\"Some Other Game\",\"ConsoleID\":2,\"ImageIcon\":\"/Images/000001.png\"," - "\"Achievements\":[],\"Leaderboards\":[]," - "\"RichPresencePatch\":\"Display:\\r\\nTest\\tTab\\r\\n\"" - "}}"; - - memset(&fetch_game_data_response, 0, sizeof(fetch_game_data_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_data_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_data_response.response.error_message); - ASSERT_STR_EQUALS(fetch_game_data_response.title, "Some Other Game"); - ASSERT_NUM_EQUALS(fetch_game_data_response.console_id, 2); - ASSERT_STR_EQUALS(fetch_game_data_response.image_name, "000001"); - ASSERT_STR_EQUALS(fetch_game_data_response.image_url, "https://media.retroachievements.org/Images/000001.png"); - ASSERT_STR_EQUALS(fetch_game_data_response.rich_presence_script, "Display:\r\nTest\tTab\r\n"); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_achievements, 0); - ASSERT_NUM_EQUALS(fetch_game_data_response.num_leaderboards, 0); - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); -} - -/* ----- fetch game sets ----- */ - -static void test_init_fetch_game_sets_request() { - rc_api_fetch_game_sets_request_t fetch_game_sets_request; - rc_api_request_t request; - - memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); - fetch_game_sets_request.username = "Username"; - fetch_game_sets_request.api_token = "API_TOKEN"; - fetch_game_sets_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&m=ABCDEF0123456789"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_game_sets_request_no_hash() { - rc_api_fetch_game_sets_request_t fetch_game_sets_request; - rc_api_request_t request; - - memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); - fetch_game_sets_request.username = "Username"; - fetch_game_sets_request.api_token = "API_TOKEN"; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_game_sets_request_by_id() { - rc_api_fetch_game_sets_request_t fetch_game_sets_request; - rc_api_request_t request; - - memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); - fetch_game_sets_request.username = "Username"; - fetch_game_sets_request.api_token = "API_TOKEN"; - fetch_game_sets_request.game_id = 953; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_game_sets_request_by_hash_and_id() { - rc_api_fetch_game_sets_request_t fetch_game_sets_request; - rc_api_request_t request; - - memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); - fetch_game_sets_request.username = "Username"; - fetch_game_sets_request.api_token = "API_TOKEN"; - fetch_game_sets_request.game_id = 953; - fetch_game_sets_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_fetch_game_sets_response_empty() { - rc_api_achievement_set_definition_t* set; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":177,\"Title\":\"My Game\",\"ConsoleId\":23," - "\"ImageIconUrl\":\"http://server/Images/012345.png\"," - "\"RichPresenceGameId\":177,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":192,\"GameId\":177,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/012345.png\"," - "\"Achievements\":[],\"Leaderboards\":[]" - "}]}"; - - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 177); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "My Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 23); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "012345"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/012345.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 177); - - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 192); - ASSERT_NUM_EQUALS(set->game_id, 177); - ASSERT_STR_EQUALS(set->title, "My Game"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); - ASSERT_STR_EQUALS(set->image_name, "012345"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/012345.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 0); - ASSERT_NUM_EQUALS(set->num_leaderboards, 0); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_invalid_credentials() { - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 403, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_game_sets_response.response.error_message, "Credentials invalid (0)"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 0); - ASSERT_PTR_NULL(fetch_game_sets_response.title); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 0); - ASSERT_PTR_NULL(fetch_game_sets_response.image_name); - ASSERT_PTR_NULL(fetch_game_sets_response.rich_presence_script); - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 0); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_not_found() { - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":false,\"Error\":\"Unknown game\",\"Code\":\"not_found\",\"Status\":404}"; - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 404, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_NOT_FOUND); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_game_sets_response.response.error_message, "Unknown game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 0); - ASSERT_PTR_NULL(fetch_game_sets_response.title); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 0); - ASSERT_PTR_NULL(fetch_game_sets_response.image_name); - ASSERT_PTR_NULL(fetch_game_sets_response.rich_presence_script); - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 0); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_achievements() { - rc_api_achievement_set_definition_t* set; - rc_api_achievement_definition_t* achievement; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":20,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":192,\"GameId\":20,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Type\":\"\"," - "\"Rarity\":100.0,\"RarityHardcore\":66.67,\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Type\":\"missable\"," - "\"Rarity\":57.43,\"RarityHardcore\":57.43,\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Type\":\"progression\"," - "\"Rarity\":6.8,\"RarityHardcore\":0,\"Created\":1376969412,\"Modified\":1376969412}," - "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":null,\"BadgeName\":\"00236\",\"Type\":\"win_condition\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5505,\"Title\":\"Ach5 [m]\",\"Description\":\"Desc5\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5506,\"Title\":\"[m] Ach6\",\"Description\":\"Desc6\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}" - "],\"Leaderboards\":[]" - "}]}"; - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 20); - - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 192); - ASSERT_NUM_EQUALS(set->game_id, 20); - ASSERT_STR_EQUALS(set->title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); - ASSERT_STR_EQUALS(set->image_name, "112233"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112233.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 7); - ASSERT_NUM_EQUALS(set->num_leaderboards, 0); - - ASSERT_PTR_NOT_NULL(set->achievements); - achievement = set->achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_STR_EQUALS(achievement->title, "Ach1"); - ASSERT_STR_EQUALS(achievement->description, "Desc1"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.67f); - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_STR_EQUALS(achievement->title, "Ach2"); - ASSERT_STR_EQUALS(achievement->description, "Desc2"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 2); - ASSERT_STR_EQUALS(achievement->definition, "0=2"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00235"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 57.43f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 57.43f); - ASSERT_NUM_EQUALS(achievement->created, 1376970283); - ASSERT_NUM_EQUALS(achievement->updated, 1376970283); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5503); - ASSERT_STR_EQUALS(achievement->title, "Ach3"); - ASSERT_STR_EQUALS(achievement->description, "Desc3"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); - ASSERT_NUM_EQUALS(achievement->points, 0); - ASSERT_STR_EQUALS(achievement->definition, "0=3"); - ASSERT_STR_EQUALS(achievement->author, "User2"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_PROGRESSION); - ASSERT_FLOAT_EQUALS(achievement->rarity, 6.8f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 0.0f); - ASSERT_NUM_EQUALS(achievement->created, 1376969412); - ASSERT_NUM_EQUALS(achievement->updated, 1376969412); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5504); - ASSERT_STR_EQUALS(achievement->title, "Ach4"); - ASSERT_STR_EQUALS(achievement->description, "Desc4"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, ""); /* null author */ - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_WIN); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5505); - ASSERT_STR_EQUALS(achievement->title, "Ach5"); /* [m] stripped */ - ASSERT_STR_EQUALS(achievement->description, "Desc5"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5506); - ASSERT_STR_EQUALS(achievement->title, "Ach6"); /* [m] stripped */ - ASSERT_STR_EQUALS(achievement->description, "Desc6"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5507); - ASSERT_STR_EQUALS(achievement->title, "Ach7"); - ASSERT_STR_EQUALS(achievement->description, "Desc7"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); /* no type specified */ - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_leaderboards() { - rc_api_achievement_set_definition_t* set; - rc_api_leaderboard_definition_t* leaderboard; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":20,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":192,\"GameId\":20,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[],\"Leaderboards\":[" - "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," - "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}," - "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," - "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}," - "{\"ID\":4403,\"Title\":\"Leaderboard3\",\"Description\":\"Desc3\"," - "\"Mem\":\"0=1\",\"Format\":\"UNKNOWN\",\"LowerIsBetter\":true,\"Hidden\":false}" - "]" - "}]}"; - - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 20); - - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 192); - ASSERT_NUM_EQUALS(set->game_id, 20); - ASSERT_STR_EQUALS(set->title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); - ASSERT_STR_EQUALS(set->image_name, "112233"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112233.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 0); - ASSERT_NUM_EQUALS(set->num_leaderboards, 3); - - ASSERT_PTR_NOT_NULL(set->leaderboards); - leaderboard = set->leaderboards; - - ASSERT_NUM_EQUALS(leaderboard->id, 4401); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); - ASSERT_NUM_EQUALS(leaderboard->hidden, 0); - - ++leaderboard; - ASSERT_NUM_EQUALS(leaderboard->id, 4402); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); - ASSERT_NUM_EQUALS(leaderboard->hidden, 1); - - ++leaderboard; - ASSERT_NUM_EQUALS(leaderboard->id, 4403); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard3"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc3"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_VALUE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); - ASSERT_NUM_EQUALS(leaderboard->hidden, 0); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_rich_presence() { - rc_api_achievement_set_definition_t* set; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":99,\"Title\":\"Some Other Game\",\"ConsoleId\":2," - "\"ImageIconUrl\":\"http://server/Images/000001.png\"," - "\"RichPresenceGameId\":99,\"RichPresencePatch\":\"Display:\\r\\nTest\\r\\n\",\"Sets\":[{" - "\"AchievementSetId\":106,\"GameId\":99,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/000001.png\"," - "\"Achievements\":[],\"Leaderboards\":[]" - "}]}"; - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 99); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Some Other Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 2); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "000001"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/000001.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, "Display:\r\nTest\r\n"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 99); - - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 106); - ASSERT_NUM_EQUALS(set->game_id, 99); - ASSERT_STR_EQUALS(set->title, "Some Other Game"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); - ASSERT_STR_EQUALS(set->image_name, "000001"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/000001.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 0); - ASSERT_NUM_EQUALS(set->num_leaderboards, 0); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_rich_presence_null() { - rc_api_achievement_set_definition_t* set; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":99,\"Title\":\"Some Other Game\",\"ConsoleId\":2," - "\"ImageIconUrl\":\"http://server/Images/000001.png\"," - "\"RichPresenceGameId\":99,\"RichPresencePatch\":null,\"Sets\":[{" - "\"AchievementSetId\":106,\"GameId\":99,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/000001.png\"," - "\"Achievements\":[],\"Leaderboards\":[]" - "}]}"; - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 99); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Some Other Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 2); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "000001"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/000001.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 99); - - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 106); - ASSERT_NUM_EQUALS(set->game_id, 99); - ASSERT_STR_EQUALS(set->title, "Some Other Game"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); - ASSERT_STR_EQUALS(set->image_name, "000001"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/000001.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 0); - ASSERT_NUM_EQUALS(set->num_leaderboards, 0); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_specialty_subset() { - rc_api_achievement_set_definition_t* set; - rc_api_achievement_definition_t* achievement; - rc_api_leaderboard_definition_t* leaderboard; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":20,\"RichPresencePatch\":\"\",\"Sets\":[" - "{" - "\"AchievementSetId\":98,\"GameId\":26,\"Title\":\"Low Level Run\",\"Type\":\"specialty\"," - "\"ImageIconUrl\":\"http://server/Images/112236.png\"," - "\"Achievements\":[" - "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}" - "],\"Leaderboards\":[]" - "},{" - "\"AchievementSetId\":192,\"GameId\":20,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\",\"Type\":\"progression\"," - "\"Rarity\":100.0,\"RarityHardcore\":66.67,\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0=2\",\"Author\":\"User1\",\"BadgeName\":\"00235\",\"Type\":\"missable\"," - "\"Rarity\":57.43,\"RarityHardcore\":57.43,\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":5,\"Points\":0," - "\"MemAddr\":\"0=3\",\"Author\":\"User2\",\"BadgeName\":\"00236\",\"Type\":\"win_condition\"," - "\"Rarity\":6.8,\"RarityHardcore\":0,\"Created\":1376969412,\"Modified\":1376969412}" - "],\"Leaderboards\":[" - "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," - "\"Mem\":\"0=1\",\"Format\":\"SCORE\"}" - "]" - "},{" - "\"AchievementSetId\":77,\"GameId\":21,\"Title\":\"Bonus\",\"Type\":\"bonus\"," - "\"ImageIconUrl\":\"http://server/Images/112236.png\"," - "\"Achievements\":[" - "{\"ID\":5504,\"Title\":\"Ach4\",\"Description\":\"Desc4\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":null,\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5505,\"Title\":\"Ach5 [m]\",\"Description\":\"Desc5\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}," - "{\"ID\":5506,\"Title\":\"[m] Ach6\",\"Description\":\"Desc6\",\"Flags\":3,\"Points\":10," - "\"MemAddr\":\"0=4\",\"Author\":\"User1\",\"BadgeName\":\"00236\",\"Type\":\"\"," - "\"Created\":1504474554,\"Modified\":1504474554}" - "],\"Leaderboards\":[" - "{\"ID\":4402,\"Title\":\"Leaderboard2\",\"Description\":\"Desc2\"," - "\"Mem\":\"0=1\",\"Format\":\"SECS\",\"LowerIsBetter\":false,\"Hidden\":true}" - "]" - "}" - "]}"; - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 20); - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 3); - - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 98); - ASSERT_NUM_EQUALS(set->game_id, 26); - ASSERT_STR_EQUALS(set->title, "Low Level Run"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_SPECIALTY); - ASSERT_STR_EQUALS(set->image_name, "112236"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112236.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 1); - ASSERT_NUM_EQUALS(set->num_leaderboards, 0); - - ASSERT_PTR_NOT_NULL(set->achievements); - achievement = set->achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5507); - ASSERT_STR_EQUALS(achievement->title, "Ach7"); - ASSERT_STR_EQUALS(achievement->description, "Desc7"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); /* no type specified */ - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - set = &fetch_game_sets_response.sets[1]; - ASSERT_NUM_EQUALS(set->id, 192); - ASSERT_NUM_EQUALS(set->game_id, 20); - ASSERT_STR_EQUALS(set->title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_CORE); - ASSERT_STR_EQUALS(set->image_name, "112233"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112233.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 3); - ASSERT_NUM_EQUALS(set->num_leaderboards, 1); - - ASSERT_PTR_NOT_NULL(set->achievements); - achievement = set->achievements; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_STR_EQUALS(achievement->title, "Ach1"); - ASSERT_STR_EQUALS(achievement->description, "Desc1"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_PROGRESSION); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.67f); - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_STR_EQUALS(achievement->title, "Ach2"); - ASSERT_STR_EQUALS(achievement->description, "Desc2"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 2); - ASSERT_STR_EQUALS(achievement->definition, "0=2"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00235"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 57.43f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 57.43f); - ASSERT_NUM_EQUALS(achievement->created, 1376970283); - ASSERT_NUM_EQUALS(achievement->updated, 1376970283); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5503); - ASSERT_STR_EQUALS(achievement->title, "Ach3"); - ASSERT_STR_EQUALS(achievement->description, "Desc3"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL); - ASSERT_NUM_EQUALS(achievement->points, 0); - ASSERT_STR_EQUALS(achievement->definition, "0=3"); - ASSERT_STR_EQUALS(achievement->author, "User2"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_WIN); - ASSERT_FLOAT_EQUALS(achievement->rarity, 6.8f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 0.0f); - ASSERT_NUM_EQUALS(achievement->created, 1376969412); - ASSERT_NUM_EQUALS(achievement->updated, 1376969412); - - ASSERT_PTR_NOT_NULL(set->leaderboards); - leaderboard = set->leaderboards; - - ASSERT_NUM_EQUALS(leaderboard->id, 4401); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard1"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc1"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); - ASSERT_NUM_EQUALS(leaderboard->hidden, 0); - - set = &fetch_game_sets_response.sets[2]; - ASSERT_NUM_EQUALS(set->id, 77); - ASSERT_NUM_EQUALS(set->game_id, 21); - ASSERT_STR_EQUALS(set->title, "Bonus"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_BONUS); - ASSERT_STR_EQUALS(set->image_name, "112236"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112236.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 3); - ASSERT_NUM_EQUALS(set->num_leaderboards, 1); - - ASSERT_PTR_NOT_NULL(set->achievements); - achievement = set->achievements; - ASSERT_NUM_EQUALS(achievement->id, 5504); - ASSERT_STR_EQUALS(achievement->title, "Ach4"); - ASSERT_STR_EQUALS(achievement->description, "Desc4"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, ""); /* null author */ - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5505); - ASSERT_STR_EQUALS(achievement->title, "Ach5"); /* [m] stripped */ - ASSERT_STR_EQUALS(achievement->description, "Desc5"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - ++achievement; - ASSERT_NUM_EQUALS(achievement->id, 5506); - ASSERT_STR_EQUALS(achievement->title, "Ach6"); /* [m] stripped */ - ASSERT_STR_EQUALS(achievement->description, "Desc6"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 10); - ASSERT_STR_EQUALS(achievement->definition, "0=4"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00236"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 100.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 100.0f); - ASSERT_NUM_EQUALS(achievement->created, 1504474554); - ASSERT_NUM_EQUALS(achievement->updated, 1504474554); - - ASSERT_PTR_NOT_NULL(set->leaderboards); - leaderboard = set->leaderboards; - - ASSERT_NUM_EQUALS(leaderboard->id, 4402); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard2"); - ASSERT_STR_EQUALS(leaderboard->description, "Desc2"); - ASSERT_STR_EQUALS(leaderboard->definition, "0=1"); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SECONDS); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 0); - ASSERT_NUM_EQUALS(leaderboard->hidden, 1); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -static void test_process_fetch_game_sets_response_exclusive_subset() { - rc_api_achievement_set_definition_t* set; - rc_api_achievement_definition_t* achievement; - rc_api_fetch_game_sets_response_t fetch_game_sets_response; - const char server_response[] = "{\"Success\":true," - "\"GameId\":20,\"Title\":\"Another Amazing Game\",\"ConsoleId\":19," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":26,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":98,\"GameId\":26,\"Title\":\"Low Level Run\",\"Type\":\"exclusive\"," - "\"ImageIconUrl\":\"http://server/Images/112236.png\"," - "\"Achievements\":[" - "{\"ID\":5507,\"Title\":\"Ach7\",\"Description\":\"Desc7\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0=1\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}" - "],\"Leaderboards\":[]" - "}]}"; - rc_api_server_response_t fetch_game_sets_server_response; - init_server_response(&fetch_game_sets_server_response, 200, server_response, sizeof(server_response) - 1); - - memset(&fetch_game_sets_response, 0, sizeof(fetch_game_sets_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, &fetch_game_sets_server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_game_sets_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_game_sets_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_game_sets_response.id, 20); - ASSERT_STR_EQUALS(fetch_game_sets_response.title, "Another Amazing Game"); - ASSERT_NUM_EQUALS(fetch_game_sets_response.console_id, 19); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_name, "112233"); - ASSERT_STR_EQUALS(fetch_game_sets_response.image_url, "http://server/Images/112233.png"); - ASSERT_STR_EQUALS(fetch_game_sets_response.rich_presence_script, ""); - ASSERT_NUM_EQUALS(fetch_game_sets_response.session_game_id, 26); - ASSERT_NUM_EQUALS(fetch_game_sets_response.num_sets, 1); - - set = &fetch_game_sets_response.sets[0]; - ASSERT_NUM_EQUALS(set->id, 98); - ASSERT_NUM_EQUALS(set->game_id, 26); - ASSERT_STR_EQUALS(set->title, "Low Level Run"); - ASSERT_NUM_EQUALS(set->type, RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE); - ASSERT_STR_EQUALS(set->image_name, "112236"); - ASSERT_STR_EQUALS(set->image_url, "http://server/Images/112236.png"); - ASSERT_NUM_EQUALS(set->num_achievements, 1); - ASSERT_NUM_EQUALS(set->num_leaderboards, 0); - - ASSERT_PTR_NOT_NULL(set->achievements); - achievement = set->achievements; - - ASSERT_NUM_EQUALS(achievement->id, 5507); - ASSERT_STR_EQUALS(achievement->title, "Ach7"); - ASSERT_STR_EQUALS(achievement->description, "Desc7"); - ASSERT_NUM_EQUALS(achievement->category, RC_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_STR_EQUALS(achievement->definition, "0=1"); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_STR_EQUALS(achievement->badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->type, RC_ACHIEVEMENT_TYPE_STANDARD); /* no type specified */ - ASSERT_NUM_EQUALS(achievement->created, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated, 1376929305); - - rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); -} - -/* ----- ping ----- */ - -static void test_init_ping_request() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - ping_request.game_id = 1234; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_ping_request_no_game_id() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_ping_request_rich_presence() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - ping_request.game_id = 1234; - ping_request.rich_presence = "Level 1, 70% complete"; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&m=Level+1%2c+70%25+complete"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_ping_request_rich_presence_unicode() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - ping_request.game_id = 1446; - ping_request.rich_presence = "\xf0\x9f\x9a\xb6:3, 1st Quest"; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1446&m=%f0%9f%9a%b6%3a3%2c+1st+Quest"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_ping_request_rich_presence_empty() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - ping_request.game_id = 1234; - ping_request.rich_presence = ""; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_ping_request_game_hash_softcore() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - ping_request.game_id = 1234; - ping_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&h=0&x=ABCDEF0123456789"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_ping_request_game_hash_hardcore() { - rc_api_ping_request_t ping_request; - rc_api_request_t request; - - memset(&ping_request, 0, sizeof(ping_request)); - ping_request.username = "Username"; - ping_request.api_token = "API_TOKEN"; - ping_request.game_id = 1234; - ping_request.hardcore = 1; - ping_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_ping_request(&request, &ping_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=ping&u=Username&t=API_TOKEN&g=1234&h=1&x=ABCDEF0123456789"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_ping_response() { - rc_api_ping_response_t ping_response; - const char* server_response = "{\"Success\":true}"; - - memset(&ping_response, 0, sizeof(ping_response)); - - ASSERT_NUM_EQUALS(rc_api_process_ping_response(&ping_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(ping_response.response.succeeded, 1); - ASSERT_PTR_NULL(ping_response.response.error_message); - - rc_api_destroy_ping_response(&ping_response); -} - -/* ----- award achievement ----- */ - -static void test_init_award_achievement_request_hardcore() { - rc_api_award_achievement_request_t award_achievement_request; - rc_api_request_t request; - - memset(&award_achievement_request, 0, sizeof(award_achievement_request)); - award_achievement_request.username = "Username"; - award_achievement_request.api_token = "API_TOKEN"; - award_achievement_request.achievement_id = 1234; - award_achievement_request.hardcore = 1; - award_achievement_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=1&m=ABCDEF0123456789&v=b8aefaad6f9659e2164bc60da0c3b64d"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_award_achievement_request_non_hardcore() { - rc_api_award_achievement_request_t award_achievement_request; - rc_api_request_t request; - - memset(&award_achievement_request, 0, sizeof(award_achievement_request)); - award_achievement_request.username = "Username"; - award_achievement_request.api_token = "API_TOKEN"; - award_achievement_request.achievement_id = 1234; - award_achievement_request.hardcore = 0; - award_achievement_request.game_hash = "ABABCBCBDEDEFFFF"; - - ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=0&m=ABABCBCBDEDEFFFF&v=ed81d6ecf825f8cbe3ae1edace098892"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_award_achievement_request_no_hash() { - rc_api_award_achievement_request_t award_achievement_request; - rc_api_request_t request; - - memset(&award_achievement_request, 0, sizeof(award_achievement_request)); - award_achievement_request.username = "Username"; - award_achievement_request.api_token = "API_TOKEN"; - award_achievement_request.achievement_id = 5432; - award_achievement_request.hardcore = 1; - - ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=5432&h=1&v=31048257ab1788386e71ab0c222aa5c8"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_award_achievement_request_no_achievement_id() { - rc_api_award_achievement_request_t award_achievement_request; - rc_api_request_t request; - - memset(&award_achievement_request, 0, sizeof(award_achievement_request)); - award_achievement_request.username = "Username"; - award_achievement_request.api_token = "API_TOKEN"; - award_achievement_request.hardcore = 1; - award_achievement_request.game_hash = "ABABCBCBDEDEFFFF"; - - ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_award_achievement_request_delayed() { - rc_api_award_achievement_request_t award_achievement_request; - rc_api_request_t request; - - memset(&award_achievement_request, 0, sizeof(award_achievement_request)); - award_achievement_request.username = "Username"; - award_achievement_request.api_token = "API_TOKEN"; - award_achievement_request.achievement_id = 1234; - award_achievement_request.hardcore = 1; - award_achievement_request.game_hash = "ABCDEF0123456789"; - award_achievement_request.seconds_since_unlock = 17; - - ASSERT_NUM_EQUALS(rc_api_init_award_achievement_request(&request, &award_achievement_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=awardachievement&u=Username&t=API_TOKEN&a=1234&h=1&m=ABCDEF0123456789&o=17&v=b2326b09d61e9264eb5d3607d947317d"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_award_achievement_response_success() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":true,\"Score\":119102,\"SoftcoreScore\":777,\"AchievementID\":56481,\"AchievementsRemaining\":11}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); - ASSERT_PTR_NULL(award_achievement_response.response.error_message); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119102); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56481); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 11); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_hardcore_already_unlocked() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494,\"AchievementsRemaining\":17}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has hardcore and regular achievements awarded."); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 17); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_non_hardcore_already_unlocked() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"User already has this achievement awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has this achievement awarded."); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_generic_failure() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":false}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_PTR_NULL(award_achievement_response.response.error_message); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_empty() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = ""; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_NO_RESPONSE); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_PTR_NULL(award_achievement_response.response.error_message); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_invalid_credentials() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Credentials invalid (0)"); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_text() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "You do not have access to that resource"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "You do not have access to that resource"); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_no_fields() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":true}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); - ASSERT_PTR_NULL(award_achievement_response.response.error_message); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_429() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = - "\n" - "429 Too Many Requests\n" - "\n" - "

429 Too Many Requests

\n" - "
nginx
\n" - "\n" - ""; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "429 Too Many Requests"); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_429_json() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = - "{\"Success\": false,\"Error\":\"Too Many Attempts\"}"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Too Many Attempts"); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_503_fancy() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = - "\n" - "\n" - "\n" - " \n" - " \n" - " \n" - " 503 Service Temporarily Unavailable\n" - "\n" - "\n" - "
\n" - "\n" - ""; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "503 Service Temporarily Unavailable"); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -static void test_process_award_achievement_response_522_simple() { - rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "error code: 522"; - - memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); - ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "error code: 522"); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); - ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); - - rc_api_destroy_award_achievement_response(&award_achievement_response); -} - -/* ----- submit lboard entry ----- */ - -static void test_init_submit_lboard_entry_request() { - rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; - rc_api_request_t request; - - memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); - submit_lboard_entry_request.username = "Username"; - submit_lboard_entry_request.api_token = "API_TOKEN"; - submit_lboard_entry_request.leaderboard_id = 1234; - submit_lboard_entry_request.score = 10999; - submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1234&s=10999&m=ABCDEF0123456789&v=e13c9132ee651256f9d2ee8f06f75d76"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_submit_lboard_entry_request_zero_value() { - rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; - rc_api_request_t request; - - memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); - submit_lboard_entry_request.username = "Username"; - submit_lboard_entry_request.api_token = "API_TOKEN"; - submit_lboard_entry_request.leaderboard_id = 1111; - submit_lboard_entry_request.score = 0; - submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1111&s=0&m=ABCDEF0123456789&v=9c2ac665157d68b8a26e83bb71dd8aaf"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_submit_lboard_entry_request_negative_value() { - rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; - rc_api_request_t request; - - memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); - submit_lboard_entry_request.username = "Username"; - submit_lboard_entry_request.api_token = "API_TOKEN"; - submit_lboard_entry_request.leaderboard_id = 1111; - submit_lboard_entry_request.score = -234781; - submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1111&s=-234781&m=ABCDEF0123456789&v=fbe290266f2d121a7a37942e1e90f453"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_submit_lboard_entry_request_no_leaderboard_id() { - rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; - rc_api_request_t request; - - memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); - submit_lboard_entry_request.username = "Username"; - submit_lboard_entry_request.api_token = "API_TOKEN"; - submit_lboard_entry_request.score = 12345; - submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_submit_lboard_entry_request_delayed() { - rc_api_submit_lboard_entry_request_t submit_lboard_entry_request; - rc_api_request_t request; - - memset(&submit_lboard_entry_request, 0, sizeof(submit_lboard_entry_request)); - submit_lboard_entry_request.username = "Username"; - submit_lboard_entry_request.api_token = "API_TOKEN"; - submit_lboard_entry_request.leaderboard_id = 1234; - submit_lboard_entry_request.score = 10999; - submit_lboard_entry_request.game_hash = "ABCDEF0123456789"; - submit_lboard_entry_request.seconds_since_completion = 33; - - ASSERT_NUM_EQUALS(rc_api_init_submit_lboard_entry_request(&request, &submit_lboard_entry_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=submitlbentry&u=Username&t=API_TOKEN&i=1234&s=10999&m=ABCDEF0123456789&o=33&v=7971fc37cf38026f99dd4bae84360ac1"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_submit_lb_entry_response_success() { - rc_api_submit_lboard_entry_response_t submit_lb_entry_response; - rc_api_lboard_entry_t* entry; - const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1},{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; - - memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); - - ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 1); - ASSERT_PTR_NULL(submit_lb_entry_response.response.error_message); - ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 1234); - ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 2345); - ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 5); - ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 17); - - ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 2); - entry = &submit_lb_entry_response.top_entries[0]; - ASSERT_NUM_EQUALS(entry->rank, 1); - ASSERT_STR_EQUALS(entry->username, "Player1"); - ASSERT_NUM_EQUALS(entry->score, 8765); - entry = &submit_lb_entry_response.top_entries[1]; - ASSERT_NUM_EQUALS(entry->rank, 2); - ASSERT_STR_EQUALS(entry->username, "Player2"); - ASSERT_NUM_EQUALS(entry->score, 7654); - - rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); -} - -static void test_process_submit_lb_entry_response_no_entries() { - rc_api_submit_lboard_entry_response_t submit_lb_entry_response; - const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," - "\"TopEntries\":[]," - "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; - - memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); - - ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 1); - ASSERT_PTR_NULL(submit_lb_entry_response.response.error_message); - ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 1234); - ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 2345); - ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 5); - ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 17); - - ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 0); - ASSERT_PTR_NULL(submit_lb_entry_response.top_entries); - - rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); -} - -static void test_process_submit_lb_entry_response_invalid_credentials() { - rc_api_submit_lboard_entry_response_t submit_lb_entry_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - - memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); - - ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 0); - ASSERT_STR_EQUALS(submit_lb_entry_response.response.error_message, "Credentials invalid (0)"); - ASSERT_NUM_EQUALS(submit_lb_entry_response.submitted_score, 0); - ASSERT_NUM_EQUALS(submit_lb_entry_response.best_score, 0); - ASSERT_NUM_EQUALS(submit_lb_entry_response.new_rank, 0); - ASSERT_NUM_EQUALS(submit_lb_entry_response.num_entries, 0); - - ASSERT_NUM_EQUALS(submit_lb_entry_response.num_top_entries, 0); - ASSERT_PTR_NULL(submit_lb_entry_response.top_entries); - - rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); -} - -static void test_process_submit_lb_entry_response_entries_not_array() { - rc_api_submit_lboard_entry_response_t submit_lb_entry_response; - const char* server_response = "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," - "\"TopEntries\":{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1}," - "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"; - - memset(&submit_lb_entry_response, 0, sizeof(submit_lb_entry_response)); - - ASSERT_NUM_EQUALS(rc_api_process_submit_lboard_entry_response(&submit_lb_entry_response, server_response), RC_MISSING_VALUE); - ASSERT_NUM_EQUALS(submit_lb_entry_response.response.succeeded, 0); - ASSERT_STR_EQUALS(submit_lb_entry_response.response.error_message, "TopEntries not found in response"); - - rc_api_destroy_submit_lboard_entry_response(&submit_lb_entry_response); -} - -/* ----- harness ----- */ - -void test_rapi_runtime(void) { - TEST_SUITE_BEGIN(); - - /* gameid */ - TEST(test_init_resolve_hash_request); - TEST(test_init_resolve_hash_request_no_credentials); - TEST(test_init_resolve_hash_request_no_hash); - TEST(test_init_resolve_hash_request_empty_hash); - - TEST(test_process_resolve_hash_response_match); - TEST(test_process_resolve_hash_response_no_match); - - /* patch */ - TEST(test_init_fetch_game_data_request); - TEST(test_init_fetch_game_data_request_no_id); - TEST(test_init_fetch_game_data_request_by_hash); - TEST(test_init_fetch_game_data_request_by_id_and_hash); - - TEST(test_process_fetch_game_data_response_empty); - TEST(test_process_fetch_game_data_response_invalid_credentials); - TEST(test_process_fetch_game_data_response_not_found); - TEST(test_process_fetch_game_data_response_achievements); - TEST(test_process_fetch_game_data_response_achievement_types); - TEST(test_process_fetch_game_data_response_achievement_rarity); - TEST(test_process_fetch_game_data_response_achievement_null_author); - TEST(test_process_fetch_game_data_response_leaderboards); - TEST(test_process_fetch_game_data_response_rich_presence); - TEST(test_process_fetch_game_data_response_rich_presence_null); - TEST(test_process_fetch_game_data_response_rich_presence_tab); - - /* hashdata */ - TEST(test_init_fetch_game_sets_request); - TEST(test_init_fetch_game_sets_request_no_hash); - TEST(test_init_fetch_game_sets_request_by_id); - TEST(test_init_fetch_game_sets_request_by_hash_and_id); - - TEST(test_process_fetch_game_sets_response_empty); - TEST(test_process_fetch_game_sets_response_invalid_credentials); - TEST(test_process_fetch_game_sets_response_not_found); - TEST(test_process_fetch_game_sets_response_achievements); - TEST(test_process_fetch_game_sets_response_leaderboards); - TEST(test_process_fetch_game_sets_response_rich_presence); - TEST(test_process_fetch_game_sets_response_rich_presence_null); - TEST(test_process_fetch_game_sets_response_specialty_subset); - TEST(test_process_fetch_game_sets_response_exclusive_subset); - - /* ping */ - TEST(test_init_ping_request); - TEST(test_init_ping_request_no_game_id); - TEST(test_init_ping_request_rich_presence); - TEST(test_init_ping_request_rich_presence_unicode); - TEST(test_init_ping_request_rich_presence_empty); - TEST(test_init_ping_request_game_hash_softcore); - TEST(test_init_ping_request_game_hash_hardcore); - - TEST(test_process_ping_response); - - /* awardachievement */ - TEST(test_init_award_achievement_request_hardcore); - TEST(test_init_award_achievement_request_non_hardcore); - TEST(test_init_award_achievement_request_no_hash); - TEST(test_init_award_achievement_request_no_achievement_id); - TEST(test_init_award_achievement_request_delayed); - - TEST(test_process_award_achievement_response_success); - TEST(test_process_award_achievement_response_hardcore_already_unlocked); - TEST(test_process_award_achievement_response_non_hardcore_already_unlocked); - TEST(test_process_award_achievement_response_generic_failure); - TEST(test_process_award_achievement_response_empty); - TEST(test_process_award_achievement_response_invalid_credentials); - TEST(test_process_award_achievement_response_text); - TEST(test_process_award_achievement_response_no_fields); - TEST(test_process_award_achievement_response_429); - TEST(test_process_award_achievement_response_429_json); - TEST(test_process_award_achievement_response_503_fancy); - TEST(test_process_award_achievement_response_522_simple); - - /* submitlbentry */ - TEST(test_init_submit_lboard_entry_request); - TEST(test_init_submit_lboard_entry_request_zero_value); - TEST(test_init_submit_lboard_entry_request_negative_value); - TEST(test_init_submit_lboard_entry_request_no_leaderboard_id); - TEST(test_init_submit_lboard_entry_request_delayed); - - TEST(test_process_submit_lb_entry_response_success); - TEST(test_process_submit_lb_entry_response_no_entries); - TEST(test_process_submit_lb_entry_response_invalid_credentials); - TEST(test_process_submit_lb_entry_response_entries_not_array); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rapi/test_rc_api_user.c b/src/rcheevos/test/rapi/test_rc_api_user.c deleted file mode 100644 index 9853469985..0000000000 --- a/src/rcheevos/test/rapi/test_rc_api_user.c +++ /dev/null @@ -1,998 +0,0 @@ -#include "rc_api_user.h" - -#include "../src/rapi/rc_api_common.h" -#include "../test_framework.h" -#include "../rc_compat.h" -#include "../rc_version.h" - -#define DOREQUEST_URL "https://retroachievements.org/dorequest.php" - -static void test_init_start_session_request() -{ - rc_api_start_session_request_t start_session_request; - rc_api_request_t request; - - memset(&start_session_request, 0, sizeof(start_session_request)); - start_session_request.username = "Username"; - start_session_request.api_token = "API_TOKEN"; - start_session_request.game_id = 1234; - - ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&l=" RCHEEVOS_VERSION_STRING); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_start_session_request_no_game() -{ - rc_api_start_session_request_t start_session_request; - rc_api_request_t request; - - memset(&start_session_request, 0, sizeof(start_session_request)); - start_session_request.username = "Username"; - start_session_request.api_token = "API_TOKEN"; - - ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_start_session_request_game_hash_softcore() -{ - rc_api_start_session_request_t start_session_request; - rc_api_request_t request; - - memset(&start_session_request, 0, sizeof(start_session_request)); - start_session_request.username = "Username"; - start_session_request.api_token = "API_TOKEN"; - start_session_request.game_id = 1234; - start_session_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&h=0&m=ABCDEF0123456789&l=" RCHEEVOS_VERSION_STRING); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_start_session_request_game_hash_hardcore() -{ - rc_api_start_session_request_t start_session_request; - rc_api_request_t request; - - memset(&start_session_request, 0, sizeof(start_session_request)); - start_session_request.username = "Username"; - start_session_request.api_token = "API_TOKEN"; - start_session_request.game_id = 1234; - start_session_request.hardcore = 1; - start_session_request.game_hash = "ABCDEF0123456789"; - - ASSERT_NUM_EQUALS(rc_api_init_start_session_request(&request, &start_session_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=startsession&u=Username&t=API_TOKEN&g=1234&h=1&m=ABCDEF0123456789&l=" RCHEEVOS_VERSION_STRING); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_process_start_session_response_legacy() -{ - rc_api_start_session_response_t start_session_response; - const char* server_response = "{\"Success\":true}"; - - memset(&start_session_response, 0, sizeof(start_session_response)); - - ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 1); - ASSERT_PTR_NULL(start_session_response.response.error_message); - ASSERT_NUM_EQUALS(start_session_response.num_unlocks, 0); - ASSERT_NUM_EQUALS(start_session_response.num_hardcore_unlocks, 0); - ASSERT_NUM_EQUALS(start_session_response.server_now, 0); - - rc_api_destroy_start_session_response(&start_session_response); -} - -static void test_process_start_session_response() -{ - rc_api_start_session_response_t start_session_response; - /* startsession API only returns HardcoreUnlocks if an achievement has been earned in hardcore, - * even if the softcore unlock has a different timestamp. Unlocks are only returned for things - * only unlocked in softcore. */ - const char* server_response = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":111,\"When\":1234567890}," - "{\"ID\":112,\"When\":1234567891}," - "{\"ID\":113,\"When\":1234567860}" - "],\"Unlocks\":[" - "{\"ID\":114,\"When\":1234567840}" - "],\"ServerNow\":1234577777}"; - - memset(&start_session_response, 0, sizeof(start_session_response)); - - ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 1); - ASSERT_PTR_NULL(start_session_response.response.error_message); - ASSERT_NUM_EQUALS(start_session_response.num_unlocks, 1); - ASSERT_NUM_EQUALS(start_session_response.unlocks[0].achievement_id, 114); - ASSERT_NUM_EQUALS(start_session_response.unlocks[0].when, 1234567840); - ASSERT_NUM_EQUALS(start_session_response.num_hardcore_unlocks, 3); - ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[0].achievement_id, 111); - ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[0].when, 1234567890); - ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[1].achievement_id, 112); - ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[1].when, 1234567891); - ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[2].achievement_id, 113); - ASSERT_NUM_EQUALS(start_session_response.hardcore_unlocks[2].when, 1234567860); - ASSERT_NUM_EQUALS(start_session_response.server_now, 1234577777); - - rc_api_destroy_start_session_response(&start_session_response); -} - -static void test_process_start_session_response_invalid_credentials() -{ - rc_api_start_session_response_t start_session_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - - memset(&start_session_response, 0, sizeof(start_session_response)); - - ASSERT_NUM_EQUALS(rc_api_process_start_session_response(&start_session_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(start_session_response.response.succeeded, 0); - ASSERT_STR_EQUALS(start_session_response.response.error_message, "Credentials invalid (0)"); - - rc_api_destroy_start_session_response(&start_session_response); -} - -static void test_init_login_request_password() -{ - rc_api_login_request_t login_request; - rc_api_request_t request; - - memset(&login_request, 0, sizeof(login_request)); - login_request.username = "Username"; - login_request.password = "Pa$$w0rd!"; - - ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&p=Pa%24%24w0rd%21"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_login_request_password_long() -{ - char buffer[1024], *ptr, *password_start; - rc_api_login_request_t login_request; - rc_api_request_t request; - int i; - - /* this generates a password that's 830 characters long */ - ptr = password_start = buffer + snprintf(buffer, sizeof(buffer), "r=login2&u=ThisUsernameIsAlsoReallyLongAtRoughlyFiftyCharacters&p="); - for (i = 0; i < 30; i++) - ptr += snprintf(ptr, sizeof(buffer) - (ptr - buffer), "%dABCDEFGHIJKLMNOPQRSTUVWXYZ", i); - - memset(&login_request, 0, sizeof(login_request)); - login_request.username = "ThisUsernameIsAlsoReallyLongAtRoughlyFiftyCharacters"; - login_request.password = password_start; - - ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, buffer); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_login_request_token() -{ - rc_api_login_request_t login_request; - rc_api_request_t request; - - memset(&login_request, 0, sizeof(login_request)); - login_request.username = "Username"; - login_request.api_token = "ABCDEFGHIJKLMNOP"; - - ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&t=ABCDEFGHIJKLMNOP"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_login_request_password_and_token() -{ - rc_api_login_request_t login_request; - rc_api_request_t request; - - memset(&login_request, 0, sizeof(login_request)); - login_request.username = "Username"; - login_request.password = "Pa$$w0rd!"; - login_request.api_token = "ABCDEFGHIJKLMNOP"; - - ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&p=Pa%24%24w0rd%21"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_login_request_no_password_or_token() -{ - rc_api_login_request_t login_request; - rc_api_request_t request; - - memset(&login_request, 0, sizeof(login_request)); - login_request.username = "Username"; - - ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_INVALID_STATE); - - rc_api_destroy_request(&request); -} - -static void test_init_login_request_alternate_host() -{ - rc_api_login_request_t login_request; - rc_api_request_t request; - - memset(&login_request, 0, sizeof(login_request)); - login_request.username = "Username"; - login_request.password = "Pa$$w0rd!"; - - rc_api_set_host("localhost"); - ASSERT_NUM_EQUALS(rc_api_init_login_request(&request, &login_request), RC_OK); - ASSERT_STR_EQUALS(request.url, "http://localhost/dorequest.php"); - ASSERT_STR_EQUALS(request.post_data, "r=login2&u=Username&p=Pa%24%24w0rd%21"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_set_host(NULL); - rc_api_destroy_request(&request); -} - -static void test_process_login_response_success() -{ - rc_api_login_response_t login_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_STR_EQUALS(login_response.username, "USER"); - ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); - ASSERT_NUM_EQUALS(login_response.score, 1234); - ASSERT_NUM_EQUALS(login_response.score_softcore, 789); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); - ASSERT_STR_EQUALS(login_response.display_name, "USER"); - - rc_api_destroy_login_response(&login_response); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 200; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_OK); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_STR_EQUALS(login_response.username, "USER"); - ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); - ASSERT_NUM_EQUALS(login_response.score, 1234); - ASSERT_NUM_EQUALS(login_response.score_softcore, 789); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); - ASSERT_STR_EQUALS(login_response.display_name, "USER"); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_unique_display_name() -{ - rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"GamingHero\",\"AvatarUrl\":\"http://host/UserPic/USER.png\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_STR_EQUALS(login_response.username, "GamingHero"); - ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); - ASSERT_NUM_EQUALS(login_response.score, 1234); - ASSERT_NUM_EQUALS(login_response.score_softcore, 789); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); - ASSERT_STR_EQUALS(login_response.display_name, "GamingHero"); - ASSERT_STR_EQUALS(login_response.avatar_url, "http://host/UserPic/USER.png"); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_invalid_credentials() -{ - rc_api_login_response_t login_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\", \"Code\":\"invalid_credentials\"}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_CREDENTIALS); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "Invalid User/Password combination. Please try again"); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 401; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_INVALID_CREDENTIALS); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "Invalid User/Password combination. Please try again"); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_access_denied() -{ - rc_api_login_response_t login_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":false,\"Error\":\"Access denied.\",\"Code\":\"access_denied\"}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_ACCESS_DENIED); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "Access denied."); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 403; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_ACCESS_DENIED); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "Access denied."); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_expired_token() -{ - rc_api_login_response_t login_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":false,\"Error\":\"The access token has expired. Please log in again.\",\"Code\":\"expired_token\"}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_EXPIRED_TOKEN); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "The access token has expired. Please log in again."); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 401; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_server_response(&login_response, &response_obj), RC_EXPIRED_TOKEN); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "The access token has expired. Please log in again."); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_generic_failure() -{ - rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":false}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_empty() -{ - rc_api_login_response_t login_response; - const char* server_response = ""; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_NO_RESPONSE); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_text() -{ - rc_api_login_response_t login_response; - const char* server_response = "You do not have access to that resource"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "You do not have access to that resource"); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_html() -{ - rc_api_login_response_t login_response; - const char* server_response = "You do not have access to that resource"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "You do not have access to that resource"); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_no_required_fields() -{ - rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_MISSING_VALUE); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "User not found in response"); - ASSERT_PTR_NULL(login_response.username); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_no_token() -{ - rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"Username\"}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_MISSING_VALUE); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); - ASSERT_STR_EQUALS(login_response.response.error_message, "Token not found in response"); - ASSERT_STR_EQUALS(login_response.username, "Username"); - ASSERT_PTR_NULL(login_response.api_token); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_PTR_NULL(login_response.display_name); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_no_optional_fields() -{ - rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\"}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_STR_EQUALS(login_response.username, "USER"); - ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_STR_EQUALS(login_response.display_name, "USER"); - - rc_api_destroy_login_response(&login_response); -} - -static void test_process_login_response_null_score() -{ - rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":null,\"SoftcoreScore\":null}"; - - memset(&login_response, 0, sizeof(login_response)); - - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(login_response.response.succeeded, 1); - ASSERT_PTR_NULL(login_response.response.error_message); - ASSERT_STR_EQUALS(login_response.username, "USER"); - ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); - ASSERT_NUM_EQUALS(login_response.score, 0); - ASSERT_NUM_EQUALS(login_response.score_softcore, 0); - ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); - ASSERT_STR_EQUALS(login_response.display_name, "USER"); - - rc_api_destroy_login_response(&login_response); -} - -static void test_init_fetch_user_unlocks_request_non_hardcore() -{ - rc_api_fetch_user_unlocks_request_t fetch_user_unlocks_request; - rc_api_request_t request; - - memset(&fetch_user_unlocks_request, 0, sizeof(fetch_user_unlocks_request)); - fetch_user_unlocks_request.username = "Username"; - fetch_user_unlocks_request.api_token = "API_TOKEN"; - fetch_user_unlocks_request.game_id = 1234; - fetch_user_unlocks_request.hardcore = 0; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_user_unlocks_request(&request, &fetch_user_unlocks_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=unlocks&u=Username&t=API_TOKEN&g=1234&h=0"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_user_unlocks_request_hardcore() -{ - rc_api_fetch_user_unlocks_request_t fetch_user_unlocks_request; - rc_api_request_t request; - - memset(&fetch_user_unlocks_request, 0, sizeof(fetch_user_unlocks_request)); - fetch_user_unlocks_request.username = "Username"; - fetch_user_unlocks_request.api_token = "API_TOKEN"; - fetch_user_unlocks_request.game_id = 2345; - fetch_user_unlocks_request.hardcore = 1; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_user_unlocks_request(&request, &fetch_user_unlocks_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=unlocks&u=Username&t=API_TOKEN&g=2345&h=1"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_user_unlocks_response_empty_array() -{ - rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; - const char* server_response = "{\"Success\":true,\"UserUnlocks\":[],\"GameID\":11277,\"HardcoreMode\":false}"; - memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); - ASSERT_PTR_NULL(fetch_user_unlocks_response.achievement_ids); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 0); - - rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); -} - -static void test_init_fetch_user_unlocks_response_invalid_credentials() -{ - rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_user_unlocks_response.response.error_message, "Credentials invalid (0)"); - ASSERT_PTR_NULL(fetch_user_unlocks_response.achievement_ids); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 0); - - rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); -} - -static void test_init_fetch_user_unlocks_response_one_item() -{ - rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; - const char* server_response = "{\"Success\":true,\"UserUnlocks\":[1234],\"GameID\":11277,\"HardcoreMode\":false}"; - memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 1); - ASSERT_PTR_NOT_NULL(fetch_user_unlocks_response.achievement_ids); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[0], 1234); - - rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); -} - -static void test_init_fetch_user_unlocks_response_several_items() -{ - rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; - const char* server_response = "{\"Success\":true,\"UserUnlocks\":[1,2,3,4],\"GameID\":11277,\"HardcoreMode\":false}"; - memset(&fetch_user_unlocks_response, 0, sizeof(fetch_user_unlocks_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_user_unlocks_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.num_achievement_ids, 4); - ASSERT_PTR_NOT_NULL(fetch_user_unlocks_response.achievement_ids); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[0], 1); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[1], 2); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[2], 3); - ASSERT_NUM_EQUALS(fetch_user_unlocks_response.achievement_ids[3], 4); - - rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); -} - -static void test_init_fetch_followed_users_request() -{ - rc_api_fetch_followed_users_request_t fetch_followed_users_request; - rc_api_request_t request; - - memset(&fetch_followed_users_request, 0, sizeof(fetch_followed_users_request)); - fetch_followed_users_request.username = "Username"; - fetch_followed_users_request.api_token = "API_TOKEN"; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_followed_users_request(&request, &fetch_followed_users_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=getfriendlist&u=Username&t=API_TOKEN"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_followed_users_response_empty_array() -{ - rc_api_fetch_followed_users_response_t fetch_followed_users_response; - rc_api_server_response_t server_response; - memset(&fetch_followed_users_response, 0, sizeof(fetch_followed_users_response)); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = "{\"Success\":true,\"Friends\":[]}"; - server_response.body_length = strlen(server_response.body); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_followed_users_server_response(&fetch_followed_users_response, &server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_followed_users_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_followed_users_response.response.error_message); - ASSERT_PTR_NULL(fetch_followed_users_response.users); - ASSERT_NUM_EQUALS(fetch_followed_users_response.num_users, 0); - - rc_api_destroy_fetch_followed_users_response(&fetch_followed_users_response); -} - -static void test_init_fetch_followed_users_response_invalid_credentials() -{ - rc_api_fetch_followed_users_response_t fetch_followed_users_response; - rc_api_server_response_t server_response; - memset(&fetch_followed_users_response, 0, sizeof(fetch_followed_users_response)); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - server_response.body_length = strlen(server_response.body); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_followed_users_server_response(&fetch_followed_users_response, &server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_followed_users_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_followed_users_response.response.error_message, "Credentials invalid (0)"); - ASSERT_PTR_NULL(fetch_followed_users_response.users); - ASSERT_NUM_EQUALS(fetch_followed_users_response.num_users, 0); - - rc_api_destroy_fetch_followed_users_response(&fetch_followed_users_response); -} - -static void test_init_fetch_followed_users_response_several_items() -{ - rc_api_fetch_followed_users_response_t fetch_followed_users_response; - rc_api_server_response_t server_response; - memset(&fetch_followed_users_response, 0, sizeof(fetch_followed_users_response)); - - memset(&server_response, 0, sizeof(server_response)); - server_response.body = "{\"Success\":true,\"Friends\":[" - "{\"Friend\":\"Bob\",\"AvatarUrl\":\"/User/Bob.png\",\"RAPoints\":1234,\"LastSeen\":\"Doing stuff\"}," /* legacy format */ - "{\"Friend\":\"Jane\",\"AvatarUrl\":\"/User/Jane.png\",\"RAPoints\":5,\"LastSeen\":\"Winning\"," - "\"LastSeenTime\":1234567890,\"LastGameId\":6,\"LastGameTitle\":\"The Game\",\"LastGameIconUrl\":\"/Badges/000006.png\"}," - "{\"Friend\":\"Bill\",\"AvatarUrl\":\"/User/Bill.png\",\"RAPoints\":0,\"LastSeen\":\"Unknown\"," - "\"LastSeenTime\":1234567800,\"LastGameId\":null,\"LastGameTitle\":null,\"LastGameIconUrl\":null}" - "]}"; - server_response.body_length = strlen(server_response.body); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_followed_users_server_response(&fetch_followed_users_response, &server_response), RC_OK); - ASSERT_NUM_EQUALS(fetch_followed_users_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_followed_users_response.response.error_message); - ASSERT_PTR_NOT_NULL(fetch_followed_users_response.users); - ASSERT_NUM_EQUALS(fetch_followed_users_response.num_users, 3); - - ASSERT_STR_EQUALS(fetch_followed_users_response.users[0].display_name, "Bob"); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[0].avatar_url, "/User/Bob.png"); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[0].score, 1234); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[0].recent_activity.description, "Doing stuff"); - ASSERT_PTR_NULL(fetch_followed_users_response.users[0].recent_activity.context); - ASSERT_PTR_NULL(fetch_followed_users_response.users[0].recent_activity.context_image_url); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[0].recent_activity.context_id, 0); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[0].recent_activity.when, 0); - - ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].display_name, "Jane"); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].avatar_url, "/User/Jane.png"); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[1].score, 5); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].recent_activity.description, "Winning"); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].recent_activity.context, "The Game"); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[1].recent_activity.context_image_url, "/Badges/000006.png"); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[1].recent_activity.context_id, 6); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[1].recent_activity.when, 1234567890); - - ASSERT_STR_EQUALS(fetch_followed_users_response.users[2].display_name, "Bill"); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[2].avatar_url, "/User/Bill.png"); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[2].score, 0); - ASSERT_STR_EQUALS(fetch_followed_users_response.users[2].recent_activity.description, "Unknown"); - ASSERT_PTR_NULL(fetch_followed_users_response.users[2].recent_activity.context); - ASSERT_PTR_NULL(fetch_followed_users_response.users[2].recent_activity.context_image_url); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[2].recent_activity.context_id, 0); - ASSERT_NUM_EQUALS(fetch_followed_users_response.users[2].recent_activity.when, 1234567800); - - rc_api_destroy_fetch_followed_users_response(&fetch_followed_users_response); -} - -static void test_init_fetch_all_user_progress_request() -{ - rc_api_fetch_all_user_progress_request_t fetch_all_user_progress_request; - rc_api_request_t request; - - memset(&fetch_all_user_progress_request, 0, sizeof(fetch_all_user_progress_request)); - fetch_all_user_progress_request.username = "Username"; - fetch_all_user_progress_request.api_token = "API_TOKEN"; - fetch_all_user_progress_request.console_id = 1; - - ASSERT_NUM_EQUALS(rc_api_init_fetch_all_user_progress_request(&request, &fetch_all_user_progress_request), RC_OK); - ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=allprogress&u=Username&t=API_TOKEN&c=1"); - ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); - - rc_api_destroy_request(&request); -} - -static void test_init_fetch_all_user_progress_response_empty_array() -{ - rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":true,\"Response\":[]}"; - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 200; - - memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); - - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_all_user_progress_response.response.error_message); - ASSERT_PTR_NULL(fetch_all_user_progress_response.entries); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 0); - - rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); -} - -static void test_init_fetch_all_user_progress_response_invalid_credentials() -{ - rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; - memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 200; - - memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); - - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 0); - ASSERT_STR_EQUALS(fetch_all_user_progress_response.response.error_message, "Credentials invalid (0)"); - ASSERT_PTR_NULL(fetch_all_user_progress_response.entries); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 0); - - rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); -} - -static void test_init_fetch_all_user_progress_response_one_item() -{ - rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; - rc_api_server_response_t response_obj; - const char* server_response = "{\"Success\":true,\"Response\":{\"10\":{\"Achievements\":11}}}"; - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 200; - - memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_all_user_progress_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 1); - ASSERT_PTR_NOT_NULL(fetch_all_user_progress_response.entries); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].game_id, 10); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_achievements, 11); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements, 0); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements_hardcore, 0); - - rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); -} - -static void test_init_fetch_all_user_progress_response_several_items() -{ - rc_api_fetch_all_user_progress_response_t fetch_all_user_progress_response; - rc_api_server_response_t response_obj; - - const char* server_response = "{\"Success\":true,\"Response\":{\"10\":{\"Achievements\":11}," - "\"20\":{\"Achievements\":21,\"Unlocked\":22}," - "\"30\":{\"Achievements\":31,\"Unlocked\":32,\"UnlockedHardcore\":33}," - "\"40\":{\"Achievements\":41,\"UnlockedHardcore\":43}}}"; - - memset(&response_obj, 0, sizeof(response_obj)); - response_obj.body = server_response; - response_obj.body_length = rc_json_get_object_string_length(server_response); - response_obj.http_status_code = 200; - - memset(&fetch_all_user_progress_response, 0, sizeof(fetch_all_user_progress_response)); - - ASSERT_NUM_EQUALS(rc_api_process_fetch_all_user_progress_server_response(&fetch_all_user_progress_response, &response_obj), RC_OK); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.response.succeeded, 1); - ASSERT_PTR_NULL(fetch_all_user_progress_response.response.error_message); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.num_entries, 4); - ASSERT_PTR_NOT_NULL(fetch_all_user_progress_response.entries); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].game_id, 10); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_achievements, 11); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements, 0); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[0].num_unlocked_achievements_hardcore, 0); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].game_id, 20); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].num_achievements, 21); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].num_unlocked_achievements, 22); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[1].num_unlocked_achievements_hardcore, 0); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].game_id, 30); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].num_achievements, 31); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].num_unlocked_achievements, 32); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[2].num_unlocked_achievements_hardcore, 33); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].game_id, 40); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].num_achievements, 41); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].num_unlocked_achievements, 0); - ASSERT_NUM_EQUALS(fetch_all_user_progress_response.entries[3].num_unlocked_achievements_hardcore, 43); - - rc_api_destroy_fetch_all_user_progress_response(&fetch_all_user_progress_response); -} - -void test_rapi_user(void) { - TEST_SUITE_BEGIN(); - - /* start session */ - TEST(test_init_start_session_request); - TEST(test_init_start_session_request_no_game); - TEST(test_init_start_session_request_game_hash_softcore); - TEST(test_init_start_session_request_game_hash_hardcore); - - TEST(test_process_start_session_response_legacy); - TEST(test_process_start_session_response); - TEST(test_process_start_session_response_invalid_credentials); - - /* login */ - TEST(test_init_login_request_password); - TEST(test_init_login_request_password_long); - TEST(test_init_login_request_token); - TEST(test_init_login_request_password_and_token); - TEST(test_init_login_request_no_password_or_token); - TEST(test_init_login_request_alternate_host); - - TEST(test_process_login_response_success); - TEST(test_process_login_response_unique_display_name); - TEST(test_process_login_response_invalid_credentials); - TEST(test_process_login_response_access_denied); - TEST(test_process_login_response_generic_failure); - TEST(test_process_login_response_expired_token); - TEST(test_process_login_response_empty); - TEST(test_process_login_response_text); - TEST(test_process_login_response_html); - TEST(test_process_login_response_no_required_fields); - TEST(test_process_login_response_no_token); - TEST(test_process_login_response_no_optional_fields); - TEST(test_process_login_response_null_score); - - /* unlocks */ - TEST(test_init_fetch_user_unlocks_request_non_hardcore); - TEST(test_init_fetch_user_unlocks_request_hardcore); - - TEST(test_init_fetch_user_unlocks_response_empty_array); - TEST(test_init_fetch_user_unlocks_response_invalid_credentials); - TEST(test_init_fetch_user_unlocks_response_one_item); - TEST(test_init_fetch_user_unlocks_response_several_items); - - /* followed users */ - TEST(test_init_fetch_followed_users_request); - - TEST(test_init_fetch_followed_users_response_empty_array); - TEST(test_init_fetch_followed_users_response_invalid_credentials); - TEST(test_init_fetch_followed_users_response_several_items); - - /* all user progress */ - TEST(test_init_fetch_all_user_progress_request); - - TEST(test_init_fetch_all_user_progress_response_empty_array); - TEST(test_init_fetch_all_user_progress_response_invalid_credentials); - TEST(test_init_fetch_all_user_progress_response_one_item); - TEST(test_init_fetch_all_user_progress_response_several_items); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos-test.sln b/src/rcheevos/test/rcheevos-test.sln deleted file mode 100644 index 5335edb284..0000000000 --- a/src/rcheevos/test/rcheevos-test.sln +++ /dev/null @@ -1,46 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28010.2036 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rcheevos-test", "rcheevos-test.vcxproj", "{74FBBFC4-5AC5-4A86-B292-B2F535E9912C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{900F3B07-2B47-4967-8296-CEC29233458F}" - ProjectSection(SolutionItems) = preProject - ..\.editorconfig = ..\.editorconfig - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "validator", "..\validator\validator.vcxproj", "{16FABFA7-A2EC-4CD0-9E04-50315A2BB613}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x64.ActiveCfg = Debug|x64 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x64.Build.0 = Debug|x64 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x86.ActiveCfg = Debug|Win32 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Debug|x86.Build.0 = Debug|Win32 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x64.ActiveCfg = Release|x64 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x64.Build.0 = Release|x64 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x86.ActiveCfg = Release|Win32 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C}.Release|x86.Build.0 = Release|Win32 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x64.ActiveCfg = Debug|x64 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x64.Build.0 = Debug|x64 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x86.ActiveCfg = Debug|Win32 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Debug|x86.Build.0 = Debug|Win32 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x64.ActiveCfg = Release|x64 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x64.Build.0 = Release|x64 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x86.ActiveCfg = Release|Win32 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {02ADF144-5109-40C0-AA02-5BC5585A9883} - EndGlobalSection -EndGlobal diff --git a/src/rcheevos/test/rcheevos-test.vcxproj b/src/rcheevos/test/rcheevos-test.vcxproj deleted file mode 100644 index 232635308d..0000000000 --- a/src/rcheevos/test/rcheevos-test.vcxproj +++ /dev/null @@ -1,239 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 15.0 - {74FBBFC4-5AC5-4A86-B292-B2F535E9912C} - rcheevostest - Application - MultiByte - v143 - - - v142 - - - v141 - - - - true - - - false - true - - - true - - - false - true - - - - - - - - - - - - - - - - - - - - - - - Level3 - Disabled - true - true - $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - true - _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) - - - Console - - - - - Level3 - Disabled - true - true - $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - true - _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) - - - Console - - - - - Level3 - MaxSpeed - true - true - true - true - $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - true - _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) - - - true - true - Console - - - - - Level3 - MaxSpeed - true - true - true - true - $(ProjectDir);$(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos;%(AdditionalIncludeDirectories) - true - _MBCS;RC_CLIENT_SUPPORTS_RAINTEGRATION;RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) - - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/rcheevos/test/rcheevos-test.vcxproj.filters b/src/rcheevos/test/rcheevos-test.vcxproj.filters deleted file mode 100644 index 5a8d4ec7b6..0000000000 --- a/src/rcheevos/test/rcheevos-test.vcxproj.filters +++ /dev/null @@ -1,335 +0,0 @@ - - - - - {c1b8966b-c711-43ce-ac8a-1dcca6b41ff3} - - - {43b6b53b-c37e-453e-97bf-df56c515f1a7} - - - {9060be9f-a1f8-4940-a6e6-8ada5c89ae94} - - - {faf643ae-e095-4db5-a701-3ea9ed343cc0} - - - {f890b4f1-8de5-4730-b612-3a7dbf65ca74} - - - {21341552-6b14-4f5b-a26c-d9393ffbdbcc} - - - {d049182b-0721-46b5-b93e-6f8f7f572b45} - - - {0b946a47-0089-4118-a5b2-cd57245dc58b} - - - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rhash - - - tests\rcheevos - - - tests - - - tests\rcheevos - - - tests\rcheevos - - - tests\rcheevos - - - tests\rcheevos - - - tests\rcheevos - - - tests\rcheevos - - - tests\rcheevos - - - src\rhash - - - tests\rhash - - - tests\rhash - - - tests\rcheevos - - - tests\rcheevos - - - src\rcheevos - - - tests\rcheevos - - - src\rcheevos - - - tests\rcheevos - - - src\rhash - - - tests\rhash - - - tests\rhash - - - src\rapi - - - src\rapi - - - src\rapi - - - tests\rapi - - - tests\rapi - - - tests\rapi - - - src\rapi - - - tests\rapi - - - src\rapi - - - tests\rapi - - - src\rcheevos - - - tests\rcheevos - - - tests\rcheevos - - - src - - - src - - - src - - - tests - - - tests - - - src - - - src - - - tests - - - tests - - - src - - - src\rhash - - - tests\rhash - - - src\rhash - - - src\rhash - - - tests\rhash - - - src\rhash - - - tests\rhash - - - src\rhash - - - src\rcheevos - - - - - src\rcheevos - - - tests\rcheevos - - - tests - - - tests\rhash - - - src\rhash - - - tests\rhash - - - src\rhash - - - src\rcheevos - - - src\rapi - - - src\rapi - - - src\rapi - - - src\rapi - - - tests\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rapi - - - src\rapi - - - src\rcheevos - - - src - - - src - - - src - - - src - - - src - - - src - - - src - - - src - - - src - - - src - - - src - - - src\rhash - - - src\rhash - - - src - - - - - src\rcheevos - - - tests\rcheevos - - - src - - - \ No newline at end of file diff --git a/src/rcheevos/test/rcheevos/mock_memory.h b/src/rcheevos/test/rcheevos/mock_memory.h deleted file mode 100644 index 388c752324..0000000000 --- a/src/rcheevos/test/rcheevos/mock_memory.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef MOCK_MEMORY_H -#define MOCK_MEMORY_H - -typedef struct { - uint8_t* ram; - uint32_t size; -} -memory_t; - -static uint32_t peekb(uint32_t address, memory_t* memory) { - return address < memory->size ? memory->ram[address] : 0; -} - -static uint32_t peek(uint32_t address, uint32_t num_bytes, void* ud) { - memory_t* memory = (memory_t*)ud; - - switch (num_bytes) { - case 1: return peekb(address, memory); - - case 2: return peekb(address, memory) | - peekb(address + 1, memory) << 8; - - case 4: return peekb(address, memory) | - peekb(address + 1, memory) << 8 | - peekb(address + 2, memory) << 16 | - peekb(address + 3, memory) << 24; - } - - return 0; -} - -#endif /* MOCK_MEMORY_H */ diff --git a/src/rcheevos/test/rcheevos/test_condition.c b/src/rcheevos/test/rcheevos/test_condition.c deleted file mode 100644 index 412cc11123..0000000000 --- a/src/rcheevos/test/rcheevos/test_condition.c +++ /dev/null @@ -1,570 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -static void _assert_operand(rc_operand_t* self, uint8_t expected_type, uint8_t expected_size, uint32_t expected_address) { - ASSERT_NUM_EQUALS(self->type, expected_type); - - switch (expected_type) { - case RC_OPERAND_ADDRESS: - case RC_OPERAND_DELTA: - case RC_OPERAND_PRIOR: - ASSERT_NUM_EQUALS(self->size, expected_size); - ASSERT_NUM_EQUALS(self->value.memref->address, expected_address); - break; - - case RC_OPERAND_CONST: - ASSERT_NUM_EQUALS(self->value.num, expected_address); - break; - } -} -#define assert_operand(operand, expected_type, expected_size, expected_address) ASSERT_HELPER(_assert_operand(operand, expected_type, expected_size, expected_address), "assert_operand") - -static void _assert_parse_condition( - const char* memaddr, uint8_t expected_type, - uint8_t expected_left_type, uint8_t expected_left_size, uint32_t expected_left_value, - uint8_t expected_operator, - uint8_t expected_right_type, uint8_t expected_right_size, uint32_t expected_right_value, - uint32_t expected_required_hits -) { - rc_condition_t* self; - rc_parse_state_t parse; - rc_memrefs_t memrefs; - char buffer[512]; - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - self = rc_parse_condition(&memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_EQUALS(self->type, expected_type); - assert_operand(&self->operand1, expected_left_type, expected_left_size, expected_left_value); - ASSERT_NUM_EQUALS(self->oper, expected_operator); - assert_operand(&self->operand2, expected_right_type, expected_right_size, expected_right_value); - ASSERT_NUM_EQUALS(self->required_hits, expected_required_hits); -} -#define assert_parse_condition(memaddr, expected_type, expected_left_type, expected_left_size, expected_left_value, \ - expected_operator, expected_right_type, expected_right_size, expected_right_value, expected_required_hits) \ - ASSERT_HELPER(_assert_parse_condition(memaddr, expected_type, expected_left_type, expected_left_size, expected_left_value, \ - expected_operator, expected_right_type, expected_right_size, expected_right_value, expected_required_hits), "assert_parse_condition") - -static void test_parse_condition(const char* memaddr, uint8_t expected_type, uint8_t expected_left_type, - uint8_t expected_operator, uint32_t expected_required_hits) { - if (expected_operator == RC_OPERATOR_NONE) { - assert_parse_condition(memaddr, expected_type, - expected_left_type, RC_MEMSIZE_8_BITS, 0x1234U, - expected_operator, - RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U, - expected_required_hits - ); - } - else { - assert_parse_condition(memaddr, expected_type, - expected_left_type, RC_MEMSIZE_8_BITS, 0x1234U, - expected_operator, - RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 8U, - expected_required_hits - ); - } -} - -static void test_parse_operands(const char* memaddr, - uint8_t expected_left_type, uint8_t expected_left_size, uint32_t expected_left_value, - uint8_t expected_right_type, uint8_t expected_right_size, uint32_t expected_right_value) { - assert_parse_condition(memaddr, RC_CONDITION_STANDARD, - expected_left_type, expected_left_size, expected_left_value, - RC_OPERATOR_EQ, - expected_right_type, expected_right_size, expected_right_value, - 0 - ); -} - -static void test_parse_modifier(const char* memaddr, uint8_t expected_operator, uint8_t expected_operand, double expected_multiplier) { - assert_parse_condition(memaddr, RC_CONDITION_ADD_SOURCE, - RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, - expected_operator, - expected_operand, RC_MEMSIZE_8_BITS, (int)expected_multiplier, - 0 - ); -} - -static void test_parse_modifier_shorthand(const char* memaddr, uint8_t expected_type) { - assert_parse_condition(memaddr, expected_type, - RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, - RC_OPERATOR_NONE, - RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1U, - 0 - ); -} - -static void test_parse_condition_error(const char* memaddr, int expected_error) { - if (expected_error == RC_OK) { - ASSERT_NUM_GREATER(rc_trigger_size(memaddr), 0); - } else { - ASSERT_NUM_EQUALS(rc_trigger_size(memaddr), expected_error); - } -} - -static int evaluate_condition(rc_condition_t* cond, memory_t* memory, rc_memrefs_t* memrefs) { - rc_eval_state_t eval_state; - - memset(&eval_state, 0, sizeof(eval_state)); - eval_state.peek = peek; - eval_state.peek_userdata = memory; - - rc_update_memref_values(memrefs, peek, memory); - return rc_test_condition(cond, &eval_state); -} - -static void test_evaluate_condition(const char* memaddr, uint8_t expected_comparator, int expected_result) { - rc_condition_t* self; - rc_parse_state_t parse; - char buffer[512]; - rc_memrefs_t memrefs; - int ret; - uint8_t ram[] = {0x00, 0x11, 0x34, 0xAB, 0x56}; - memory_t memory; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - self = rc_parse_condition(&memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_EQUALS(*memaddr, 0); - - rc_update_memref_values(&memrefs, peek, &memory); /* capture delta for ram[1] */ - ram[1] = 0x12; - - ASSERT_NUM_EQUALS(self->optimized_comparator, expected_comparator); - ret = evaluate_condition(self, &memory, &memrefs); - - if (expected_result) { - ASSERT_NUM_EQUALS(ret, 1); - } else { - ASSERT_NUM_EQUALS(ret, 0); - } -} - -static void test_default_comparator(const char* memaddr) { - rc_condset_t* condset; - rc_condition_t* condition; - rc_parse_state_t parse; - char buffer[512]; - rc_memrefs_t memrefs; - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - condset = rc_parse_condset(&memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_EQUALS(*memaddr, 0); - - condition = condset->conditions; - while (condition->next) - condition = condition->next; - - /* expect last condition to have default comparator - that's the point of this test */ - ASSERT_NUM_EQUALS(condition->optimized_comparator, RC_PROCESSING_COMPARE_DEFAULT); -} - -static void test_evaluate_condition_float(const char* memaddr, int expected_result) { - rc_condition_t* self; - rc_parse_state_t parse; - char buffer[512]; - rc_memrefs_t memrefs; - int ret; - uint8_t ram[] = {0x00, 0x00, 0x00, 0x40, 0x83, 0x49, 0x0F, 0xDB}; /* FF0=2, FF4=2*pi */ - memory_t memory; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - self = rc_parse_condition(&memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_EQUALS(*memaddr, 0); - - ret = evaluate_condition(self, &memory, &memrefs); - - if (expected_result) { - ASSERT_NUM_EQUALS(ret, 1); - } else { - ASSERT_NUM_EQUALS(ret, 0); - } -} - -static void test_condition_compare_delta() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condition_t* cond; - rc_parse_state_t parse; - char buffer[512]; - rc_memrefs_t memrefs; - - const char* cond_str = "0xH0001>d0xH0001"; - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - cond = rc_parse_condition(&cond_str, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_EQUALS(*cond_str, 0); - memory.ram = ram; - memory.size = sizeof(ram); - - /* initial delta value is 0, 0x12 > 0 */ - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* delta value is now 0x12, 0x12 = 0x12 */ - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* delta value is now 0x12, 0x11 < 0x12 */ - ram[1] = 0x11; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* delta value is now 0x13, 0x12 > 0x11 */ - ram[1] = 0x12; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); -} - -static void test_condition_delta_24bit() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condition_t* cond; - rc_parse_state_t parse; - char buffer[512]; - rc_memrefs_t memrefs; - - const char* cond_str = "0xW0001>d0xW0001"; - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - cond = rc_parse_condition(&cond_str, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_EQUALS(*cond_str, 0); - memory.ram = ram; - memory.size = sizeof(ram); - - /* initial delta value is 0x000000, 0xAB3412 > 0x000000 */ - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* delta value is now 0xAB3412, 0xAB3412 == 0xAB3412 */ - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* value changes to 0xAB3411, delta value is now 0xAB3412, 0xAB3411 < 0xAB3412 */ - ram[1] = 0x11; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* value changes to 0xAB3412, delta value is now 0xAB3411, 0xAB3412 > 0xAB3411 */ - ram[1] = 0x12; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* ram[4] should not affect the 24-bit value, 0xAB3412 == 0xAB3412 */ - ram[4] = 0xAC; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* value changes to 0xAB3411, delta is still 0xAB3412, 0xAB3411 < 0xAB3412 */ - ram[1] = 0x11; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* ram[4] should not affect the 24-bit value, 0xAB3411 == 0xAB3411 */ - ram[4] = 0xAD; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); -} - -static void test_condition_prior_24bit() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condition_t* cond; - rc_parse_state_t parse; - char buffer[512]; - rc_memrefs_t memrefs; - - const char* cond_str = "0xW0001>p0xW0001"; - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - cond = rc_parse_condition(&cond_str, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_EQUALS(*cond_str, 0); - memory.ram = ram; - memory.size = sizeof(ram); - - /* initial prior value is 0x000000, 0xAB3412 > 0x000000 */ - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* delta value is now 0xAB3412, but prior is still 0x000000, 0xAB3412 > 0x000000 */ - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* value changes to 0xAB3411, delta and prior values are now 0xAB3412, 0xAB3411 < 0xAB3412 */ - ram[1] = 0x11; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* value changes to 0xAB3412, delta and prior values are now 0xAB3411, 0xAB3412 > 0xAB3411 */ - ram[1] = 0x12; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* ram[4] should not affect the 24-bit value, 0xAB3412 > 0xAB3411 */ - ram[4] = 0xAC; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* ram[4] should not affect the 24-bit value, 0xAB3412 > 0xAB3411 */ - ram[4] = 0xAD; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 1); - - /* value changes to 0xAB3411, delta and prior values are now 0xAB3412, 0xAB3411 < 0xAB3412 */ - ram[1] = 0x11; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* ram[4] should not affect the 24-bit value, 0xAB3411 < 0xAB3412 */ - ram[4] = 0xAE; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); - - /* ram[4] should not affect the 24-bit value, 0xAB3411 < 0xAB3412 */ - ram[4] = 0xAF; - ASSERT_NUM_EQUALS(evaluate_condition(cond, &memory, &memrefs), 0); -} - -void test_condition(void) { - TEST_SUITE_BEGIN(); - - /* different comparison operators */ - TEST_PARAMS5(test_parse_condition, "0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234==8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234!=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_NE, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234<8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LT, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234<=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LE, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234>8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_GT, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234>=8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_GE, 0); - TEST_PARAMS5(test_parse_condition, "0xH1234<8", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_LT, 0); - - /* special accessors */ - TEST_PARAMS5(test_parse_condition, "d0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_DELTA, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "p0xH1234=8", RC_CONDITION_STANDARD, RC_OPERAND_PRIOR, RC_OPERATOR_EQ, 0); - - /* flags */ - TEST_PARAMS5(test_parse_condition, "R:0xH1234=8", RC_CONDITION_RESET_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "P:0xH1234=8", RC_CONDITION_PAUSE_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234=8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); - TEST_PARAMS5(test_parse_condition, "B:0xH1234=8", RC_CONDITION_SUB_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); - TEST_PARAMS5(test_parse_condition, "C:0xH1234=8", RC_CONDITION_ADD_HITS, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "D:0xH1234=8", RC_CONDITION_SUB_HITS, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "M:0xH1234=8", RC_CONDITION_MEASURED, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "G:0xH1234=8", RC_CONDITION_MEASURED, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "Q:0xH1234=8", RC_CONDITION_MEASURED_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "I:0xH1234=8", RC_CONDITION_ADD_ADDRESS, RC_OPERAND_ADDRESS, RC_OPERATOR_NONE, 0); - TEST_PARAMS5(test_parse_condition, "T:0xH1234=8", RC_CONDITION_TRIGGER, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - TEST_PARAMS5(test_parse_condition, "Z:0xH1234=8", RC_CONDITION_RESET_NEXT_IF, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 0); - - /* modifiers (only valid with some flags, use A:) */ - TEST_PARAMS5(test_parse_condition, "A:0xH1234*8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_MULT, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234/8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_DIV, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234&8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_AND, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234^8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_XOR, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234%8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_MOD, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234+8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_ADD, 0); - TEST_PARAMS5(test_parse_condition, "A:0xH1234-8", RC_CONDITION_ADD_SOURCE, RC_OPERAND_ADDRESS, RC_OPERATOR_SUB, 0); - - TEST_PARAMS4(test_parse_modifier, "A:0xH1234", RC_OPERATOR_NONE, RC_OPERAND_CONST, 1); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234*1", RC_OPERATOR_MULT, RC_OPERAND_CONST, 1); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234*3", RC_OPERATOR_MULT, RC_OPERAND_CONST, 3); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234*f0.5", RC_OPERATOR_MULT, RC_OPERAND_FP, 0.5); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234*f.5", RC_OPERATOR_MULT, RC_OPERAND_FP, 0.5); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234*-1", RC_OPERATOR_MULT, RC_OPERAND_CONST, -1); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234*0xH3456", RC_OPERATOR_MULT, RC_OPERAND_ADDRESS, 0x3456); - - /* legacy serializers would include whatever happened to be in the right side before it was converted to a modifier. - * they should be ignored */ - TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234!=0xH1234", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0.60.", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); - TEST_PARAMS4(test_parse_modifier, "A:0xH1234=0(60)", RC_OPERATOR_NONE, RC_OPERAND_CONST, 0); - - /* hit counts */ - TEST_PARAMS5(test_parse_condition, "0xH1234=8(1)", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1); - TEST_PARAMS5(test_parse_condition, "0xH1234=8.1.", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1); /* legacy format */ - TEST_PARAMS5(test_parse_condition, "0xH1234=8(1000)", RC_CONDITION_STANDARD, RC_OPERAND_ADDRESS, RC_OPERATOR_EQ, 1000); - - /* hex value is interpreted as a 16-bit memory reference */ - TEST_PARAMS7(test_parse_operands, "0xH1234=0x80", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U, RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x80U); - - TEST_PARAMS7(test_parse_operands, "0xL1234=0xU3456", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U, RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x3456U); - - /* shorthard for modifier conditions */ - TEST_PARAMS2(test_parse_modifier_shorthand, "A:0xH1234", RC_CONDITION_ADD_SOURCE); - TEST_PARAMS2(test_parse_modifier_shorthand, "B:0xH1234", RC_CONDITION_SUB_SOURCE); - TEST_PARAMS2(test_parse_modifier_shorthand, "I:0xH1234", RC_CONDITION_ADD_ADDRESS); - - /* parse errors */ - TEST_PARAMS2(test_parse_condition_error, "0xH1234==0", RC_OK); - TEST_PARAMS2(test_parse_condition_error, "H0x1234==0", RC_INVALID_CONST_OPERAND); - TEST_PARAMS2(test_parse_condition_error, "0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "C:0x1234", RC_INVALID_OPERATOR); /* shorthand only valid on modifier conditions */ - TEST_PARAMS2(test_parse_condition_error, "N:0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "O:0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "P:0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "R:0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "M:0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "G:0x1234", RC_INVALID_OPERATOR); - TEST_PARAMS2(test_parse_condition_error, "Y:0x1234", RC_INVALID_CONDITION_TYPE); - TEST_PARAMS2(test_parse_condition_error, "0x1234=1.2", RC_INVALID_REQUIRED_HITS); - TEST_PARAMS2(test_parse_condition_error, "0.1234==0", RC_INVALID_OPERATOR); /* period is assumed to be operator */ - TEST_PARAMS2(test_parse_condition_error, "0==0.1234", RC_INVALID_REQUIRED_HITS); /* period is assumed to be start of hit target, no end marker */ - - /* simple evaluations (ram[1] = 18, delta(ram[1]) = 17, ram[2] = 52, delta(ram[2]) = 52) */ - TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001>0", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); - - TEST_PARAMS3(test_evaluate_condition, "0xH0001=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001!=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0001<=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001>=18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001<18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0001>18", RC_PROCESSING_COMPARE_MEMREF_TO_CONST, 0); - - TEST_PARAMS3(test_evaluate_condition, "d0xH0001=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001!=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001<=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001>=18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001<18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001>18", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); - - TEST_PARAMS3(test_evaluate_condition, "d0xH0002=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002!=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002<=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002>=52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002<52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002>52", RC_PROCESSING_COMPARE_DELTA_TO_CONST, 0); - - TEST_PARAMS3(test_evaluate_condition, "0xH0001<0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001>0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0001=0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, 1); - - TEST_PARAMS3(test_evaluate_condition, "0xH0001=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0001!=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001<=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0001>=d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001d0xH0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); - - TEST_PARAMS3(test_evaluate_condition, "0xH0002=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0002!=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0002<=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0002>=d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0002d0xH0002", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, 0); - - TEST_PARAMS3(test_evaluate_condition, "d0xH0001=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001!=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001<=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001>=0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001<0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0001>0xH0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); - - TEST_PARAMS3(test_evaluate_condition, "d0xH0002=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002!=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002<=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002>=0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002<0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xH0002>0xH0002", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, 0); - - TEST_PARAMS3(test_evaluate_condition, "0xM0001=1", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 0); - TEST_PARAMS3(test_evaluate_condition, "0xM0001!=1", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "0xK0001=2", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "0xK0001!=2", RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, 0); - - TEST_PARAMS3(test_evaluate_condition, "d0xM0001=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xM0001!=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xM0002=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 0); - TEST_PARAMS3(test_evaluate_condition, "d0xM0002!=1", RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, 1); - - TEST_PARAMS3(test_evaluate_condition, "0xM0001=0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); - TEST_PARAMS3(test_evaluate_condition, "0xM0001!=0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "0xM0001=0xH0000", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "0xM0001!=0xH0000", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); - TEST_PARAMS3(test_evaluate_condition, "0xH0000=0xM0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0000!=0xM0001", RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, 0); - - TEST_PARAMS3(test_evaluate_condition, "0xM0001=d0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "0xM0001!=d0xN0001", RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, 0); - - TEST_PARAMS3(test_evaluate_condition, "d0xM0001=0xN0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, 1); - TEST_PARAMS3(test_evaluate_condition, "d0xM0001!=0xN0001", RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, 0); - - TEST_PARAMS3(test_evaluate_condition, "0xH0001=0xH0001", RC_PROCESSING_COMPARE_ALWAYS_TRUE, 1); - TEST_PARAMS3(test_evaluate_condition, "0xH0001!=0xH0001", RC_PROCESSING_COMPARE_ALWAYS_FALSE, 0); - TEST_PARAMS3(test_evaluate_condition, "1=1", RC_PROCESSING_COMPARE_ALWAYS_TRUE, 1); - TEST_PARAMS3(test_evaluate_condition, "0=1", RC_PROCESSING_COMPARE_ALWAYS_FALSE, 0); - - TEST_PARAMS1(test_default_comparator, "I:0xH0000_0xH0001=0"); /* indirect cannot be optimized */ - TEST_PARAMS1(test_default_comparator, "fF0001=f2.0"); /* float is not common enough to be optimized */ - TEST_PARAMS1(test_default_comparator, "p0xH0001=0"); /* prior is not common enough to be optimized */ - TEST_PARAMS1(test_default_comparator, "b0xH0001=0"); /* bcd is not common enough to be optimized */ - TEST_PARAMS1(test_default_comparator, "~0xH0001=0"); /* inverted is not common enough to be optimized */ - TEST_PARAMS1(test_default_comparator, "d0xH0001=0x 0001"); /* delta comparison only optimized for same address, same size */ - TEST_PARAMS1(test_default_comparator, "0xH0001=d0x 0001"); /* delta comparison only optimized for same address, same size */ - TEST_PARAMS1(test_default_comparator, "d0xH0001=0xH0002"); /* delta comparison only optimized for same address, same size */ - TEST_PARAMS1(test_default_comparator, "0xH0001=d0xH0002"); /* delta comparison only optimized for same address, same size */ - - /* float evaluations (ram[0] = 2.0, ram[4] = 3.14159 */ - TEST_PARAMS2(test_evaluate_condition_float, "fF0000=f2.0", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=f2.0", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000<=f2.0", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000>=f2.0", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000f2.0", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000f1.999999", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000f2.000001", 0); - - TEST_PARAMS2(test_evaluate_condition_float, "fF0000=2", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=2", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000<=2", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000>=2", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000<2", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000>2", 0); - - TEST_PARAMS2(test_evaluate_condition_float, "fM0004=f6.283185", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004!=f6.283185", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004<=f6.283185", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004>=f6.283185", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283185", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283183", 1); /* binary to float, the last decimal digit may */ - TEST_PARAMS2(test_evaluate_condition_float, "fM0004f6.283187", 0); /* ensure we cover an epsilon gap */ - - TEST_PARAMS2(test_evaluate_condition_float, "fM0004=6", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004!=6", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004<=6", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004>=6", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004<6", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fM0004>6", 1); - - TEST_PARAMS2(test_evaluate_condition_float, "fF0000==fF0000", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=fF0000", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000==fM0004", 0); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000!=fM0004", 1); - TEST_PARAMS2(test_evaluate_condition_float, "fF0000fM0004", 0); - - TEST(test_condition_compare_delta); - TEST(test_condition_delta_24bit); - TEST(test_condition_prior_24bit); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_condset.c b/src/rcheevos/test/rcheevos/test_condset.c deleted file mode 100644 index 7996328608..0000000000 --- a/src/rcheevos/test/rcheevos/test_condset.c +++ /dev/null @@ -1,5170 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -#include "../src/rc_compat.h" - -static void _assert_parse_condset(rc_condset_t** condset, rc_memrefs_t* memrefs, void* buffer, const char* memaddr) -{ - rc_parse_state_t parse; - int size; - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, memrefs); - - *condset = rc_parse_condset(&memaddr, &parse); - size = parse.offset; - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(size, 0); - ASSERT_PTR_NOT_NULL(*condset); -} -#define assert_parse_condset(condset, memrefs_out, buffer, memaddr) ASSERT_HELPER(_assert_parse_condset(condset, memrefs_out, buffer, memaddr), "assert_parse_condset") - -static void _assert_evaluate_condset(rc_condset_t* condset, rc_memrefs_t* memrefs, memory_t* memory, int expected_result) { - int result; - rc_eval_state_t eval_state; - - rc_update_memref_values(memrefs, peek, memory); - - memset(&eval_state, 0, sizeof(eval_state)); - eval_state.peek = peek; - eval_state.peek_userdata = memory; - - result = rc_test_condset(condset, &eval_state); - - /* NOTE: reset normally handled by trigger since it's not group specific */ - if (eval_state.was_reset) - rc_reset_condset(condset); - - ASSERT_NUM_EQUALS(result, expected_result); -} -#define assert_evaluate_condset(condset, memrefs, memory, expected_result) ASSERT_HELPER(_assert_evaluate_condset(condset, &memrefs, memory, expected_result), "assert_evaluate_condset") - -static rc_condition_t* condset_get_cond(rc_condset_t* condset, int cond_index) { - rc_condition_t* cond = condset->conditions; - - while (cond_index-- != 0) { - if (cond == NULL) - break; - - cond = cond->next; - } - - return cond; -} - -static void _assert_hit_count(rc_condset_t* condset, int cond_index, uint32_t expected_hit_count) { - rc_condition_t* cond = condset_get_cond(condset, cond_index); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->current_hits, expected_hit_count); -} -#define assert_hit_count(condset, cond_index, expected_hit_count) ASSERT_HELPER(_assert_hit_count(condset, cond_index, expected_hit_count), "assert_hit_count") - - -static void test_hitcount_increment_when_true() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18"); /* one condition, true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1U); - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2U); -} - -static void test_hitcount_does_not_increment_when_false() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001!=18"); /* one condition, false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0U); - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0U); -} - -static void test_hitcount_target() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=20(2)_0xH0002=52"); - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - - /* hit target met, overall is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); /* hit target met, not incremented */ - assert_hit_count(condset, 1, 4); - - /* first condition no longer true, but hit count was met so it acts true */ - ram[1] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 5); -} - -static void test_hitcount_two_conditions(const char* memaddr, int expected_result, uint32_t expected_hitcount1, uint32_t expected_hitcount2) { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, memaddr); - assert_evaluate_condset(condset, memrefs, &memory, expected_result); - assert_hit_count(condset, 0, expected_hitcount1); - assert_hit_count(condset, 1, expected_hitcount2); -} - -static void test_hitcount_three_conditions(const char* memaddr, int expected_result, uint32_t expected_hitcount1, - uint32_t expected_hitcount2, uint32_t expected_hitcount3) { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, memaddr); - assert_evaluate_condset(condset, memrefs, &memory, expected_result); - assert_hit_count(condset, 0, expected_hitcount1); - assert_hit_count(condset, 1, expected_hitcount2); - assert_hit_count(condset, 2, expected_hitcount3); -} - -static void test_pauseif() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52_P:0xL0x0004=6"); - - /* first condition true, but ignored because both pause conditions are true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); /* Also true, but processing stops on first PauseIf */ - - /* first pause condition no longer true, but second still is */ - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); /* PauseIf goes to 0 when false */ - assert_hit_count(condset, 2, 1); - - /* both pause conditions not true, set will trigger */ - ram[4] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); -} - -static void test_pauseif_hitcount_one() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.1."); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* pause condition no longer true, but hitcount prevents trigger */ - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); -} - -static void test_pauseif_hitcount_two() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.2."); - - /* pause hit target has not been met, group is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - - /* pause hit target has been met, group is false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - - /* pause condition is no longer true, but hitcount prevents trigger */ - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); -} - -static void test_pauseif_hitcount_with_reset() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_P:0xH0002=52.1._R:0xH0003=1"); - - /* pauseif triggered, non-pauseif conditions ignored */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* pause condition is no longer true, but hitcount prevents trigger */ - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* pause has precedence over reset, reset in group is ignored */ - ram[3] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); -} - -static void test_pauseif_resetnextif() -{ - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0002=0_0xH0001=18_Z:0xH0003=1_N:0xH0000=0_P:0xH0002=25.1."); - - /* accumulate hit counts */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 0); - - /* pauseif triggered, non-pauseif conditions ignored */ - ram[2] = 25; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 2); - assert_hit_count(condset, 4, 1); - - /* pause condition is no longer true, but its hitcount prevents the non-pauseif conditions from being processed */ - ram[2] = 10; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 3); - assert_hit_count(condset, 4, 1); - - /* reset next if clears hit on pause, but not on non-pauseif conditions */ - ram[3] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); /* resetnextif keeps its own hitcount */ - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 0); - - /* trigger is true */ - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 2); /* resetnextif keeps its own hitcount */ - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 0); -} - -static void test_pauseif_does_not_increment_hits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_P:0xL0004=4"); - - /* both conditions true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* pause condition is true, other conditions should not tally hits */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* pause condition not true, other conditions should tally hits */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - - /* pause condition is true, other conditions should not tally hits */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); - - /* pause condition not true, other conditions should tally hits */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 0); -} - -static void test_pauseif_delta_updated() { - uint8_t ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_d0xH0002=60"); - - /* upaused, delta = 0, current = 52 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* paused, delta = 52, current = 44 */ - ram[1] = 1; - ram[2] = 44; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - - /* paused, delta = 44, current = 60 */ - ram[2] = 60; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - - /* unpaused, delta = 60, current = 97 */ - ram[1] = 0; - ram[2] = 97; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); -} - -static void test_pauseif_indirect_delta_updated() { - uint8_t ram[] = {0x00, 0x00, 0x34, 0x3C, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_I:0xH0000_d0xH0002=60"); - - /* upaused, delta = 0, current = 52 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 2, 0); - - /* paused, delta = 52, current = 44 */ - ram[1] = 1; - ram[2] = 44; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 2, 0); - - /* paused, delta = 44, current = 60 */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 2, 0); - - /* unpaused, delta = 60, current = 97 */ - ram[1] = 0; - ram[3] = 97; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 2, 1); -} - -static void test_pauseif_short_circuit() { - uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* evaluation of an achievement stops at the first true pauseif condition - * - * this allows achievements to prevent accumulating hits on a pauselock farther down - * in a group. a better solution would be to use an AndNext on the pauselock, but - * there are achievements in the wild relying on this behavior. - * see https://retroachievements.org/achievement/66804, which has a PauseIf 2040 frames - * pass (condition 5), but don't tally those frames if the game is paused (condition 3). - * similarly, https://retroachievements.org/achievement/154804 has a PauseIf 480 frames - * (condition 5), but don't tally those frames if the map is visible (condition 4). - */ - assert_parse_condset(&condset, &memrefs, buffer, "P:0xH0001=1_P:0xH0002=1.3._0xH0003=1.4."); - - /* nothing true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* non-pauseif true */ - ram[3] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - - /* second pauseif tallies a hit, but it's not enough to pause the non-pauseif */ - ram[2] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 2); - - /* first pauseif is true, pauses the second pauseif and the non-pauseif */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 2); - - /* first pauseif is false, the second pauseif and the non-pauseif can update */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 3); - - /* second pauseif reaches hitcount, non-pauseif does not update */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 3); - - /* pauseif hitcount still met, non-pauseif does not update */ - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 3); -} - -static void test_resetif() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18_R:0xH0002=50_R:0xL0x0004=4"); - - /* first condition true, neither reset true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - - /* first reset true */ - ram[2] = 50; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); /* hitcount reset */ - - /* both resets true */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - - /* only second reset is true */ - ram[2] = 52; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - - /* neither reset true */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); -} - -static void test_resetif_cond_with_hittarget() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4"); - - /* both conditions true, reset not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* hit target met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 0); - - /* reset */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* reset no longer true, hit target not met */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* hit target met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); -} - -static void test_resetif_hitcount() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4.2."); - - /* hitcounts on conditions 1 and 2 are incremented */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* hitcounts on conditions 1 and 2 are incremented. cond 1 now true, so entire set is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* hitcount on condition 2 is incremented, cond 1 already at its target */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 0); - - /* reset condition is true, but its hitcount is not met */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 1); - ASSERT_NUM_EQUALS(condset_get_cond(condset, 2)->is_true, 1); - - /* second hit on reset condition should reset everything */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - /* is_true=3 indicates the condition was true this frame, and its hit target was met before it got reset */ - ASSERT_NUM_EQUALS(condset_get_cond(condset, 2)->is_true, 3); -} - -static void test_resetif_hitcount_one() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_0xH0002=52_R:0xL0004=4.1."); - - /* hitcounts on conditions 1 and 2 are incremented */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* hitcounts on conditions 1 and 2 are incremented. cond 1 now true, so entire set is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* hitcount on condition 2 is incremented, cond 1 already at its target */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 0); - - /* reset condition is true, its hitcount is met, so all hitcounts (including the resetif) should be reset */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); -} - -static void test_resetif_hitcount_addhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* never(repeated(3, byte(1) == 18 || low(4) == 6)) */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_R:0xL0004=6(3)"); - - /* result is true, no non-reset conditions */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - ASSERT_NUM_EQUALS(condset_get_cond(condset, 0)->is_true, 1); - ASSERT_NUM_EQUALS(condset_get_cond(condset, 1)->is_true, 1); - - /* total hitcount is met (2 for first condition, 1 for second, need 3 total) , everything resets */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - ASSERT_NUM_EQUALS(condset_get_cond(condset, 0)->is_true, 1); - /* is_true=2 indicates the condition was not true this frame, but its hit target was met before it got reset due to the AddHits */ - ASSERT_NUM_EQUALS(condset_get_cond(condset, 1)->is_true, 2); -} - -static void test_pauseif_resetif_hitcounts() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=18(2)_R:0xH0002=50_P:0xL0004=4"); - - /* first condition is true, pauseif and resetif are not, so it gets a hit */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - - /* pause is true, hit not incremented or reset */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - - /* reset if true, but set is still paused */ - ram[2] = 50; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - - /* set no longer paused, reset clears hitcount */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - - /* reset no longer true, hits increment again */ - ram[2] = 52; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - - /* hitcount met, set is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); -} - -static void test_resetnextif() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_0xH0001=18(2)_0xH0002=52.4."); - - /* both conditions true, resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* hit target met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 3); - - /* trigger resetnextif, last condition should not be reset */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 4); - - /* reset no longer true, hit target not met */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 4); - - /* hit target met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 4); -} - -static void test_resetnextif_non_hitcount_condition() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* resetnextif for non-hitcount condition will still set the hitcount to 0 and make it false */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_0xH0001=18_0xH0002=52.4."); - - /* both conditions true, resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* conditions continue to tally */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 3); - - /* target hitcount met, condset true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 4); - - /* trigger resetnextif, last condition should not be reset */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 4); - - /* reset no longer true (hit count on reset kept), condset is true again */ - ram[4] = 0x56; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 4); -} - -static void test_resetnextif_addhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf low4(0x0004)=4 - * AddHits byte(0x0001)=18 <-- ResetNextIf resets hits on this condition before it's added to the accumulator - * upper4(0x0003)=10 (4) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_C:0xH0001=18_0xU0003=10(4)_0xH0002=52.1."); - - /* both conditions true, resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* total tallies match limit, trigger */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 1); - - /* resetnextif resets hits on condition 2, but not condition 3 - total will be 3/4 - does not trigger */ - ram[4] = 0x54; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 3); - assert_hit_count(condset, 3, 1); -} - -static void test_resetnextif_addhits_chain() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf low4(0x0004)=4 - * AddHits byte(0x0001)=18 - * ResetNextIf low4(0x0004)=5 - * AddHits byte(0x0000)=0 - * upper4(0x0003)=10 (6) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_C:0xH0001=18_Z:0xL0004=5_C:0xH0000=0_0xU0003=10(6)_0xH0002=52.1."); - - /* resetnextifs not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 1); /* <- total hits is 3/6 */ - assert_hit_count(condset, 5, 1); - - /* first resetnextif true, only affects first addhits condition */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 2); - assert_hit_count(condset, 4, 2); /* <- total hits is 4/6 */ - assert_hit_count(condset, 5, 1); - - /* second resetnextif true, only affects second addhits condition */ - ram[4] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 3); /* <- total hits is 4/6 */ - assert_hit_count(condset, 5, 1); - - /* total hits reaches hit target */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 4); /* <- total hits is 6/6 */ - assert_hit_count(condset, 5, 1); -} - -static void test_resetnextif_addhits_chain_total() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AddHits byte(0x0001)=18 - * AddHits byte(0x0000)=0 - * ResetNextIf low4(0x0004)=4 - * upper4(0x0003)=10 (6) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_C:0xH0000=0_Z:0xL0004=4_0xU0003=10(6)_0xH0002=52.1."); - - /* resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); /* <- total hits is 3/6 */ - assert_hit_count(condset, 4, 1); - - /* resetnextif true, only affects that condition, not total */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 0); /* <- total hits is 4/6 */ - assert_hit_count(condset, 4, 1); - - /* resetnextif still true, total matches target */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 0); /* <- total hits is 6/6 */ - assert_hit_count(condset, 4, 1); -} - -static void test_resetnextif_using_andnext() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AndNext byte(0x0001)=18 - * ResetNextIf low4(0x0004)=4 - * upper4(0x0003)=10 (3) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=18_Z:0xL0004=4_0xU0003=10(3)_0xH0002=52.1."); - - /* conditions 1, 3, and 4 true; resetnextif relies on conditions 1 and 2, so it not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* both resetnextif conditions true */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - - /* first part of resetnextif not true */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* both resetnextif conditions true */ - ram[1] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); -} - -static void test_resetnextif_andnext() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf low4(0x0004)=4 - * AndNext byte(0x0001)=18 - * upper4(0x0003)=10 (3) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_N:0xH0001=18_0xU0003=10(3)_0xH0002=52.1."); - - /* both conditions true, resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* partial andnext not true */ - ram[3] = 0x86; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* andnext true again */ - ram[3] = 0xA0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 1); - - /* resetnextif resets all hits in the andnext chain */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); -} - -static void test_resetnextif_andnext_hitchain() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf low4(0x0004)=4 - * AndNext byte(0x0001)=18 (2) - * upper4(0x0003)=10 (3) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_N:0xH0001=18.2._0xU0003=10(3)_0xH0002=52.1."); - - /* both conditions true, resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); /* condition 2 must meet hitcount before condition three tallies a hit */ - assert_hit_count(condset, 3, 1); - - /* resetnextif true, should reset conditions 2 and 3 (3 was already 0) */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - - /* resetnextif not true, condition 2 tallies a hit */ - ram[4] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - - /* resetnextif still not true, condition 2 tallies another hit, which allows condition 3 to tally a hit */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* resetnextif true, should reset both conditions 2 and 3 */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); -} - -static void test_resetnextif_andnext_chain() -{ - uint8_t ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf byte(0x0001)!=0 - * AndNext byte(0x0002)=0 - * ResetNextIf byte(0x0001)=0 (2) - * byte(0x0003)=1 (5) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xH0001!=0_N:0xH0002=0_Z:0xH0001=0.2._0xH0003=1.5."); - - /* first resetnextif not true, conditions 1, 2 and 3 are true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* first resetnextif true, conditions 1 and 2 should reset, but not 3 */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 2); - - /* first resetnextif not true, condition 1, 2 and 3 are true */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 3); - - /* hitcount on condition 2 reached, condition 3 is reset */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 0); -} - -static void test_resetnextif_addaddress_andnext_chain() -{ - uint8_t ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AddAddress byte(0x0004) - * ResetNextIf byte(0x0001)!=0 - * AndNext byte(0x0002)=0 - * AddAddress byte(0x0004) - * ResetNextIf byte(0x0001)=0 (2) - * AddAddress byte(0x0004) - * byte(0x0003)=1 (5) - * Trigger byte(0x0003)=6 - */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0004_Z:0xH0001!=0_N:0xH0002=0_A:0xH0004_Z:0xH0001=0.2._A:0xH0004_0xH0003=1.5._T:0xH0003=6"); - - /* first resetnextif not true, conditions 2, 4 and 6 are true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 4, 1); - assert_hit_count(condset, 6, 1); - - /* first resetnextif true, conditions 2 and 4 should reset, but not 6 */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 4, 0); - assert_hit_count(condset, 6, 2); - - /* first resetnextif not true, conditions 2, 4 and 6 are true */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 4, 1); - assert_hit_count(condset, 6, 3); - - /* hitcount on condition 4 reached, condition 6 is reset */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 4, 2); - assert_hit_count(condset, 6, 0); - - /* allow last condition to reach hit target */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 6); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 4, 0); - assert_hit_count(condset, 6, 5); - - /* first resetnextif not true, conditions 2, 4 and 6 are true */ - ram[1] = 0; - ram[3] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 6); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 4, 1); - assert_hit_count(condset, 6, 5); - - /* first resetnextif true, conditions 2 and 4 should reset, but not 6 */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 7); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 4, 0); - assert_hit_count(condset, 6, 5); -} - -static void test_resetnextif_addaddress() { - uint8_t ram[] = {0x00, 0x00, 0x02, 0x03, 0x04}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_Z:0xH0001=1_I:0xH0000_0xH0002=2(3)_0xH0004=4.8."); - - /* both conditions true, resetnextif not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 1); - - /* resetnextif true */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 2); - - /* pointer changes. resetnextif not true, condition not true */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 3); - - /* condition true, resetnextif not true */ - ram[3] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 4); - - /* resetnextif true */ - ram[2] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 5); - - /* pointer changes, resetnextif and condition true */ - ram[0] = 0; - ram[2] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 6); - - /* resetnextif not true */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 7); -} - -static void test_resetnextif_chain() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf low4(0x0004)=4 - * ResetNextIf byte(0x0001)=1 - * upper4(0x0003)=10 (3) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_Z:0xH0001=1_0xU0003=10(3)_0xH0002=52.1."); - - /* both conditions true, resetnextifs not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* second resetnextif true, resets first hit count */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - - /* first resetnextif true, disables second, allows hitcount */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* second resetnextif no longer true, first still keeps it disabled */ - ram[1] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 1); - - /* first resetnextif no longer true (hit count on resetnextif itself is not reset), second already false */ - ram[4] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 3); - assert_hit_count(condset, 3, 1); -} - -static void test_resetnextif_chain_andnext() { - uint8_t ram[] = {0x00, 0x00, 0x01}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AndNext byte(0x0000)=0 - * ResetNextIf byte(0x0001)=1 - * AndNext byte(0x0000)=1 - * ResetNextIf byte(0x0001)=1 - * byte(0x0002)=1 (5) - */ - assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0000=0_Z:0xH0001=1_N:0xH0000=1_Z:0xH0001=1_0xH0002=1.5."); - - /* no ResetNextIf true. hit count incremented */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); /* hit captured on byte(0) == 0 */ - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 1); - - /* second ResetNextIf clause true */ - ram[0] = ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); /* ResetNextIf on condition 3 doesn't affect condition 1 */ - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 0); - - /* no ResetNextIf true, hit count incremented */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 1); - - /* first ResetNextIf clause true */ - ram[0] = 0; - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); /* first ResetNextIf affects second ResetNextIf */ - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 2); /* but not final clause */ -} - -static void test_resetnextif_chain_with_hits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf low4(0x0004)=4 - * ResetNextIf byte(0x0001)=1 (2) - * upper4(0x0003)=10 (3) - * byte(0x0002)=52 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xL0004=4_Z:0xH0001=1(2)_0xU0003=10(8)_0xH0002=52.1."); - - /* both conditions true, resetnextifs not true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* second resetnextif true, but hit target not met */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 1); - - /* first resetnextif true, resets second, allows hitcount */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 3); - assert_hit_count(condset, 3, 1); - - /* second resetnextif no longer true, first still keeps it disabled */ - ram[1] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 4); - assert_hit_count(condset, 3, 1); - - /* first resetnextif no longer true (hit count on resetnextif itself is not reset), second already false */ - ram[4] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 5); - assert_hit_count(condset, 3, 1); - - /* second resetnextif true again, but hitcount not met */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 6); - assert_hit_count(condset, 3, 1); - - /* second resetnextif hitcount met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - - /* second resetnextif condition no longer true, but hitcount keeps it active */ - ram[1] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 1); - - /* first resetnextif true, resets second, allows hit on third condition */ - ram[4] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); -} - -static void test_resetnextif_pause_lock() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf byte(0x0002)=1 - * PauseIf byte(0x0001)=1 (1) - */ - assert_parse_condset(&condset, &memrefs, buffer, "Z:0xH0002=1_P:0xH0001=1(1)"); - - /* both conditions false */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* reset next true */ - ram[2] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - - /* reset next and pause true */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - - /* only pause true */ - ram[2] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - - /* both conditions true */ - ram[2] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 0); - - /* only pause true */ - ram[2] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 1); - - /* both conditions false */ - ram[1] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 1); - - /* reset next true */ - ram[2] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 0); -} - -static void test_resetnextif_unfinished() { - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[512]; - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=0_Z:0xH0002=22"); - - ASSERT_NUM_EQUALS(condset->num_other_conditions, 2); -} - -static void test_addsource() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_0xH0002=22"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[2] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ - - /* first condition is true, but not sum */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* first condition is true, sum is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_overflow() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* adding two bytes will result in a value larger than 256, don't truncate to a byte */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_0xH0002=22"); - - /* sum is 0x102 (0x12 + 0xF0) */ - ram[2] = 0xF0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* sum is 0x122 (0x32 + 0xF0) */ - ram[1] = 0x32; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_subsource() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* NOTE: SubSource subtracts the first item from the second! */ - /* byte(1) - byte(2) == 14 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0_0xH0001=14"); - - /* difference is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* difference is correct */ - ram[2] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ - - /* first condition is true, but not difference */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* first condition is true, difference is negative inverse of expected value */ - ram[2] = 14; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is correct again */ - ram[1] = 28; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_subsource_legacy_garbage() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) - byte(2) == 14 */ - /* old serializers would store the comparison and right-side value from the condition before it was converted to SubSource */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0xH0000_0xH0001=14"); - - /* difference is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* difference is correct */ - ram[2] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); /* hit only tallied on final condition */ - - /* first condition is true, but not difference */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* first condition is true, difference is negative inverse of expected value */ - ram[2] = 14; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is correct again */ - ram[1] = 28; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_subsource_overflow() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* subtracting two bytes will result in a very large positive number, don't truncate to a byte */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0002=0_0xH0001=14"); - - /* difference is -10 (8 - 18) */ - ram[2] = 8; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* difference is 0xFFFFFF0E (8 - 0xFA) */ - ram[1] = 0xFA; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addsource_subsource() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) - low(2) + low(4) == 14 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_B:0xL0002=0_0xL0004=14"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* sum is correct */ - ram[1] = 12; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - - /* first condition is true, but not sum */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - - /* byte(4) would make sum true, but not low(4) */ - ram[4] = 0x12; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); - - /* difference is correct again */ - ram[2] = 1; - ram[4] = 15; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 2); -} - -static void test_addsource_multiply() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) * 3 + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*3_0xH0002=22"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[2] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct */ - ram[2] = 19; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_subsource_multiply() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(2) - byte(1) * 3 == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001*3_0xH0002=14"); - - /* difference is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* difference is correct */ - ram[2] = 32; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is not correct */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is correct */ - ram[2] = 17; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_multiply_fraction() { - uint8_t ram[] = {0x00, 0x08, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) * 0.75 + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*f0.75_0xH0002=22"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[2] = 16; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct */ - ram[1] = 15; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct */ - ram[2] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_multiply_address() { - uint8_t ram[] = {0x00, 0x06, 0x04, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) * byte(0) + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*0xH00000_0xH0002=22"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[0] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct */ - ram[2] = 19; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_divide() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) / 3 + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/3_0xH0002=22"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct */ - ram[1] = 14; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct */ - ram[2] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_compare_percentage() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* (byte(0)/byte(1) > 0.5) => (byte(1) * 0.5) < byte(0) */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001*f0.5_0<0xH0000"); - - /* 0/6 > 50% = false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - ram[0] = 2; assert_evaluate_condset(condset, memrefs, &memory, 0); /* 2/6 > 50% = false */ - ram[0] = 4; assert_evaluate_condset(condset, memrefs, &memory, 1); /* 4/6 > 50% = true */ - ram[1] = 7; assert_evaluate_condset(condset, memrefs, &memory, 1); /* 4/7 > 50% = true */ - ram[0] = 3; assert_evaluate_condset(condset, memrefs, &memory, 0); /* 3/7 > 50% = false */ -} - -static void test_subsource_divide() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(2) - byte(1) / 3 == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001/3_0xH0002=14"); - - /* difference is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* difference is correct */ - ram[2] = 16; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is not correct */ - ram[1] = 14; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is correct */ - ram[2] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_divide_address() { - uint8_t ram[] = {0x00, 0x06, 0x10, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) / byte(0) + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/0xH00000_0xH0002=22"); - - /* sum is not correct (divide by zero) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct */ - ram[2] = 21; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_divide_self() { - uint8_t ram[] = {0x00, 0x06, 0x10, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) / byte(1) + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001/0xH00001_0xH0002=22"); - - /* sum is not correct (1 + 16 != 22) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct (1 + 21 == 22) */ - ram[2] = 21; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct (0 + 21 == 22) */ - ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct (0 + 22 == 22) */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_mask() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) & 0x07 + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001&h7_0xH0002=22"); - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum is correct */ - ram[2] = 16; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is not correct */ - ram[1] = 0x74; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum is correct */ - ram[2] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_xor() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(1) ^ 0x05 + byte(2) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001^h5_0xH0002=22"); - - /* sum (6 ^ 5 + 52 == 22) is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* sum ((6 ^ 5 = 3) + 19 = 22) is correct */ - ram[2] = 19; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum ((17 ^ 5 = 20) + 19 = 22) is not correct */ - ram[1] = 0x11; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* sum ((17 ^ 5 = 20) + 2 = 22) is correct */ - ram[2] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_addsource_float_first() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F}; /* fF0004 = 1.5 */ - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* float(4) + float(4) + float(4) + 1 == 5.5 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:fF0004_A:fF0004_A:fF0004_1=f5.5"); - - /* +1 (int) will convert the intermediate value to an integer - * (1.5 + 1.5 + 1.5) => 4.5 => 4 + 1 = 5 - * which will not equal 5.5, so logic will fail. */ - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* float(4) + float(4) + float(4) + 1.0 == 5.5 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:fF0004_A:fF0004_A:fF0004_f1.0=f5.5"); - - /* +1.0 (float) will not convert the intermediate value to an integer - * (1.5 + 1.5 + 1.5) => 4.5 + 1.0 = 5.5 - * which will equal 5.5, so logic will succeed. */ - - /* sum is correct */ - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addsource_float_second() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F}; /* fF0004 = 1.5 */ - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* 1 + float(4) + float(4) + float(4) == 5.5 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_fF0004=f5.5"); - - /* the first value defines the type of the accumulator. since it's an integer, each float will be - * truncated when it is added to the accumulator, so 1 + (1.5) + (1.5) is 3. when the accumulator is - * added to the final value, the accumulator is converted to match the final value, so 3.0 + 1.5 = 4.5. */ - - /* sum is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 1 + float(4) + float(4) + float(4) == 4.5 (note: two intermediary floats are converted to int, but not last) */ - assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_fF0004=f4.5"); - /* sum is correct */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 1 + float(4) + float(4) + float(4) + 0 == 4.0 (note: all intermediate floats are converted to int) */ - assert_parse_condset(&condset, &memrefs, buffer, "A:1_A:fF0004_A:fF0004_A:fF0004_0=f4.0"); - /* sum is correct */ - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_subsource_mask() { - uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(2) - byte(1) & 0x06 == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0001&6_0xH0002=14"); - - /* difference is not correct */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - - /* difference is correct */ - ram[2] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is not correct */ - ram[1] = 10; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - /* difference is correct */ - ram[2] = 16; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); -} - -static void test_subsource_overflow_comparison_equal() { - uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ASSERT: "A==B" can be expressed as "-A+B==0" */ - - /* - byte(0) + byte(1) = 0 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_0xH0001=0"); - - /* 1 == 0 = false */ - ram[0] = 1; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 1 == 1 = true */ - ram[0] = 1; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 == 0 = true */ - ram[0] = 0; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 == 1 = false */ - ram[0] = 0; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 == 255 = false */ - ram[0] = 0; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 255 == 255 = true */ - ram[0] = 255; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 == 254 = false */ - ram[0] = 255; ram[1] = 254; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 255 == 0 = false */ - ram[0] = 255; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_subsource_overflow_comparison_greater() { - uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ASSERT: "A>B" can be expressed as "-A+B>M" where M is the largest number that cannot be - * represented by A or B */ - - /* - byte(0) + byte(1) > 256 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_0xH0001>256"); - - /* 1 > 0 = true */ - ram[0] = 1; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 1 > 1 = false */ - ram[0] = 1; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 > 0 = false */ - ram[0] = 0; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 > 1 = false */ - ram[0] = 0; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 > 255 = false */ - ram[0] = 0; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 255 > 255 = false */ - ram[0] = 255; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 255 > 254 = true */ - ram[0] = 255; ram[1] = 254; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 > 0 = true */ - ram[0] = 255; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_subsource_overflow_comparison_greater_or_equal() { - uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ASSERT: "A>=B" can be expressed as "-A-1+B>=M" where M is the largest number that cannot be - * represented by A or B */ - - /* - byte(0) - 1 + byte(1) > 256 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_B:1_0xH0001>=256"); - - /* 1 >= 0 = true */ - ram[0] = 1; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 1 >= 1 = true */ - ram[0] = 1; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 >= 0 = true */ - ram[0] = 0; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 >= 1 = false */ - ram[0] = 0; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 >= 255 = false */ - ram[0] = 0; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 255 >= 255 = true */ - ram[0] = 255; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 >= 254 = true */ - ram[0] = 255; ram[1] = 254; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 >= 0 = true */ - ram[0] = 255; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_subsource_overflow_comparison_lesser() { - uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ASSERT: "AM" where M is the largest number that cannot be - * represented by A or B */ - - /* - byte(0) + byte(1) + 256 > 256 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_A:0xH0001_256>256"); - - /* 1 < 0 = false */ - ram[0] = 1; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 1 < 1 = false */ - ram[0] = 1; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 < 0 = false */ - ram[0] = 0; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 0 < 1 = true */ - ram[0] = 0; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 < 255 = true */ - ram[0] = 0; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 < 255 = false */ - ram[0] = 255; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 254 < 255 = true */ - ram[0] = 254; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 < 0 = false */ - ram[0] = 255; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_subsource_overflow_comparison_lesser_or_equal() { - uint8_t ram[] = {0x00, 0x6C, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ASSERT: "AM" where M is the largest number that cannot be - * represented by A or B */ - - /* - byte(0) + byte(1) + 256 >= 256 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_A:0xH0001_256>=256"); - - /* 1 <= 0 = false */ - ram[0] = 1; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 1 <= 1 = true */ - ram[0] = 1; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 <= 0 = true */ - ram[0] = 0; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 <= 1 = true */ - ram[0] = 0; ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 0 <= 255 = true */ - ram[0] = 0; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 <= 255 = true */ - ram[0] = 255; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 254 <= 255 = true */ - ram[0] = 254; ram[1] = 255; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 255 <= 0 = false */ - ram[0] = 255; ram[1] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_subsource_float() { - uint8_t ram[] = {0x06, 0x00, 0x00, 0x00, 0x92, 0x44, 0x9A, 0x42}; /* fF0004 = 77.133926 */ - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* float(0x0004) - word(0) * 1.666667 > 65.8 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0x 0000*f1.666667_fF0004>f65.8"); - - /* 77.133926 - (6 * 1.666667) = 77.133926 - 10 = 67.133926 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 77.133926 - (7 * 1.666667) = 77.133926 - 11.6667 = 65.4672 */ - ram[0] = 7; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addsource_bits_change() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* bit0(0x01) + bit1(0x01) + bit2(0x01) == 3 && delta(bit0(0x01)) + delta(bit1(0x01)) + delta(bit2(0x01)) == 2 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xM0001_A:0xN0001_0xO0001=3_A:d0xM0001_A:d0xN0001_d0xO0001=2"); - - /* bit sum = 1 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 5, 0); - - /* bit sum = 3 (delta = 1) */ - ram[1] = 0x17; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 5, 0); - - /* bit sum = 2 (delta = 3) */ - ram[1] = 0x16; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 5, 0); - - /* bit sum = 1 (delta = 2) */ - ram[1] = 0x14; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 5, 1); - - /* bit sum = 2 (delta = 1) */ - ram[1] = 0x16; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 5, 1); - - /* bit sum = 3 (delta = 2) */ - ram[1] = 0x17; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 5, 2); -} - -static void test_addsource_bcd() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* bcd(byte(0)) * 100 + bcd(byte(1)) > 4990 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:b0xH0000*100_b0xH0001>4990"); - - /* 0*100+12 = 12 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 49*100+89 = 4989 */ - ram[0] = 0x49; - ram[1] = 0x89; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 49*100+99 = 4999 */ - ram[1] = 0x99; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 50*100+00 = 5000 */ - ram[0] = 0x50; - ram[1] = 0x00; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addsource_invert() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ~low4(0) + ~high4(0) > 20 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:~0xL0000_~0xU0000>20"); - - /* ~0 (F) + ~0 (F) = 1E */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* ~4 (B) + ~9 (6) = 11 */ - ram[0] = 0x49; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* ~9 (6) + ~9 (6) = 0C */ - ram[1] = 0x99; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* ~5 (A) + ~1 (E) = 18 */ - ram[0] = 0x51; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_subsource_mem_delta() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* -byte(0) + prior(byte(0)) = 10 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_d0xH0000=10"); - - /* -0 + 0 = 10 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* -5 + 0 = 10 */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* -15 + 5 = 10 */ - ram[0] = 15; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* -5 + 15 = 10 */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* -5 + 5 = 10 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_subsource_mem_prior() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* -byte(0) + prior(byte(0)) = 10 */ - assert_parse_condset(&condset, &memrefs, buffer, "B:0xH0000_p0xH0000=10"); - - /* -0 + 0 = 10 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* -5 + 0 = 10 */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* -15 + 5 = 10 */ - ram[0] = 15; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* -5 + 15 = 10 */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* -5 + 15 = 10 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addsource_unfinished() { - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[512]; - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=0_A:0xH0002=22"); - - ASSERT_NUM_EQUALS(condset->num_other_conditions, 1); - ASSERT_NUM_EQUALS(condset->num_indirect_conditions, 1); -} - -static void test_addsource_float_division() -{ - uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0x80, 0x40 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0) / remember(float(4)) > 0.5 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0000/f4.0_f0.0>f1.4"); - - /* 0 / 4.0 > 1.4 = false (0.00) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 6 / 4.0 > 1.4 = true (1.50) */ - ram[0] = 6; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 5 / 4.0 > 1.4 = false (1.25) */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 7 / 4.0 > 1.4 = true (1.75) */ - ram[0] = 7; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addsource_recall_float_division() { - uint8_t ram[] = {0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0x80, 0x40}; /* fF0004 = 4.0 */ - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0) / remember(float(4)) > 0.5 */ - assert_parse_condset(&condset, &memrefs, buffer, "K:fF0004_A:0xH0000/{recall}_f0.0>f1.4"); - - /* 0 / 4.0 > 1.4 = false (0.00) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 6 / 4.0 > 1.4 = true (1.50) */ - ram[0] = 6; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* 5 / 4.0 > 1.4 = false (1.25) */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 7 / 4.0 > 1.4 = true (1.75) */ - ram[0] = 7; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addsource_long_chain() { - /* assume 22 bytes per cond (10 for base, 11 for delta, 1 for separator) - * and approximately 320 bytes per cond when convered to data structures. - * 320+22 = 342. Round that up to 384 just to be safe. - */ - const size_t cond_count = 1500; - const size_t buffer_size = 384 * cond_count; - char* buffer = (char*)malloc(buffer_size); - char* ptr = buffer; - const char* memaddr = buffer; - rc_condset_t* condset; - rc_memrefs_t memrefs; - rc_parse_state_t parse; - clock_t start, end; - size_t i, len, remaining; - - ASSERT_PTR_NOT_NULL(buffer); - - remaining = buffer_size; - for (i = 0; i < cond_count; i++) { - len = snprintf(ptr, remaining, "A:0xH%04x_", (uint32_t)i); - remaining -= len; - ptr += len; - } - len = snprintf(ptr, remaining, "0=500"); - remaining -= len; - ptr += len; - for (i = 0; i < cond_count; i++) { - len = snprintf(ptr, remaining, "_A:d0xH%04x", (uint32_t)i); - remaining -= len; - ptr += len; - } - ptr += snprintf(ptr, remaining, "=499"); - - ptr = (char*)RC_ALIGN((size_t)ptr); - remaining = buffer_size - (ptr - buffer); - - rc_init_parse_state(&parse, ptr); - rc_init_parse_state_memrefs(&parse, &memrefs); - - start = clock(); - condset = rc_parse_condset(&memaddr, &parse); - end = clock(); - - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER(parse.offset, 0); - ASSERT_NUM_LESS(parse.offset, remaining); - ASSERT_PTR_NOT_NULL(condset); - - double elapsed = (double)(end - start) * 1000 / CLOCKS_PER_SEC; - /* this shouldn't take more than 20-40ms (depending on the machine its running on). - * allow up to 1 full second before this errors. it was taking over 10s when reported */ - ASSERT_NUM_LESS(elapsed, 1000); - - /* each condition and its delta should share a memref */ - ASSERT_NUM_EQUALS(rc_memrefs_count_memrefs(&memrefs), cond_count); - /* one modified memref should be generated for each condition past the first - * plus one for the final condition: cond_count - 1 + 1 */ - ASSERT_NUM_EQUALS(rc_memrefs_count_modified_memrefs(&memrefs), cond_count); - - free(buffer); -} - -static void test_addhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(4, byte(1) == 18 || low(4) == 6) */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18(2)_0xL0004=6(4)"); - - /* both conditions true, total not met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - - /* total hits met (two for each condition) */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - - /* target met for first, it stops incrementing, second continues */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - - /* reset hit counts */ - rc_reset_condset(condset); - - /* both conditions true, total not met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - - /* first condition not true, total not met*/ - ram[1] = 16; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 2); - - /* total met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 3); -} - -static void test_addhits_multiple() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(4, byte(1) == 18 || low(4) == 6) */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18(2)_0xL0004=6(4)_0xL0004=6(3)"); - - /* both conditions true, total not met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* total hits met (two for each condition), third condition not yet met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - - /* target met for first, it stops incrementing, second and third continue */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 3); -} - -static void test_addhits_no_target() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AddHits is not a substitution for OrNext */ - /* since the second condition doesn't have a target hit count, the hits tallied by the first condition are ignored */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=18_0xH0000=1"); - - /* first condition true, but ignored */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - - /* second condition true, overall is true */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - - /* second condition no longer true, overall is not true */ - ram[0] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 1); -} - -static void test_addhits_with_addsource() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(2, (byte(1) + byte(2) == 70) || byte(0) == 0) */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001_C:0xH0002=70_0xH0000=0(2)"); - - /* addsource (conditions 1 and 2) is true, condition 3 is true, total of two hits, overall is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* repeated(2, byte(0) == 0 || (byte(1) + byte(2) == 70)) */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0000=0_A:0xH0001=0_0xH0002=70(2)"); - - /* condition 1 is true, addsource (conditions 2 and 3) is true, total of two hits, overall is true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 1); -} - -static void test_subhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(4, low(4) == 6, deducting = repeated(2, byte(1) == 16)) */ - /* NOTE: cannot have SubHits without AddHits as there's no way to reach the final hit target - * if hits are subtracted but not added */ - assert_parse_condset(&condset, &memrefs, buffer, "D:0xH0001=16(2)_C:0xL0004=6_0=1(4)"); - - /* second condition true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 2); - - /* both conditions true 1+3 == 4, not -1+3 != 4, no trigger */ - ram[1] = 16; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 3); - - /* -2+4 != 4 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 4); - - /* first condition target met, -2+5 != 4 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 5); - - /* total met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 6); -} - -static void test_subhits_below_zero() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(4, low(4) == 6, deducting = repeated(2, byte(1) == 16)) */ - /* NOTE: cannot have SubHits without AddHits as there's no way to reach the final hit target - * if hits are subtracted but not added */ - assert_parse_condset(&condset, &memrefs, buffer, "D:0xH0001=18(2)_C:0xL0002=6_0=1(4)"); - - /* first condition true. -1 less than 0. target hit count is unsigned. - make sure comparison doesn't treat -1 as unsigned */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - - /* first condition target met */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - - /* both conditions true. takes 6 counts on second condition to reach hit target because - first condition is currently -2 */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 3); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 4); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 5); - - /* total met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 6); -} - -static void test_addhits_unfinished() { - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[512]; - - assert_parse_condset(&condset, &memrefs, buffer, "0xH0001=0_C:0xH0002=22"); - - ASSERT_NUM_EQUALS(condset->num_other_conditions, 2); -} - -static void test_addhits_float_coercion() { - uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F }; /* fF0004 = 1.5 */ - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* 0 + float(4) * 10 - prev(float(4)) * 10 + 0 + 0 = 1 */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0_A:fF0004*10_B:dfF0004*10_A:0_0=1"); - - /* float(4) = 1.5, prev(float(4)) = 0.0. 0+15-0+0+0=1 = false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* float(4) = 2.0, prev(float(4)) = 1.5. 0+20-15+0+0=1 = false */ - ram[6] = 0x00; - ram[7] = 0x40; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* float(4) = 2.1, prev(float(4)) = 2.0. 0+21-20+0+0=1 = true */ - ram[6] = 0x06; - ram[5] = 0x66; - ram[4] = 0x66; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_andnext() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(3, byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ - assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=20_N:0xH0002=20_0xH0003=20.3."); - - /* all conditions are false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* final condition is not enough */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* first two are true, still not enough */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* all three are true, tally hits. hits are tallied for each true statement starting with the first */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* middle condition not true, only first tallies a hit */ - ram[2] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* all three conditions are true */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - - /* third condition not true, first two tally hits */ - ram[3] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 2); - - /* all three conditions are true, hit target reached */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 5); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 3); - - /* hit target previously reached */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 6); - assert_hit_count(condset, 1, 5); - assert_hit_count(condset, 2, 3); - - /* second condition no longer true, only first condition tallied, hit target was previously met */ - ram[2] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 7); - assert_hit_count(condset, 1, 5); - assert_hit_count(condset, 2, 3); -} - -static void test_andnext_boundaries() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0000) == 0 && once(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) && byte(0x0000) == 0 */ - assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_0xH0003=20.1._0xH0000=0"); - - /* first and last condition are true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 1); - - /* final condition of AndNext chain is not enough */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 2); - - /* two conditions of AndNext chain are true, still not enough */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 4, 3); - - /* whole AndNext chain is true */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 4, 4); -} - -static void test_andnext_resetif() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0000) == 0 && never(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ - assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_R:0xH0003=20"); - - /* tally a hit */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* final condition of AndNext chain is not enough */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* two conditions of AndNext chain are true, still not enough */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* whole AndNext chain is true */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* middle condition not true */ - ram[2] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* whole AndNext chain is true */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* third condition not true */ - ram[3] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 0); - - /* whole AndNext chain is true */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); -} - -static void test_andnext_pauseif() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0000) == 0 && unless(byte(0x0001) == 20 && byte(0x0002) == 20 && byte(0x0003) == 20) */ - assert_parse_condset(&condset, &memrefs, buffer, "0xH0000=0_N:0xH0001=20_N:0xH0002=20_P:0xH0003=20"); - - /* tally a hit */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* final condition of AndNext chain is not enough */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* two conditions of AndNext chain are true, still not enough */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* whole AndNext chain is true */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 1); - - /* middle condition not true */ - ram[2] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); - assert_hit_count(condset, 3, 0); /* pauseif goes to 0 when not true */ - - /* whole AndNext chain is true */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 2); - assert_hit_count(condset, 3, 1); - - /* third condition not true */ - ram[3] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 5); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 3); - assert_hit_count(condset, 3, 0); /* pauseif goes to 0 when not true */ - - /* whole AndNext chain is true */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 5); - assert_hit_count(condset, 1, 5); - assert_hit_count(condset, 2, 4); - assert_hit_count(condset, 3, 1); -} - -static void test_andnext_addsource() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* once(byte(0x0001) + byte(0x0002) == 20 && byte(0x0003) == 20) */ - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH0001=0_N:0xH0002=20_0xH0003=20.1."); - - /* nothing true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* final condition true */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* conditions 2 and 3 true, but AddSource in condition 1 makes condition 2 not true */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* AddSource condition true via sum, whole set is true */ - ram[1] = ram[2] = 10; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); -} - -static void test_andnext_addhits() { - uint8_t ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(5, (byte(0) == 1 && byte(0x0001) > prev(byte(0x0001))) || byte(0) == 2 || 0 == 1) */ - assert_parse_condset(&condset, &memrefs, buffer, "N:0xH00=1_C:0xH01>d0xH01_N:0=1_0=1.2."); - - /* initialize delta */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* second part of AndNext is true, but first is still false */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* both parts of AndNext are true */ - ram[0] = 1; - ram[1] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); - - /* And Next true again, hit count should match target */ - ram[1] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - assert_hit_count(condset, 3, 0); -} - -static void test_andnext_between_addhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AndNext has higher priority than AddHits - * - * AddHits byte(0x0001) == 20 (2) - * AndNext byte(0x0002) == 20 (2) <-- hit count only applies to line 2, AddHits on line 1 modifies line 3 - * byte(0x0003) == 20 (4) - * - * The AndNext on line 2 will combine with line 3, not line 1, so the overall interpretation is: - * - * repeated(4, repeated(2, byte(0x0001) == 20) || (byte(0x0002) == 20 && byte(0x0003) == 20))) - */ - assert_parse_condset(&condset, &memrefs, buffer, "C:0xH0001=20.2._N:0xH0002=20.2._0xH0003=20.4."); - - /* nothing true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* final condition is not enough to trigger */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* second condition is true, but only has one hit, so won't increment third */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* first condition true, but not second, only first will increment */ - /* hits from first condition should not cause second condition to act true */ - ram[2] = 0; - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* all three conditions are true. the first has already hit its target hit count, the - * second and third will increment. the total of the first and third is only 3, so no trigger */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 1); - - /* third clause will tally again and set will be true */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); -} - -static void test_andnext_with_hits_chain() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AndNext has higher priority than AddHits - * - * AndNext byte(0x0001) == 20 (1) - * AndNext byte(0x0002) == 20 (1) - * byte(0x0003) == 20 (1) - * - * Line 1 must be true before line 2 can be true, which has to be true before line 3 - * - * a = once(byte(0x0001) == 20) - * b = once(a && byte(0x0002) == 20) - * c = once(b && byte(0x0003) == 20) - */ - assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=20.1._N:0xH0002=20.1._0xH0003=20.1."); - - /* nothing true */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* final condition is not enough to trigger */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* second condition is true, cut can't tally until the first is true */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* first condition is true, but not second, only first will increment */ - ram[2] = 0; - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* final condition cannot tally without the previous items in the chain */ - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* only second condition true. first is historically true, so second can tally */ - ram[3] = ram[1] = 0; - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* only final condition true, first two historically true, so can tally */ - ram[3] = 20; - ram[2] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* nothing true, but all historically true, overall still true */ - ram[3] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); -} - -static void test_andnext_changes_to() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0001) ~> 18 */ - assert_parse_condset(&condset, &memrefs, buffer, "N:0xH0001=18_d0xH0001!=18"); - - /* value already 18, initial delta value is 0, so its considered changed */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* value already 18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value no longer 18 */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value changes to 18 */ - ram[1] = 18; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* value already 18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_ornext() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* repeated(5, byte(0x0001) == 20 || byte(0x0002) == 20 || byte(0x0003) == 20) */ - assert_parse_condset(&condset, &memrefs, buffer, "O:0xH0001=20_O:0xH0002=20_0xH0003=20.6."); - - /* first condition is true, which chains to make the second and third conditions true */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 1); - - /* first and second are true, all but third should update, but only 1 hit each */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 2); - - /* all three true, only increment each once */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 3); - - /* only middle is true, first won't be incremented */ - ram[1] = ram[3] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 4); - - /* only last is true, only it will be incremented */ - ram[2] = 30; - ram[3] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 5); - - /* none are true */ - ram[3] = 30; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 4); - assert_hit_count(condset, 2, 5); - - /* first is true, hit target met, set is true */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 5); - assert_hit_count(condset, 2, 6); - - /* hit target met */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 5); - assert_hit_count(condset, 1, 6); - assert_hit_count(condset, 2, 6); -} - -static void test_andnext_ornext_interaction() { - uint8_t ram[] = {0, 0, 0, 0, 0}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* AndNext and OrNext are evaluated at each step: (((1 || 2) && 3) || 4) */ - assert_parse_condset(&condset, &memrefs, buffer, "O:0xH0001=1_N:0xH0002=1_O:0xH0003=1_0xH0004=1"); - - ram[3] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((0 || 0) && 0) || 0) = 0 */ - ram[4] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 0) || 1) = 1 */ - ram[3] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 1) || 1) = 1 */ - ram[4] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((0 || 0) && 1) || 0) = 0 */ - ram[2] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((0 || 0) && 1) || 0) = 1 */ - ram[1] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 0) = 1 */ - ram[2] = 0; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 0) = 1 */ - ram[3] = 0; assert_evaluate_condset(condset, memrefs, &memory, 0); /* (((1 || 0) && 0) || 0) = 0 */ - ram[4] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 0) || 1) = 1 */ - ram[3] = 1; assert_evaluate_condset(condset, memrefs, &memory, 1); /* (((1 || 0) && 1) || 1) = 1 */ -} - -static void test_addaddress_direct_pointer() { - uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0000 + byte(0xh0000)) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=22"); - - /* initially, byte(0x0000 + 1) == 22, false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* pointed-at value is correct */ - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to original value, still correct */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* original value no longer correct */ - ram[1] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addaddress_direct_pointer_delta() { - uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* delta(byte(0x0000 + byte(0xh0000))) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_d0xH0000=22"); - - /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - /* byte(0x0000 + 2); value=52, prev=22, prior=22 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0x0000 + 2); value=52, prev=52, prior=22 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to original value, still correct */ - /* byte(0x0000 + 1); value=22, prev=22, prior=52 */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* original value no longer correct */ - /* byte(0x0000 + 1); value=11, prev=22, prior=22 */ - ram[1] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0x0000 + 1); value=11, prev=11, prior=22 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* point to secondary value, which is correct */ - /* byte(0x0000 + 2); value=22, prev=11, prior=11 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=22, prev=22, prior=11 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_direct_pointer_prior() { - uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* delta(byte(0x0000 + byte(0xh0000))) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_p0xH0000=22"); - - /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* point to new value */ - /* byte(0x0000 + 2); value=52, prev=22, prior=22 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0x0000 + 2); value=52, prev=52, prior=22 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* new pointed-at value is correct */ - /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* point to original value, still correct */ - /* byte(0x0000 + 1); value=22, prev=22, prior=52 */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* original value no longer correct */ - /* byte(0x0000 + 1); value=11, prev=22, prior=22 */ - ram[1] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0x0000 + 1); value=11, prev=11, prior=22 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to secondary value, which is correct */ - /* byte(0x0000 + 2); value=22, prev=11, prior=11 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=22, prev=22, prior=11 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=32, prev=22, prior=22 */ - ram[2] = 32; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_direct_pointer_change() { - uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0000 + byte(0xh0000)) == 20 && delta(byte(0x0000 + byte(0xh0000))) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=20_I:0xH0000=0_d0xH0000=22"); - - /* byte(0x0000 + 1); value=18, prev=0, prior=0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=18, prev=18, prior=0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=22, prev=18, prior=18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=22, prev=22, prior=18 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 1); value=20, prev=22, prior=22 */ - ram[1] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0x0000 + 1); value=20, prev=20, prior=22 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* point to new value */ - /* byte(0x0000 + 2); value=52, prev=20, prior=20 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=52, prev=52, prior=20 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - /* byte(0x0000 + 2); value=22, prev=52, prior=52 */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=22, prev=22, prior=52 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* point to original value. looks correct! */ - /* byte(0x0000 + 1); value=20, prev=22, prior=22 */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to secondary value, which is not correct */ - /* byte(0x0000 + 2); value=22, prev=20, prior=20 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=22, prev=22, prior=20 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0x0000 + 2); value=20, prev=22, prior=22 */ - ram[2] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_indirect_pointer() { - uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0002 + byte(0xh0000)) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xH0002=22"); - - /* initially, byte(0x0001 + 1) == 22, false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* non-offset value is correct */ - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* pointed-at value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[4] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[0] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_indirect_pointer_negative() { - uint8_t ram[] = {0x02, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(byte(0xh0000) - 1) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xHFFFFFFFF=22"); - - /* initially, byte(0x0002 - 1) == 22, false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* non-offset value is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* pointed-at value is correct */ - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[0] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to invalid address */ - ram[0] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* point to already correct value */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_indirect_pointer_out_of_range() { - uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56, 0x16}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram) - 1; /* purposely hide ram[5] */ - - /* byte(0x0002 + byte(0xh0000)) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0xH0002=22"); - - /* pointed-at value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* way out of bounds */ - ram[0] = 100; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* boundary condition - ram[5] value is correct, but should be unreachable */ - /* NOTE: address validation must be handled by the registered 'peek' callback */ - ram[0] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addaddress_indirect_pointer_multiple() { - uint8_t ram[] = {0x01, 0x02, 0x03, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* the expectation is that the AddAddress lines will share rc_memref_value_t's, but the following lines - will generate their own rc_memref_value_t's for indirection. none of that is actually verified. */ - assert_parse_condset(&condset, &memrefs, buffer, - "I:0xH0000=0_0xH0002=22_I:0xH0000=0_0xH0003=23_I:0xH0001=0_0xH0003=24"); - /* $(0002 + $0000) == 22 && $(0003 + $0000) == 23 && $(0003 + $0001) == 24 */ - /* $0003 (0x34) == 22 && $0004 (0xAB) == 23 && $0005 (0x56) == 24 */ - - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 5, 0); - - /* first condition is true */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 3, 0); - assert_hit_count(condset, 5, 0); - - /* second condition is true */ - ram[4] = 23; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 3, 1); - assert_hit_count(condset, 5, 0); - - /* third condition is true */ - ram[5] = 24; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 3, 2); - assert_hit_count(condset, 5, 1); -} - -static void test_addaddress_indirect_pointer_with_delta() -{ - uint8_t ram[] = { 0x01, 0x02, 0x03, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(byte(0)*2+2) == 22 && prev(byte(byte(0)*2+2) == 20 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xH0002=22_I:0xH0000*2_d0xH0002=20"); - - /* byte(0) = 1, byte(1*2+2 [4]) = 0xAB, delta = 0 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 3, 0); - - /* byte(0) = 1, byte(1*2+2 [4]) = 22, delta = 0xAB */ - ram[4] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 3, 0); - - /* byte(0) = 1, byte(1*2+2 [4]) = 20, delta = 22 */ - ram[4] = 20; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 3, 0); - - /* byte(0) = 0, byte(0*2+2 [2]) = 2, delta = 20 */ - ram[0] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 3, 1); - - /* byte(0) = 0, byte(0*2+2 [2]) = 20, delta = 2 */ - ram[2] = 20; - ram[4] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 3, 1); - - /* byte(0) = 1, byte(1*2+2 [4]) = 22, delta = 20 */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 3, 2); -} - -static void test_addaddress_indirect_pointer_with_subsource() -{ - uint8_t ram[] = { 0x01, 0x02, 0x03, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(byte(0)*2+2) == 22 - prev(byte(byte(0)*2+2) == 20 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_B:d0xH0002_I:0xH0000*2_0xH0002=20"); - - /* byte(0) = 1, byte(1*2+2 [4]) = 0xAB, delta = 0, diff = 0xAB */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0) = 1, byte(1*2+2 [4]) = 0x22, delta = 0xAB, diff = 0x77 */ - ram[4] = 0x22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0) = 1, byte(1*2+2 [4]) = 0x36, delta = 0x22, diff = 0x14 */ - ram[4] = 0x36; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0) = 0, byte(0*2+2 [2]) = 0x03, delta = 0x36, diff = 0xCD */ - ram[0] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* byte(0) = 0, byte(0*2+2 [2]) = 0x17, delta = 0x03, diff = 0x14 */ - ram[2] = 0x17; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* byte(0) = 1, byte(1*2+2 [4]) = 0x2B, delta = 0x17, diff = 0x14 */ - ram[0] = 1; - ram[4] = 0x2B; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_indirect_pointer_with_subsource_float() -{ - uint8_t ram[] = { 0x02, 0x05, 0x00, 0x00, 0x42, 0x70, 0x00, 0x00, 0x41, 0xc4, 0x00, 0x00 }; - /* ^ ------ 60.0 ------ ^ ^ ------ 24.5 ------ ^ */ - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* float(byte(0) + 2) - float(byte(1) + 3) == 15.0 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0001_B:fB0003_I:0xH0000_fB0002=f15.0"); - - /* float(4) = 60.0, float(8) = 24.5, 60.0 - 24.5 = 35.5 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* float(4) = 60.0, float(8) = 45.0, 60.0 - 45.0 = 15.0 */ - ram[8] = 0x42; - ram[9] = 0x34; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* float(8) = 45.0, float(4) = 60.0, 45.0 - 60.0 = -15.0 */ - ram[0] = 0x06; - ram[1] = 0x01; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* float(8) = 24.5, float(4) = 60.0, 24.5 - 60.0 = -35.5 */ - ram[8] = 0x41; - ram[9] = 0xc4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* float(8) = 24.5, float(4) = 9.5, 24.5 - 9.5 = 15.0 */ - ram[4] = 0x41; - ram[5] = 0x18; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_indirect_pointer_from_memory() { - uint8_t ram[] = { 0x01, 0x01, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(byte(0x0000) + byte(0x0001)) == 22 --- byte(0x0000) = pointer, byte(0x0001) = index */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000+0xH0001_0xH0000=22"); - - /* initially, byte(1 + 1) == 22, false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* pointed-at value is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[1] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[4] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_indirect_constant() { - uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0002 + 1) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:1_0xH0002=22"); - - /* initially, byte(0x0003) == 22, false */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* memory at constant is correct */ - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* memory at address is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* memory at offset address is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_pointer_data_size_differs_from_pointer_size() { - uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0002 + word(0xh0000)) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000_0x 0002=22"); - - /* 8-bit pointed-at value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 16-bit pointed-at value is correct */ - ram[4] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to new value */ - ram[0] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is only partially correct */ - ram[3] = 0; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_double_indirection() { - uint8_t ram[] = {0x01, 0x02, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0000 + byte(0x0000 + byte(0x0000))) == 22 | $($($0000))) == 22*/ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_I:0xH0000=0_0xH0000=22"); - - /* value is correct: $0000=1, $0001=2, $0002 = 22 */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* second pointer in chain causes final pointer to point at address 3 */ - ram[1] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new pointed-at value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* first pointer points at address 2, which is 22, so out-of-bounds */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* second pointer points at address 3, which is correct */ - ram[2] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* first pointer is out of range, so returns 0 for the second pointer, $0 contains the correct value */ - ram[0] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_double_indirection_with_delta() { - uint8_t ram[] = { 0, 2, 4 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* prev(byte(0x0000 + byte(0x0000 + byte(0x0000)))) == 22 | prev($($($0000)))) == 22*/ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_I:0xH0000=0_d0xH0000=4"); - - /* NOTE: indirectly calculated memrefs keep their own delta, not the delta of the newly pointed to - * value. using the intermediate deltas to calculate an address for the current frame will - * generate incorrect values. Only the final item in the chain should have the delta. */ - - /* 1st frame: A = Mem[0] = 0 (delta[0] = 0), B = Mem[A] = Mem[0] = 0 (delta B = 0), C = Mem[B] = Mem[0] = 0 (delta C = 0) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 2nd frame: A = Mem[0] = 1 (delta[0] = 0), B = Mem[A] = Mem[1] = 2 (delta B = 0), C = Mem[B] = Mem[2] = 4 (delta C = 0) */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 3nd frame: A = Mem[0] = 1 (delta[0] = 1), B = Mem[A] = Mem[1] = 2 (delta B = 2), C = Mem[B] = Mem[2] = 4 (delta C = 4) */ - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_double_indirection_with_delta_incorrect() { - uint8_t ram[] = { 0, 2, 4 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* prev(byte(0x0000 + prev(byte(0x0000 + prev(byte(0x0000)))))) == 22 | prev($($($0000)))) == 22*/ - assert_parse_condset(&condset, &memrefs, buffer, "I:d0xH0000=0_I:d0xH0000=0_d0xH0000=4"); - - /* putting prevs on each step of the chain results in using old pointers to get to data that may not exist yet, - * but this validates that incorrect behavior */ - - /* 1st frame: Mem[0] = 0 (delta[0] = 0), B = Mem[deltaA] = Mem[0] = 0 (delta B = 0), C = Mem[deltaB] = Mem[0] = 0 (delta C = 0) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 2nd frame: Mem[0] = 1 (delta[0] = 0), B = Mem[deltaA] = Mem[0] = 1 (delta B = 0), C = Mem[deltaB] = Mem[0] = 1 (delta C = 0) */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 3rd frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 1), C = Mem[deltaB] = Mem[1] = 2 (delta C = 1) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 4th frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 2), C = Mem[deltaB] = Mem[2] = 4 (delta C = 2) */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* 5th frame: Mem[0] = 1 (delta[0] = 1), B = Mem[deltaA] = Mem[1] = 2 (delta B = 2), C = Mem[deltaB] = Mem[2] = 4 (delta C = 4) */ - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_adjust_both_sides() { - uint8_t ram[] = {0x02, 0x11, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* $($0) > delta $($0) */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000>d0xH0000"); - - /* initial delta will be 0, so 2 will be greater */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* delta should be the same as current */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value increased */ - ram[2]++; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* value decreased */ - ram[2]--; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* this is a small hiccup in the AddAddress behavior. when the pointer changes, we - * can't reasonably know the previous value, so delta will be 0 for the first frame. - * 52 is greater than 0 (even though it didn't change), so set will be true. */ - ram[0] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_adjust_both_sides_different_bases() { - uint8_t ram[] = {0x02, 0x11, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* $($0) == $($0 + 1) */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000=0_0xH0000=0xH0001"); - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* values are the same */ - ram[2] = ram[3]; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* adjust pointer */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* values are the same */ - ram[1] = ram[2]; - assert_evaluate_condset(condset, memrefs, &memory, 1); -} - -static void test_addaddress_scaled() { - uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* $($0 * 2) */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xH0000=22"); - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value is correct */ - ram[2] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* adjust pointer */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new value is correct */ - ram[4] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to original value */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* original value no longer correct */ - ram[2] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addaddress_scaled_negative() { - uint8_t ram[] = {0x01, 0x12, 0x34, 0xAB, 0x01}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* $($4 * -1 + 2) */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0004*4294967295_0xH0002=22"); /* 4294967295 = 0xFFFFFFFF = -1 */ - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value is correct: $(1 * -1 + 2) = $(1) */ - ram[1] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* adjust pointer: $(2 * -1 + 2) = $(0) */ - ram[4] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new value is correct */ - ram[0] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to original value */ - ram[4] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* original value no longer correct */ - ram[1] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addaddress_double_read() { - uint8_t ram[] = {0x01, 0x02, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* $($0 + $1) == 22 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000+0xH0001_0xH0000=22"); - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value is correct */ - ram[3] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* adjust first address */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* new value is correct */ - ram[4] = 22; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* point to original value */ - ram[1] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* original value no longer correct */ - ram[3] = 11; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_addaddress_shared_size() { - uint8_t ram[] = { 0x01, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* bit2(byte(0) * 2) == 1 */ - assert_parse_condset(&condset, &memrefs, buffer, "I:0xH0000*2_0xO0000=1"); - - /* bit2( [0x34] ) = 1 */ - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* bit2( [0x01] ) = 0 */ - ram[2] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* adjust pointer. bit2( [0x56] ) = 1 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 1); - - /* new value is correct */ - ram[4] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); -} - -static void test_prior_sequence() { - uint8_t ram[] = {0x00}; - memory_t memory; - rc_condset_t* condset; - rc_memrefs_t memrefs; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* prior(bit0(0))==1 && prior(bit1(0))==1 && prior(bit2(0))==1 */ - assert_parse_condset(&condset, &memrefs, buffer, "p0xM0000=1_p0xN0000=1_p0xO0000=1"); - assert_evaluate_condset(condset, memrefs, &memory, 0); - - /* value ~> 1 [0001], all priors still 0 */ - ram[0] = 1; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 0); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* value ~> 2 [0010], prior(bit0(0)) = 1, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ - ram[0] = 2; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* value ~> 3 [0011], prior(bit0(0)) = 0, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ - ram[0] = 3; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 1); - assert_hit_count(condset, 1, 0); - assert_hit_count(condset, 2, 0); - - /* value ~> 4 [0100], prior(bit0(0)) = 1, prior(bit1(0)) = 1, prior(bit2(0)) = 0 */ - ram[0] = 4; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 1); - assert_hit_count(condset, 2, 0); - - /* value ~> 5 [0101], prior(bit0(0)) = 0, prior(bit1(0)) = 1, prior(bit2(0)) = 0 */ - ram[0] = 5; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 2); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - - /* value ~> 6 [0110], prior(bit0(0)) = 1, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ - ram[0] = 6; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - - /* value ~> 7 [0111], prior(bit0(0)) = 0, prior(bit1(0)) = 0, prior(bit2(0)) = 0 */ - ram[0] = 7; - assert_evaluate_condset(condset, memrefs, &memory, 0); - assert_hit_count(condset, 0, 3); - assert_hit_count(condset, 1, 2); - assert_hit_count(condset, 2, 0); - - /* value ~> 8 [1000], prior(bit0(0)) = 1, prior(bit1(0)) = 1, prior(bit2(0)) = 1 */ - ram[0] = 8; - assert_evaluate_condset(condset, memrefs, &memory, 1); - assert_hit_count(condset, 0, 4); - assert_hit_count(condset, 1, 3); - assert_hit_count(condset, 2, 1); -} - -static void test_get_real_operand1(void) -{ - rc_condset_t* condset; - rc_condition_t* condition; - const rc_operand_t* operand; - rc_memrefs_t memrefs; - char buffer[2048]; - - assert_parse_condset(&condset, &memrefs, buffer, "A:0xH1234*100_A:0xH1235*10_M:0xH1236>850"); - - condition = condset->conditions; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 0x1234); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 0x1235); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 1); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 0x1236); - ASSERT_NUM_EQUALS(operand->is_combining, 0); -} - -static void test_get_real_operand1_recall(void) -{ - rc_condset_t* condset; - rc_condition_t* condition; - const rc_operand_t* operand; - rc_memrefs_t memrefs; - char buffer[2048]; - - assert_parse_condset(&condset, &memrefs, buffer, "A:1_K:0xH0001_I:{recall}_I:0xH0002_0xH0003=4"); - - condition = condset->conditions; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_CONST); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_32_BITS); - ASSERT_NUM_EQUALS(operand->value.num, 1); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 1); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 1); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_RECALL); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_32_BITS); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 2); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 3); - ASSERT_NUM_EQUALS(operand->is_combining, 0); -} - -static void test_get_real_operand1_indirect_static() -{ - rc_condset_t* condset; - rc_condition_t* condition; - const rc_operand_t* operand; - rc_memrefs_t memrefs; - char buffer[2048]; - - assert_parse_condset(&condset, &memrefs, buffer, "I:8_I:0xH0010_0xH0020=2"); - - condition = condset->conditions; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_CONST); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_32_BITS); - ASSERT_NUM_EQUALS(operand->value.num, 8); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 0x0010); - ASSERT_NUM_EQUALS(operand->is_combining, 0); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.is_combining, 0); - operand = rc_condition_get_real_operand1(condition); - ASSERT_NUM_EQUALS(operand->type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(operand->size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(operand->value.memref->address, 0x0020); - ASSERT_NUM_EQUALS(operand->is_combining, 0); -} - -static void test_ignore_parse_errors(const char* memaddr, uint8_t is_value, int32_t expected_error) -{ - const char* memaddr_test; - - rc_preparse_state_t preparse; - rc_init_preparse_state(&preparse); - preparse.parse.is_value = is_value; - - memaddr_test = memaddr; - rc_parse_condset(&memaddr_test, &preparse.parse); - ASSERT_NUM_EQUALS(preparse.parse.offset, expected_error); - rc_destroy_preparse_state(&preparse); - - rc_init_preparse_state(&preparse); - preparse.parse.is_value = is_value; - preparse.parse.ignore_non_parse_errors = 1; - - memaddr_test = memaddr; - rc_parse_condset(&memaddr_test, &preparse.parse); - ASSERT_NUM_GREATER(preparse.parse.offset, 0); - rc_destroy_preparse_state(&preparse); -} - -void test_condset(void) { - TEST_SUITE_BEGIN(); - - /* hit counts */ - TEST(test_hitcount_increment_when_true); - TEST(test_hitcount_does_not_increment_when_false); - TEST(test_hitcount_target); - - /* two conditions */ - TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001=18_0xH0002=52", 1, 1, 1); - TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001=18_0xH0002!=52", 0, 1, 0); - TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001>18_0xH0002=52", 0, 0, 1); - TEST_PARAMS4(test_hitcount_two_conditions, "0xH0001<18_0xH0002>52", 0, 0, 0); - - /* three conditions */ - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002=52_0xL0004=6", 1, 1, 1, 1); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002=52_0xL0004>6", 0, 1, 1, 0); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002<52_0xL0004=6", 0, 1, 0, 1); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001=18_0xH0002<52_0xL0004>6", 0, 1, 0, 0); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002=52_0xL0004=6", 0, 0, 1, 1); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002=52_0xL0004>6", 0, 0, 1, 0); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002<52_0xL0004=6", 0, 0, 0, 1); - TEST_PARAMS5(test_hitcount_three_conditions, "0xH0001>18_0xH0002<52_0xL0004>6", 0, 0, 0, 0); - - /* pauseif */ - TEST(test_pauseif); - TEST(test_pauseif_hitcount_one); - TEST(test_pauseif_hitcount_two); - TEST(test_pauseif_hitcount_with_reset); - TEST(test_pauseif_resetnextif); - TEST(test_pauseif_does_not_increment_hits); - TEST(test_pauseif_delta_updated); - TEST(test_pauseif_indirect_delta_updated); - TEST(test_pauseif_short_circuit); - - /* resetif */ - TEST(test_resetif); - TEST(test_resetif_cond_with_hittarget); - TEST(test_resetif_hitcount); - TEST(test_resetif_hitcount_one); - TEST(test_resetif_hitcount_addhits); - - TEST(test_pauseif_resetif_hitcounts); - - /* resetnextif */ - TEST(test_resetnextif); - TEST(test_resetnextif_non_hitcount_condition); - TEST(test_resetnextif_addhits); - TEST(test_resetnextif_addhits_chain); - TEST(test_resetnextif_addhits_chain_total); - TEST(test_resetnextif_using_andnext); - TEST(test_resetnextif_andnext); - TEST(test_resetnextif_andnext_chain); - TEST(test_resetnextif_andnext_hitchain); - TEST(test_resetnextif_addaddress); - TEST(test_resetnextif_addaddress_andnext_chain); - TEST(test_resetnextif_chain); - TEST(test_resetnextif_chain_andnext); - TEST(test_resetnextif_chain_with_hits); - TEST(test_resetnextif_pause_lock); - TEST(test_resetnextif_unfinished); - - /* addsource/subsource */ - TEST(test_addsource); - TEST(test_addsource_overflow); - TEST(test_subsource); - TEST(test_subsource_legacy_garbage); - TEST(test_subsource_overflow); - TEST(test_addsource_subsource); - TEST(test_addsource_multiply); - TEST(test_subsource_multiply); - TEST(test_addsource_multiply_fraction); - TEST(test_addsource_multiply_address); - TEST(test_addsource_divide); - TEST(test_addsource_divide_address); - TEST(test_addsource_divide_self); - TEST(test_subsource_divide); - TEST(test_addsource_compare_percentage); - TEST(test_addsource_mask); - TEST(test_addsource_xor); - TEST(test_addsource_float_first); - TEST(test_addsource_float_second); - TEST(test_subsource_mask); - TEST(test_subsource_overflow_comparison_equal); - TEST(test_subsource_overflow_comparison_greater); - TEST(test_subsource_overflow_comparison_greater_or_equal); - TEST(test_subsource_overflow_comparison_lesser); - TEST(test_subsource_overflow_comparison_lesser_or_equal); - TEST(test_subsource_float); - TEST(test_addsource_bits_change); - TEST(test_addsource_bcd); - TEST(test_addsource_invert); - TEST(test_subsource_mem_delta); - TEST(test_subsource_mem_prior); - TEST(test_addsource_unfinished); - TEST(test_addsource_float_division); - TEST(test_addsource_recall_float_division); - TEST(test_addsource_long_chain); - - /* addhits/subhits */ - TEST(test_addhits); - TEST(test_addhits_no_target); - TEST(test_addhits_with_addsource); - TEST(test_addhits_multiple); - TEST(test_subhits); - TEST(test_subhits_below_zero); - TEST(test_addhits_unfinished); - TEST(test_addhits_float_coercion) - - /* andnext */ - TEST(test_andnext); - TEST(test_andnext_boundaries); - TEST(test_andnext_resetif); - TEST(test_andnext_pauseif); - TEST(test_andnext_addsource); - TEST(test_andnext_addhits); - TEST(test_andnext_between_addhits); - TEST(test_andnext_with_hits_chain); - TEST(test_andnext_changes_to); - - /* ornext */ - TEST(test_ornext); - TEST(test_andnext_ornext_interaction); - - /* addaddress */ - TEST(test_addaddress_direct_pointer); - TEST(test_addaddress_direct_pointer_delta); - TEST(test_addaddress_direct_pointer_prior); - TEST(test_addaddress_direct_pointer_change); - TEST(test_addaddress_indirect_pointer); - TEST(test_addaddress_indirect_pointer_negative); - TEST(test_addaddress_indirect_pointer_out_of_range); - TEST(test_addaddress_indirect_pointer_multiple); - TEST(test_addaddress_indirect_pointer_with_delta); - TEST(test_addaddress_indirect_pointer_with_subsource); - TEST(test_addaddress_indirect_pointer_with_subsource_float); - TEST(test_addaddress_indirect_pointer_from_memory); - TEST(test_addaddress_indirect_constant); - TEST(test_addaddress_pointer_data_size_differs_from_pointer_size); - TEST(test_addaddress_double_indirection); - TEST(test_addaddress_double_indirection_with_delta); - TEST(test_addaddress_double_indirection_with_delta_incorrect); - TEST(test_addaddress_adjust_both_sides); - TEST(test_addaddress_adjust_both_sides_different_bases); - TEST(test_addaddress_scaled); - TEST(test_addaddress_scaled_negative); - TEST(test_addaddress_double_read); - TEST(test_addaddress_shared_size); - - /* prior */ - TEST(test_prior_sequence); - - /* get_real_operand1 */ - TEST(test_get_real_operand1); - TEST(test_get_real_operand1_recall); - TEST(test_get_real_operand1_indirect_static); - - /* ignore parse errors */ - TEST_PARAMS3(test_ignore_parse_errors, "M:0x1234=5_M:0x1234=6", 0, RC_MULTIPLE_MEASURED); - TEST_PARAMS3(test_ignore_parse_errors, "M:0x1234", 0, RC_INVALID_OPERATOR); /* right side required for Measured non-value */ - TEST_PARAMS3(test_ignore_parse_errors, "M:0x1234=0x2345", 0, RC_INVALID_MEASURED_TARGET); - TEST_PARAMS3(test_ignore_parse_errors, "0x1234=6", 1, RC_INVALID_VALUE_FLAG); - TEST_PARAMS3(test_ignore_parse_errors, "T:0x1234=6", 1, RC_INVALID_VALUE_FLAG); - TEST_PARAMS3(test_ignore_parse_errors, "R:0x1234", 0, RC_INVALID_OPERATOR); - TEST_PARAMS3(test_ignore_parse_errors, "K:0x1234<6", 0, RC_INVALID_OPERATOR); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_consoleinfo.c b/src/rcheevos/test/rcheevos/test_consoleinfo.c deleted file mode 100644 index f761016fdb..0000000000 --- a/src/rcheevos/test/rcheevos/test_consoleinfo.c +++ /dev/null @@ -1,203 +0,0 @@ -#include "rc_consoles.h" - -#include "../test_framework.h" - -static void test_name(uint32_t console_id, const char* expected_name) -{ - ASSERT_STR_EQUALS(rc_console_name(console_id), expected_name); -} - -static void test_memory(uint32_t console_id, uint32_t expected_total_memory, uint32_t expected_searchable_memory) -{ - const rc_memory_regions_t* regions = rc_console_memory_regions(console_id); - uint32_t total_memory = 0; - uint32_t searchable_memory = 0; - uint32_t max_address = 0; - uint32_t i; - ASSERT_PTR_NOT_NULL(regions); - - if (expected_total_memory == 0) - { - ASSERT_NUM_EQUALS(regions->num_regions, 0); - return; - } - - ASSERT_NUM_GREATER(regions->num_regions, 0); - for (i = 0; i < regions->num_regions; ++i) { - uint32_t region_size = (regions->region[i].end_address - regions->region[i].start_address + 1); - total_memory += region_size; - - if (regions->region[i].type != RC_MEMORY_TYPE_UNUSED) - searchable_memory += region_size; - - if (regions->region[i].end_address > max_address) - max_address = regions->region[i].end_address; - - ASSERT_PTR_NOT_NULL(regions->region[i].description); - } - - ASSERT_NUM_EQUALS(total_memory, expected_total_memory); - ASSERT_NUM_EQUALS(searchable_memory, expected_searchable_memory); - ASSERT_NUM_EQUALS(max_address, expected_total_memory - 1); -} - -void test_consoleinfo(void) { - TEST_SUITE_BEGIN(); - - /* use raw numbers instead of constants to ensure constants don't change */ - TEST_PARAMS2(test_name, 0, "Unknown"); - TEST_PARAMS2(test_name, 1, "Sega Genesis"); - TEST_PARAMS2(test_name, 2, "Nintendo 64"); - TEST_PARAMS2(test_name, 3, "Super Nintendo Entertainment System"); - TEST_PARAMS2(test_name, 4, "GameBoy"); - TEST_PARAMS2(test_name, 5, "GameBoy Advance"); - TEST_PARAMS2(test_name, 6, "GameBoy Color"); - TEST_PARAMS2(test_name, 7, "Nintendo Entertainment System"); - TEST_PARAMS2(test_name, 8, "PC Engine"); - TEST_PARAMS2(test_name, 9, "Sega CD"); - TEST_PARAMS2(test_name, 10, "Sega 32X"); - TEST_PARAMS2(test_name, 11, "Master System"); - TEST_PARAMS2(test_name, 12, "PlayStation"); - TEST_PARAMS2(test_name, 13, "Atari Lynx"); - TEST_PARAMS2(test_name, 14, "Neo Geo Pocket"); - TEST_PARAMS2(test_name, 15, "Game Gear"); - TEST_PARAMS2(test_name, 16, "GameCube"); - TEST_PARAMS2(test_name, 17, "Atari Jaguar"); - TEST_PARAMS2(test_name, 18, "Nintendo DS"); - TEST_PARAMS2(test_name, 19, "Wii"); - TEST_PARAMS2(test_name, 20, "Wii-U"); - TEST_PARAMS2(test_name, 21, "PlayStation 2"); - TEST_PARAMS2(test_name, 22, "XBOX"); - TEST_PARAMS2(test_name, 23, "Magnavox Odyssey 2"); - TEST_PARAMS2(test_name, 24, "Pokemon Mini"); - TEST_PARAMS2(test_name, 25, "Atari 2600"); - TEST_PARAMS2(test_name, 26, "MS-DOS"); - TEST_PARAMS2(test_name, 27, "Arcade"); - TEST_PARAMS2(test_name, 28, "Virtual Boy"); - TEST_PARAMS2(test_name, 29, "MSX"); - TEST_PARAMS2(test_name, 30, "Commodore 64"); - TEST_PARAMS2(test_name, 31, "ZX-81"); - TEST_PARAMS2(test_name, 32, "Oric"); - TEST_PARAMS2(test_name, 33, "SG-1000"); - TEST_PARAMS2(test_name, 34, "VIC-20"); - TEST_PARAMS2(test_name, 35, "Amiga"); - TEST_PARAMS2(test_name, 36, "Atari ST"); - TEST_PARAMS2(test_name, 37, "Amstrad CPC"); - TEST_PARAMS2(test_name, 38, "Apple II"); - TEST_PARAMS2(test_name, 39, "Sega Saturn"); - TEST_PARAMS2(test_name, 40, "Dreamcast"); - TEST_PARAMS2(test_name, 41, "PlayStation Portable"); - TEST_PARAMS2(test_name, 42, "CD-I"); - TEST_PARAMS2(test_name, 43, "3DO"); - TEST_PARAMS2(test_name, 44, "ColecoVision"); - TEST_PARAMS2(test_name, 45, "Intellivision"); - TEST_PARAMS2(test_name, 46, "Vectrex"); - TEST_PARAMS2(test_name, 47, "PC-8000/8800"); - TEST_PARAMS2(test_name, 48, "PC-9800"); - TEST_PARAMS2(test_name, 49, "PC-FX"); - TEST_PARAMS2(test_name, 50, "Atari 5200"); - TEST_PARAMS2(test_name, 51, "Atari 7800"); - TEST_PARAMS2(test_name, 52, "X68K"); - TEST_PARAMS2(test_name, 53, "WonderSwan"); - TEST_PARAMS2(test_name, 54, "CassetteVision"); - TEST_PARAMS2(test_name, 55, "Super CassetteVision"); - TEST_PARAMS2(test_name, 56, "Neo Geo CD"); - TEST_PARAMS2(test_name, 57, "Fairchild Channel F"); - TEST_PARAMS2(test_name, 58, "FM Towns"); - TEST_PARAMS2(test_name, 59, "ZX Spectrum"); - TEST_PARAMS2(test_name, 60, "Game & Watch"); - TEST_PARAMS2(test_name, 61, "Nokia N-Gage"); - TEST_PARAMS2(test_name, 62, "Nintendo 3DS"); - TEST_PARAMS2(test_name, 63, "Watara Supervision"); - TEST_PARAMS2(test_name, 64, "Sharp X1"); - TEST_PARAMS2(test_name, 65, "TIC-80"); - TEST_PARAMS2(test_name, 66, "Thomson TO8"); - TEST_PARAMS2(test_name, 67, "PC-6000"); - TEST_PARAMS2(test_name, 68, "Sega Pico"); - TEST_PARAMS2(test_name, 69, "Mega Duck"); - TEST_PARAMS2(test_name, 70, "Zeebo"); - TEST_PARAMS2(test_name, 71, "Arduboy"); - TEST_PARAMS2(test_name, 72, "WASM-4"); - TEST_PARAMS2(test_name, 73, "Arcadia 2001"); - TEST_PARAMS2(test_name, 74, "Interton VC 4000"); - TEST_PARAMS2(test_name, 75, "Elektor TV Games Computer"); - TEST_PARAMS2(test_name, 76, "PC Engine CD"); - TEST_PARAMS2(test_name, 77, "Atari Jaguar CD"); - TEST_PARAMS2(test_name, 78, "Nintendo DSi"); - TEST_PARAMS2(test_name, 79, "TI-83"); - TEST_PARAMS2(test_name, 80, "Uzebox"); - TEST_PARAMS2(test_name, 81, "Famicom Disk System"); - TEST_PARAMS2(test_name, 82, "Unknown"); - - TEST_PARAMS2(test_name, 100, "Hubs"); - TEST_PARAMS2(test_name, 101, "Events"); - TEST_PARAMS2(test_name, 102, "Standalone"); - - /* memory maps */ - TEST_PARAMS3(test_memory, RC_CONSOLE_UNKNOWN, 0x000000, 0x000000); - TEST_PARAMS3(test_memory, RC_CONSOLE_3DO, 0x200000, 0x200000); - TEST_PARAMS3(test_memory, RC_CONSOLE_AMIGA, 0x100000, 0x100000); - TEST_PARAMS3(test_memory, RC_CONSOLE_AMSTRAD_PC, 0x090000, 0x090000); - TEST_PARAMS3(test_memory, RC_CONSOLE_APPLE_II, 0x020000, 0x020000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ARCADE, 0x000000, 0x000000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ARCADIA_2001, 0x000300, 0x000300); - TEST_PARAMS3(test_memory, RC_CONSOLE_ARDUBOY, 0x000F00, 0x000F00); - TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_2600, 0x000080, 0x000080); - TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_7800, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_JAGUAR, 0x200000, 0x200000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_JAGUAR_CD, 0x200000, 0x200000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ATARI_LYNX, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_COLECOVISION, 0x008400, 0x008400); - TEST_PARAMS3(test_memory, RC_CONSOLE_COMMODORE_64, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_DREAMCAST, 0x01000000, 0x01000000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, 0x001800, 0x001700); - TEST_PARAMS3(test_memory, RC_CONSOLE_FAIRCHILD_CHANNEL_F, 0x010C40, 0x010C40); - TEST_PARAMS3(test_memory, RC_CONSOLE_FAMICOM_DISK_SYSTEM, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_GAMEBOY, 0x034000, 0x02DFA0); - TEST_PARAMS3(test_memory, RC_CONSOLE_GAMEBOY_COLOR, 0x034000, 0x033FA0); - TEST_PARAMS3(test_memory, RC_CONSOLE_GAMEBOY_ADVANCE, 0x058000, 0x058000); - TEST_PARAMS3(test_memory, RC_CONSOLE_GAMECUBE, 0x01800000, 0x01800000); - TEST_PARAMS3(test_memory, RC_CONSOLE_GAME_GEAR, 0x00A000, 0x00A000); - TEST_PARAMS3(test_memory, RC_CONSOLE_INTELLIVISION, 0x040080, 0x03BC00); - TEST_PARAMS3(test_memory, RC_CONSOLE_INTERTON_VC_4000, 0x000600, 0x000600); - TEST_PARAMS3(test_memory, RC_CONSOLE_MAGNAVOX_ODYSSEY2, 0x000140, 0x000140); - TEST_PARAMS3(test_memory, RC_CONSOLE_MASTER_SYSTEM, 0x00A000, 0x00A000); - TEST_PARAMS3(test_memory, RC_CONSOLE_MEGA_DRIVE, 0x020000, 0x020000); - TEST_PARAMS3(test_memory, RC_CONSOLE_MEGADUCK, 0x010000, 0x00FFA0); - TEST_PARAMS3(test_memory, RC_CONSOLE_MSX, 0x080000, 0x080000); - TEST_PARAMS3(test_memory, RC_CONSOLE_MS_DOS, 0x4200000, 0x4140000); - TEST_PARAMS3(test_memory, RC_CONSOLE_NEOGEO_POCKET, 0x004000, 0x004000); - TEST_PARAMS3(test_memory, RC_CONSOLE_NEO_GEO_CD, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO_64, 0x800000, 0x800000); - TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO_DS, 0x01004000, 0x00404000); - TEST_PARAMS3(test_memory, RC_CONSOLE_NINTENDO_DSI, 0x01004000, 0x01004000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ORIC, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_PC8800, 0x011000, 0x011000); - TEST_PARAMS3(test_memory, RC_CONSOLE_PC_ENGINE, 0x02000, 0x02000); - TEST_PARAMS3(test_memory, RC_CONSOLE_PC_ENGINE_CD, 0x42800, 0x42800); - TEST_PARAMS3(test_memory, RC_CONSOLE_PCFX, 0x210000, 0x210000); - TEST_PARAMS3(test_memory, RC_CONSOLE_PLAYSTATION, 0x200400, 0x200400); - TEST_PARAMS3(test_memory, RC_CONSOLE_PLAYSTATION_2, 0x02004000, 0x02004000); - TEST_PARAMS3(test_memory, RC_CONSOLE_PSP, 0x02000000, 0x02000000); - TEST_PARAMS3(test_memory, RC_CONSOLE_POKEMON_MINI, 0x002000, 0x002000); - TEST_PARAMS3(test_memory, RC_CONSOLE_SATURN, 0x200000, 0x200000); - TEST_PARAMS3(test_memory, RC_CONSOLE_SEGA_32X, 0x060000, 0x060000); - TEST_PARAMS3(test_memory, RC_CONSOLE_SEGA_CD, 0x0B0000, 0x0B0000); - TEST_PARAMS3(test_memory, RC_CONSOLE_SG1000, 0x006000, 0x006000); - TEST_PARAMS3(test_memory, RC_CONSOLE_SUPER_CASSETTEVISION, 0x010000, 0x00B000); - TEST_PARAMS3(test_memory, RC_CONSOLE_SUPER_NINTENDO, 0x0A0800, 0x0A0800); - TEST_PARAMS3(test_memory, RC_CONSOLE_SUPERVISION, 0x006000, 0x006000); - TEST_PARAMS3(test_memory, RC_CONSOLE_THOMSONTO8, 0x080000, 0x080000); - TEST_PARAMS3(test_memory, RC_CONSOLE_TI83, 0x08000, 0x08000); - TEST_PARAMS3(test_memory, RC_CONSOLE_TIC80, 0x018000, 0x018000); - TEST_PARAMS3(test_memory, RC_CONSOLE_UZEBOX, 0x001000, 0x001000); - TEST_PARAMS3(test_memory, RC_CONSOLE_WASM4, 0x010000, 0x010000); - TEST_PARAMS3(test_memory, RC_CONSOLE_WII, 0x14000000, 0x05800000); - TEST_PARAMS3(test_memory, RC_CONSOLE_WONDERSWAN, 0x090000, 0x090000); - TEST_PARAMS3(test_memory, RC_CONSOLE_VECTREX, 0x000400, 0x000400); - TEST_PARAMS3(test_memory, RC_CONSOLE_VIRTUAL_BOY, 0x020000, 0x020000); - TEST_PARAMS3(test_memory, RC_CONSOLE_ZX_SPECTRUM, 0x020000, 0x020000); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_format.c b/src/rcheevos/test/rcheevos/test_format.c deleted file mode 100644 index adf27efe72..0000000000 --- a/src/rcheevos/test/rcheevos/test_format.c +++ /dev/null @@ -1,112 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" - -static void test_format_value(int format, int value, const char* expected) { - char buffer[64]; - int result; - - result = rc_format_value(buffer, sizeof(buffer), value, format); - ASSERT_STR_EQUALS(buffer, expected); - ASSERT_NUM_EQUALS(result, strlen(expected)); -} - -static void test_parse_format(const char* format, int expected) { - ASSERT_NUM_EQUALS(rc_parse_format(format), expected); -} - -void test_format(void) { - TEST_SUITE_BEGIN(); - - /* rc_format_value */ - TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, 12345, "12,345"); - TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, -12345, "-12,345"); - TEST_PARAMS3(test_format_value, RC_FORMAT_VALUE, 0xFFFFFFFF, "-1"); - TEST_PARAMS3(test_format_value, RC_FORMAT_UNSIGNED_VALUE, 0xFFFFFFFF, "4,294,967,295"); - TEST_PARAMS3(test_format_value, RC_FORMAT_UNFORMATTED, 0xFFFFFFFF, "4294967295"); /* UNFORMATTED does not add commas */ - TEST_PARAMS3(test_format_value, RC_FORMAT_SCORE, 12345, "012345"); /* SCORE does not add commas */ - TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 45, "0:45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 345, "5:45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS, 12345, "3h25:45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 345, "0:03.45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 12345, "2:03.45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_CENTISECS, 1234567, "3h25:45.67"); - TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 45, "0h00"); - TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 345, "0h05"); - TEST_PARAMS3(test_format_value, RC_FORMAT_SECONDS_AS_MINUTES, 12345, "3h25"); - TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 45, "0h45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 345, "5h45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_MINUTES, 12345, "205h45"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 345, "0:05.75"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 12345, "3:25.75"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 1234567, "5h42:56.11"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, 0, "0.0"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, 1, "0.1"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, 1234, "123.4"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED1, -1234, "-123.4"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, 0, "0.00"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, 1, "0.01"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, 1234, "12.34"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED2, -1234, "-12.34"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, 0, "0.000"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, 1, "0.001"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, 1234, "1.234"); - TEST_PARAMS3(test_format_value, RC_FORMAT_FIXED3, -1234, "-1.234"); - TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, 0, "0"); - TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, 1, "10"); - TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, 1234, "12,340"); - TEST_PARAMS3(test_format_value, RC_FORMAT_TENS, -1234, "-12,340"); - TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, 0, "0"); - TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, 1, "100"); - TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, 1234, "123,400"); - TEST_PARAMS3(test_format_value, RC_FORMAT_HUNDREDS, -1234, "-123,400"); - TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, 0, "0"); - TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, 1, "1,000"); - TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, 1234, "1,234,000"); - TEST_PARAMS3(test_format_value, RC_FORMAT_THOUSANDS, -1234, "-1,234,000"); - - /* because of the internal conversion to centiseconds, anything above MAX_INT / 10 could overflow */ - TEST_PARAMS3(test_format_value, RC_FORMAT_FRAMES, 0x19999999, "1,988h24:38.81"); - - /* rc_parse_format */ - TEST_PARAMS2(test_parse_format, "VALUE", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "SECS", RC_FORMAT_SECONDS); - TEST_PARAMS2(test_parse_format, "TIMESECS", RC_FORMAT_SECONDS); - TEST_PARAMS2(test_parse_format, "TIME", RC_FORMAT_FRAMES); - TEST_PARAMS2(test_parse_format, "MINUTES", RC_FORMAT_MINUTES); - TEST_PARAMS2(test_parse_format, "SECS_AS_MINS", RC_FORMAT_SECONDS_AS_MINUTES); - TEST_PARAMS2(test_parse_format, "FRAMES", RC_FORMAT_FRAMES); - TEST_PARAMS2(test_parse_format, "SCORE", RC_FORMAT_SCORE); - TEST_PARAMS2(test_parse_format, "POINTS", RC_FORMAT_SCORE); - TEST_PARAMS2(test_parse_format, "MILLISECS", RC_FORMAT_CENTISECS); - TEST_PARAMS2(test_parse_format, "TENS", RC_FORMAT_TENS); - TEST_PARAMS2(test_parse_format, "HUNDREDS", RC_FORMAT_HUNDREDS); - TEST_PARAMS2(test_parse_format, "THOUSANDS", RC_FORMAT_THOUSANDS); - TEST_PARAMS2(test_parse_format, "UNSIGNED", RC_FORMAT_UNSIGNED_VALUE); - TEST_PARAMS2(test_parse_format, "OTHER", RC_FORMAT_SCORE); - TEST_PARAMS2(test_parse_format, "INVALID", RC_FORMAT_VALUE); - - TEST_PARAMS2(test_parse_format, "FLOAT", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FLOAT0", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FLOAT1", RC_FORMAT_FLOAT1); - TEST_PARAMS2(test_parse_format, "FLOAT2", RC_FORMAT_FLOAT2); - TEST_PARAMS2(test_parse_format, "FLOAT3", RC_FORMAT_FLOAT3); - TEST_PARAMS2(test_parse_format, "FLOAT4", RC_FORMAT_FLOAT4); - TEST_PARAMS2(test_parse_format, "FLOAT5", RC_FORMAT_FLOAT5); - TEST_PARAMS2(test_parse_format, "FLOAT6", RC_FORMAT_FLOAT6); - TEST_PARAMS2(test_parse_format, "FLOAT7", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FLOAT10", RC_FORMAT_VALUE); - - TEST_PARAMS2(test_parse_format, "FIXED", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FIXED0", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FIXED1", RC_FORMAT_FIXED1); - TEST_PARAMS2(test_parse_format, "FIXED2", RC_FORMAT_FIXED2); - TEST_PARAMS2(test_parse_format, "FIXED3", RC_FORMAT_FIXED3); - TEST_PARAMS2(test_parse_format, "FIXED4", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FIXED5", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FIXED6", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FIXED7", RC_FORMAT_VALUE); - TEST_PARAMS2(test_parse_format, "FIXED10", RC_FORMAT_VALUE); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_lboard.c b/src/rcheevos/test/rcheevos/test_lboard.c deleted file mode 100644 index fe568c3c8a..0000000000 --- a/src/rcheevos/test/rcheevos/test_lboard.c +++ /dev/null @@ -1,746 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -static void _assert_parse_lboard(rc_lboard_t** lboard, void* buffer, const char* memaddr) -{ - int size; - unsigned* overflow; - - size = rc_lboard_size(memaddr); - ASSERT_NUM_GREATER(size, 0); - - overflow = (unsigned*)(((char*)buffer) + size); - *overflow = 0xCDCDCDCD; - - *lboard = rc_parse_lboard(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(*lboard); - - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } -} -#define assert_parse_lboard(lboard, buffer, memaddr) ASSERT_HELPER(_assert_parse_lboard(lboard, buffer, memaddr), "assert_parse_lboard") - -static int evaluate_lboard(rc_lboard_t* lboard, memory_t* memory, int* value) { - return rc_evaluate_lboard(lboard, value, peek, memory, NULL); -} - -static void test_simple_leaderboard() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02"); - ASSERT_NUM_EQUALS(lboard->state, RC_LBOARD_STATE_WAITING); - - /* submit is true, but leaderboard has not started */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); /* value is only calculated in STARTED and TRIGGERED states */ - - /* cancel is true - still not started */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* start is true - will activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x34); - - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x34); - - /* cancel is true - will deactivate */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* submit is true, but leaderboard is not active */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* start is true - will activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x34); - - /* submit is true - will submit */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 0x34); -} - -static void test_start_and_cancel_same_frame() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=18::SUB:0xH00=3::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* start and cancel are both true - should not start */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* cancel no longer true - should start */ - ram[1] = 0x13; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* start and cancel are both true - should cancel */ - ram[1] = 0x12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - - /* cancel no longer true, but start still is - shouldn't activate */ - ram[1] = 0x13; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_INACTIVE); - - /* start no longer true - can activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* start true - should start */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); -} - -static void test_start_and_submit_same_frame() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* start and submit are both true - should trigger */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 0x34); - - /* disable submit - leaderboard should not start */ - ram[1] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_INACTIVE); - - /* disable start - leaderboard can activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* enable start - leaderboard should start */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); -} - -static void test_start_and_submit_same_frame_hitcount() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0=1::SUB:1=1::VAL:M:0xH02=52"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* start and submit are both true - should trigger. value logic is true, submit one hit */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 1); - - /* disable start - leaderboard can activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* reactivate leaderboard - it should immediately submit again. value logic is still true, submit one hit */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 1); - - /* disable start - leaderboard can activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* reactivate leaderboard - it should immediately submit again. value logic is not true, submit zero hits */ - ram[2] = 55; - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 0); -} - -static void test_start_and_conditions() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0_0xH01=0::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* only first start condition true - should not start */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* only second start condition true - should not start */ - ram[0] = 1; - ram[1] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* both conditions true - should start */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); -} - -static void test_start_or_conditions() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:S0xH00=1S0xH01=1::CAN:0xH01=10::SUB:0xH01=18::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* neither start condition true - should not start */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* only second start condition true - should start */ - ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* reset lboard state */ - ram[1] = 0; - lboard->state = RC_LBOARD_STATE_ACTIVE; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - - /* only first start condition true - should start */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); -} - -static void test_start_resets_value() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH01=0::CAN:0xH01=10::SUB:0xH01=18::VAL:M:0xH02!=d0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* not started */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* start condition true - should start */ - ram[1] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); - - /* tally a couple hits */ - ram[2]++; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); - - ram[2]++; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 2); - - /* canceled, hitcount kept, but ignored */ - ram[1] = 10; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* must wait one frame to switch back to active */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* restarted, hitcount should be reset */ - ram[1] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); - - /* tally a hit */ - ram[2]++; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); -} - -static void test_cancel_or_conditions() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:S0xH01=12S0xH02=12::SUB:0xH00=3::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* start condition true - should start */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* second cancel condition true - should cancel */ - ram[2] = 12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - - /* reset lboard state */ - ram[2] = 0; - lboard->state = RC_LBOARD_STATE_ACTIVE; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* first cancel condition true - should cancel */ - ram[1] = 12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); -} - -static void test_submit_and_conditions() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:0xH01=18_0xH03=18::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* only first submit condition is true - should not submit */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* only second submit condition true - should not submit */ - ram[1] = 0; - ram[3] = 18; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* both conditions true - should submit */ - ram[1] = 18; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); -} - -static void test_submit_or_conditions() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH01=10::SUB:S0xH01=12S0xH03=12::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* neither start condition true - should not submit */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* only second submit condition true - should submit */ - ram[1] = 12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - - /* reset lboard state */ - ram[1] = 0; - lboard->state = RC_LBOARD_STATE_ACTIVE; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - - /* only first submit condition true - should submit */ - ram[3] = 12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); -} - -static void test_progress() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* start true - should start - value from PRO */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x56); - - /* submit true - should trigger - value from VAL */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 0x34); -} - -static void test_value_from_hitcount() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02!=d0xH02"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* not started, value should not be tallied */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - ram[2] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* started, value will not be tallied as it hasn't changed */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); - - /* value changed, expect tally */ - ram[2] = 11; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); - - /* not changed, no tally */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); - - /* changed, tally */ - ram[2] = 12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 2); - - /* canceled, expect no value */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* waiting to start, expect no value */ - ram[2] = 13; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* restarted, tally should be reset */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); - - /* value changed, expect tally */ - ram[2] = 11; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); -} - -static void test_value_from_addhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:C:0xH03=1_M:0xH02=1"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* started, nothing to tally */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); - - /* second value tallied*/ - ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); - - /* both values tallied */ - ram[3] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 3); - - /* only first value tallied */ - ram[2] = 12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 4); - - /* canceled, expect no value */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* waiting to start, expect no value */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* restarted, tally should be reset, but first is still true, so it'll be tallied */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 1); -} - -static void test_value_from_float() { - /* bytes 5-8 are the float value for pi */ - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:fF0005"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* started, nothing to tally */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 3); - - /* canceled, expect no value */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* waiting to start, expect no value */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* restarted */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 3); -} - -static void test_value_from_float_scaled() { - /* bytes 5-8 are the float value for pi */ - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:fF0005*100"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* started, nothing to tally */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 314); - - /* canceled, expect no value */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* waiting to start, expect no value */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* restarted */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 314); -} - -static void test_value_from_division() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02/2"); - ASSERT_NUM_EQUALS(lboard->state, RC_LBOARD_STATE_WAITING); - - /* submit is true, but leaderboard has not started */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); /* value is only calculated in STARTED and TRIGGERED states */ - - /* cancel is true - still not started */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* start is true - will activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x1a); - - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x1a); - - /* cancel is true - will deactivate */ - ram[0] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(value, 0); - - /* submit is true, but leaderboard is not active */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(value, 0); - - /* start is true - will activate */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x1a); - - /* submit is true - will submit */ - ram[0] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(value, 0x1a); -} - -static void test_maximum_value_from_conditions() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_lboard_t* lboard; - char buffer[1024]; - int value; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_lboard(&lboard, buffer, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:Q:0xH01=1_M:0x 02$Q:0xH01=2_M:0x 03"); - lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* started, neither value is active */ - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); - - /* first value is active */ - ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0xAB34); - - /* second value is active */ - ram[1] = 2; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x56AB); - - /* value updated */ - ram[3] = 0x12; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0x5612); - - /* neither value is active */ - ram[1] = 3; - ASSERT_NUM_EQUALS(evaluate_lboard(lboard, &memory, &value), RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(value, 0); -} - -static void test_measured_value_and_condition() -{ - rc_lboard_t* lboard; - char buffer[1024]; - - /* a Measured is irrelevant in the STA/CAN/SUB conditions, but if present, allow them to be unique */ - assert_parse_lboard(&lboard, buffer, "STA:M:0xH00=0::CAN:M:0xH00=2::SUB:M:0xH00=3::VAL:M:0xH04"); -} - -static void test_unparsable_lboard(const char* memaddr, int expected_error) { - ASSERT_NUM_EQUALS(rc_lboard_size(memaddr), expected_error); -} - -static void test_unparsable_strings() { - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::GARBAGE", RC_INVALID_LBOARD_FIELD); - TEST_PARAMS2(test_unparsable_lboard, "CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02", RC_MISSING_START); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::SUB:0xH00=3::PRO:0xH04::VAL:0xH02", RC_MISSING_CANCEL); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::PRO:0xH04::VAL:0xH02", RC_MISSING_SUBMIT); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04", RC_MISSING_VALUE); - TEST_PARAMS2(test_unparsable_lboard, "STA:::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_MISSING_START); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:::SUB:0xH00=3::VAL:0xH02", RC_MISSING_CANCEL); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:::VAL:0xH02", RC_MISSING_SUBMIT); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:", RC_MISSING_VALUE); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::STA:0=0", RC_DUPLICATED_START); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::CAN:0=0", RC_DUPLICATED_CANCEL); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::SUB:0=0", RC_DUPLICATED_SUBMIT); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::VAL:0", RC_DUPLICATED_VALUE); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::PRO:0xH04::VAL:0xH02::PRO:0", RC_DUPLICATED_PROGRESS); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH01=1_M:0xH01=2", RC_MULTIPLE_MEASURED); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH01=1_T:0xH01=2", RC_INVALID_VALUE_FLAG); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1_0xH01=2", RC_INVALID_VALUE_FLAG); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1", RC_MISSING_VALUE_MEASURED); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:R:0xH01=1$M:0xH03", RC_MISSING_VALUE_MEASURED); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=0::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02SM:0xH03", RC_INVALID_VALUE_FLAG); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=A::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_MEMORY_OPERAND); - - /* "STA:0xH00=1" is valid, but that leaves the read pointer pointing at the "A", which is not "::", so a generic - * invalid field error is returned. */ - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1A::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_LBOARD_FIELD); - - /* Missing '_' causes "0xH00=10" to be valid, but that leaves the read pointer pointing at the "x", which is not - * "::", so a generic invalid field error is returned. */ - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=10xH01=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02", RC_INVALID_LBOARD_FIELD); - - /* Garbage following value field (legacy format conversion will return invalid comparison) */ - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02=1=2", RC_INVALID_COMPARISON); - TEST_PARAMS2(test_unparsable_lboard, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:M:0xH02=1=2", RC_INVALID_LBOARD_FIELD); -} - -void test_lboard(void) { - TEST_SUITE_BEGIN(); - - TEST(test_simple_leaderboard); - TEST(test_start_and_cancel_same_frame); - TEST(test_start_and_submit_same_frame); - TEST(test_start_and_submit_same_frame_hitcount); - - TEST(test_start_and_conditions); - TEST(test_start_or_conditions); - - TEST(test_start_resets_value); - - TEST(test_cancel_or_conditions); - - TEST(test_submit_and_conditions); - TEST(test_submit_or_conditions); - - TEST(test_progress); - - TEST(test_value_from_hitcount); - TEST(test_value_from_addhits); - TEST(test_value_from_float); - TEST(test_value_from_float_scaled); - TEST(test_value_from_division); - TEST(test_maximum_value_from_conditions); - TEST(test_measured_value_and_condition); - - test_unparsable_strings(); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_memref.c b/src/rcheevos/test/rcheevos/test_memref.c deleted file mode 100644 index 3749ab3bf9..0000000000 --- a/src/rcheevos/test/rcheevos/test_memref.c +++ /dev/null @@ -1,520 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -#include -#include /* pow */ - -static void test_mask(char size, uint32_t expected) -{ - ASSERT_NUM_EQUALS(rc_memref_mask(size), expected); -} - -static void test_shared_masks(void) -{ - TEST_PARAMS2(test_mask, RC_MEMSIZE_8_BITS, 0x000000ff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_0, 0x00000001); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_1, 0x00000002); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_2, 0x00000004); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_3, 0x00000008); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_4, 0x00000010); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_5, 0x00000020); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_6, 0x00000040); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BIT_7, 0x00000080); - TEST_PARAMS2(test_mask, RC_MEMSIZE_LOW, 0x0000000f); - TEST_PARAMS2(test_mask, RC_MEMSIZE_HIGH, 0x000000f0); - TEST_PARAMS2(test_mask, RC_MEMSIZE_BITCOUNT, 0x000000ff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_16_BITS, 0x0000ffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_16_BITS_BE, 0x0000ffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_24_BITS, 0x00ffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_24_BITS_BE, 0x00ffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_32_BITS, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_32_BITS_BE, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_FLOAT, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_FLOAT_BE, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_DOUBLE32, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_DOUBLE32_BE, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_MBF32, 0xffffffff); - TEST_PARAMS2(test_mask, RC_MEMSIZE_VARIABLE, 0xffffffff); -} - -static void test_shared_size(char size, char expected) -{ - ASSERT_NUM_EQUALS(rc_memref_shared_size(size), expected); -} - -static void test_shared_sizes(void) -{ - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_8_BITS, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_0, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_1, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_2, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_3, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_4, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_5, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_6, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BIT_7, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_LOW, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_HIGH, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_BITCOUNT, RC_MEMSIZE_8_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_16_BITS, RC_MEMSIZE_16_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_16_BITS_BE, RC_MEMSIZE_16_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_24_BITS, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_24_BITS_BE, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_32_BITS, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_32_BITS_BE, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_FLOAT, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_FLOAT_BE, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_DOUBLE32, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_DOUBLE32_BE, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_MBF32, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_MBF32_LE, RC_MEMSIZE_32_BITS); - TEST_PARAMS2(test_shared_size, RC_MEMSIZE_VARIABLE, RC_MEMSIZE_32_BITS); -} - -static void test_transform(uint32_t value, uint8_t size, uint32_t expected) -{ - rc_typed_value_t typed_value; - typed_value.type = RC_VALUE_TYPE_UNSIGNED; - typed_value.value.u32 = value; - rc_transform_memref_value(&typed_value, size); - ASSERT_NUM_EQUALS(typed_value.value.u32, expected); -} - -static void test_transform_float(uint32_t value, uint8_t size, double expected) -{ - rc_typed_value_t typed_value; - typed_value.type = RC_VALUE_TYPE_UNSIGNED; - typed_value.value.u32 = value; - rc_transform_memref_value(&typed_value, size); - ASSERT_FLOAT_EQUALS(typed_value.value.f32, expected); -} - -static double rc_round(double n) -{ - return floor(n + 0.5); /* no round() in c89 */ -} - -static void test_transform_double32(uint32_t value, uint8_t size, double expected) -{ - rc_typed_value_t typed_value; - typed_value.type = RC_VALUE_TYPE_UNSIGNED; - typed_value.value.u32 = value; - rc_transform_memref_value(&typed_value, size); - - /* a 20-bit mantissa only has 6 digits of precision. round to 6 digits, then do a float comparison. */ - if (fabs(expected) != 0.0) { - const double digits = floor(log10(fabs(expected))) + 1; - const double expected_pow = pow(10, 6 - digits); - expected = rc_round(expected * expected_pow) / expected_pow; - typed_value.value.f32 = (float)(rc_round(typed_value.value.f32 * expected_pow) / expected_pow); - } - - ASSERT_FLOAT_EQUALS(typed_value.value.f32, expected); -} - -static void test_transform_float_inf(uint32_t value, uint8_t size) -{ - /* C89 does not provide defines for NAN and INFINITY, nor does it provide isnan() or isinf() functions */ - rc_typed_value_t typed_value; - typed_value.type = RC_VALUE_TYPE_UNSIGNED; - typed_value.value.u32 = value; - rc_transform_memref_value(&typed_value, size); - - if (typed_value.value.f32 < FLT_MAX) { - /* infinity will be greater than max float value */ - ASSERT_FAIL("result of transform is not infinity") - } -} - -static void test_transform_float_nan(uint32_t value, uint8_t size) -{ - /* C89 does not provide defines for NAN and INFINITY, nor does it provide isnan() or isinf() functions */ - rc_typed_value_t typed_value; - typed_value.type = RC_VALUE_TYPE_UNSIGNED; - typed_value.value.u32 = value; - rc_transform_memref_value(&typed_value, size); - - if (typed_value.value.f32 == typed_value.value.f32) { - /* NaN cannot be compared, will fail equality check with itself */ - ASSERT_FAIL("result of transform is not NaN") - } -} - -static void test_transforms(void) -{ - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_8_BITS, 0x00000078); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_16_BITS, 0x00005678); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_24_BITS, 0x00345678); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_32_BITS, 0x12345678); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_LOW, 0x00000008); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_HIGH, 0x00000007); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_BITCOUNT, 0x00000004); /* only counts bits in lowest byte */ - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_16_BITS_BE, 0x00007856); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_24_BITS_BE, 0x00785634); - TEST_PARAMS3(test_transform, 0x12345678, RC_MEMSIZE_32_BITS_BE, 0x78563412); - - TEST_PARAMS3(test_transform, 0x00000001, RC_MEMSIZE_BIT_0, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000002, RC_MEMSIZE_BIT_1, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000004, RC_MEMSIZE_BIT_2, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000008, RC_MEMSIZE_BIT_3, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000010, RC_MEMSIZE_BIT_4, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000020, RC_MEMSIZE_BIT_5, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000040, RC_MEMSIZE_BIT_6, 0x00000001); - TEST_PARAMS3(test_transform, 0x00000080, RC_MEMSIZE_BIT_7, 0x00000001); - - TEST_PARAMS3(test_transform, 0x000000FE, RC_MEMSIZE_BIT_0, 0x00000000); - TEST_PARAMS3(test_transform, 0x000000FD, RC_MEMSIZE_BIT_1, 0x00000000); - TEST_PARAMS3(test_transform, 0x000000FB, RC_MEMSIZE_BIT_2, 0x00000000); - TEST_PARAMS3(test_transform, 0x000000F7, RC_MEMSIZE_BIT_3, 0x00000000); - TEST_PARAMS3(test_transform, 0x000000EF, RC_MEMSIZE_BIT_4, 0x00000000); - TEST_PARAMS3(test_transform, 0x000000DF, RC_MEMSIZE_BIT_5, 0x00000000); - TEST_PARAMS3(test_transform, 0x000000BF, RC_MEMSIZE_BIT_6, 0x00000000); - TEST_PARAMS3(test_transform, 0x0000007F, RC_MEMSIZE_BIT_7, 0x00000000); - - TEST_PARAMS3(test_transform_float, 0x3F800000, RC_MEMSIZE_FLOAT, 1.0); - TEST_PARAMS3(test_transform_float, 0x41460000, RC_MEMSIZE_FLOAT, 12.375); - TEST_PARAMS3(test_transform_float, 0x42883EFA, RC_MEMSIZE_FLOAT, 68.123); - TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_FLOAT, 0.0); - TEST_PARAMS3(test_transform_float, 0x80000000, RC_MEMSIZE_FLOAT, -0.0); - TEST_PARAMS3(test_transform_float, 0xC0000000, RC_MEMSIZE_FLOAT, -2.0); - TEST_PARAMS3(test_transform_float, 0x40490FDB, RC_MEMSIZE_FLOAT, 3.14159274101257324); - TEST_PARAMS3(test_transform_float, 0x3EAAAAAB, RC_MEMSIZE_FLOAT, 0.333333334326744076); - TEST_PARAMS3(test_transform_float, 0x429A4492, RC_MEMSIZE_FLOAT, 77.133926); - TEST_PARAMS3(test_transform_float, 0x4350370A, RC_MEMSIZE_FLOAT, 208.214996); - TEST_PARAMS3(test_transform_float, 0x45AE36E9, RC_MEMSIZE_FLOAT, 5574.863770); - TEST_PARAMS3(test_transform_float, 0x58635FA9, RC_MEMSIZE_FLOAT, 1000000000000000.0); - TEST_PARAMS3(test_transform_float, 0x24E69595, RC_MEMSIZE_FLOAT, 0.0000000000000001); - TEST_PARAMS3(test_transform_float, 0x000042B4, RC_MEMSIZE_FLOAT, 2.39286e-41); - TEST_PARAMS2(test_transform_float_inf, 0x7F800000, RC_MEMSIZE_FLOAT); - TEST_PARAMS2(test_transform_float_nan, 0x7FFFFFFF, RC_MEMSIZE_FLOAT); - - TEST_PARAMS3(test_transform_double32, 0x3FF00000, RC_MEMSIZE_DOUBLE32, 1.0); - TEST_PARAMS3(test_transform_double32, 0x4028C000, RC_MEMSIZE_DOUBLE32, 12.375); - TEST_PARAMS3(test_transform_double32, 0x405107DF, RC_MEMSIZE_DOUBLE32, 68.123); - TEST_PARAMS3(test_transform_double32, 0x00000000, RC_MEMSIZE_DOUBLE32, 0.0); - TEST_PARAMS3(test_transform_double32, 0x80000000, RC_MEMSIZE_DOUBLE32, -0.0); - TEST_PARAMS3(test_transform_double32, 0xC0000000, RC_MEMSIZE_DOUBLE32, -2.0); - TEST_PARAMS3(test_transform_double32, 0x400921FB, RC_MEMSIZE_DOUBLE32, 3.14159274101257324); - TEST_PARAMS3(test_transform_double32, 0x3FD55555, RC_MEMSIZE_DOUBLE32, 0.333333334326744076); - TEST_PARAMS3(test_transform_double32, 0x40534892, RC_MEMSIZE_DOUBLE32, 77.133926); - TEST_PARAMS3(test_transform_double32, 0x406A06E1, RC_MEMSIZE_DOUBLE32, 208.214996); - TEST_PARAMS3(test_transform_double32, 0x40B5C6DD, RC_MEMSIZE_DOUBLE32, 5574.863770); - TEST_PARAMS3(test_transform_double32, 0x430C6BF5, RC_MEMSIZE_DOUBLE32, 1000000000000000.0); - TEST_PARAMS3(test_transform_double32, 0x3C9CD2B2, RC_MEMSIZE_DOUBLE32, 0.0000000000000001); - TEST_PARAMS3(test_transform_double32, 0x3780AD01, RC_MEMSIZE_DOUBLE32, 2.39286e-41); - TEST_PARAMS3(test_transform_double32, 0x3FF3C0CA, RC_MEMSIZE_DOUBLE32, 1.234568); - TEST_PARAMS2(test_transform_float_inf, 0x7FF00000, RC_MEMSIZE_DOUBLE32); - TEST_PARAMS2(test_transform_float_nan, 0x7FFFFFFF, RC_MEMSIZE_DOUBLE32); - - TEST_PARAMS3(test_transform_double32, 0x000000C0, RC_MEMSIZE_DOUBLE32_BE, -2.0); - TEST_PARAMS3(test_transform_double32, 0x00003840, RC_MEMSIZE_DOUBLE32_BE, 24.0); - TEST_PARAMS3(test_transform_double32, 0xCAC0F33F, RC_MEMSIZE_DOUBLE32_BE, 1.234568); - TEST_PARAMS3(test_transform_double32, 0xFB210940, RC_MEMSIZE_DOUBLE32_BE, 3.14159274101257324); - - TEST_PARAMS3(test_transform_float, 0x0000803F, RC_MEMSIZE_FLOAT_BE, 1.0); - TEST_PARAMS3(test_transform_float, 0x00004641, RC_MEMSIZE_FLOAT_BE, 12.375); - TEST_PARAMS3(test_transform_float, 0xFA3E8842, RC_MEMSIZE_FLOAT_BE, 68.123); - TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_FLOAT_BE, 0.0); - TEST_PARAMS3(test_transform_float, 0x00000080, RC_MEMSIZE_FLOAT_BE, -0.0); - TEST_PARAMS3(test_transform_float, 0x000000C0, RC_MEMSIZE_FLOAT_BE, -2.0); - TEST_PARAMS3(test_transform_float, 0xDB0F4940, RC_MEMSIZE_FLOAT_BE, 3.14159274101257324); - TEST_PARAMS3(test_transform_float, 0xABAAAA3E, RC_MEMSIZE_FLOAT_BE, 0.333333334326744076); - TEST_PARAMS3(test_transform_float, 0x92449A42, RC_MEMSIZE_FLOAT_BE, 77.133926); - TEST_PARAMS3(test_transform_float, 0x0A375043, RC_MEMSIZE_FLOAT_BE, 208.214996); - TEST_PARAMS3(test_transform_float, 0xE936AE45, RC_MEMSIZE_FLOAT_BE, 5574.863770); - TEST_PARAMS3(test_transform_float, 0xA95F6358, RC_MEMSIZE_FLOAT_BE, 1000000000000000.0); - TEST_PARAMS3(test_transform_float, 0x9595E624, RC_MEMSIZE_FLOAT_BE, 0.0000000000000001); - TEST_PARAMS3(test_transform_float, 0xB4420000, RC_MEMSIZE_FLOAT_BE, 2.39286e-41); - TEST_PARAMS2(test_transform_float_inf, 0x0000807F, RC_MEMSIZE_FLOAT_BE); - TEST_PARAMS2(test_transform_float_nan, 0xFFFFFF7F, RC_MEMSIZE_FLOAT_BE); - - /* MBF values are stored big endian (at least on Apple II), so will be byteswapped - * when passed to rc_transform_memref_value. MBF doesn't support infinity or NaN. */ - TEST_PARAMS3(test_transform_float, 0x00000081, RC_MEMSIZE_MBF32, 1.0); /* 81 00 00 00 */ - TEST_PARAMS3(test_transform_float, 0x00002084, RC_MEMSIZE_MBF32, 10.0); /* 84 20 00 00 */ - TEST_PARAMS3(test_transform_float, 0x00004687, RC_MEMSIZE_MBF32, 99.0); /* 87 46 00 00 */ - TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_MBF32, 0.0); /* 00 00 00 00 */ - TEST_PARAMS3(test_transform_float, 0x00000080, RC_MEMSIZE_MBF32, 0.5); /* 80 00 00 00 */ - TEST_PARAMS3(test_transform_float, 0x00008082, RC_MEMSIZE_MBF32, -2.0); /* 82 80 00 00 */ - TEST_PARAMS3(test_transform_float, 0xF3043581, RC_MEMSIZE_MBF32, 1.41421354); /* 81 34 04 F3 */ - TEST_PARAMS3(test_transform_float, 0xDA0F4982, RC_MEMSIZE_MBF32, 3.14159256); /* 82 49 0F DA */ - TEST_PARAMS3(test_transform_float, 0xDB0F4983, RC_MEMSIZE_MBF32, 6.28318548); /* 83 49 0F DB */ - - /* Some flavors of BASIC (notably Locomotive BASIC on the Amstrad CPC) use the native endian-ness of - * the system for their MBF values, so we support both MBF32 (big endian) and MBF32_LE (little endian). - * Also note that Amstrad BASIC and Apple II BASIC both use MBF40, but since MBF40 just adds 8 extra bits - * of significance as the end of the MBF32 value, we can discard those as we convert to a 32-bit float. */ - TEST_PARAMS3(test_transform_float, 0x81000000, RC_MEMSIZE_MBF32_LE, 1.0); /* 00 00 00 81 */ - TEST_PARAMS3(test_transform_float, 0x84200000, RC_MEMSIZE_MBF32_LE, 10.0); /* 00 00 20 84 */ - TEST_PARAMS3(test_transform_float, 0x87460000, RC_MEMSIZE_MBF32_LE, 99.0); /* 00 00 46 87 */ - TEST_PARAMS3(test_transform_float, 0x00000000, RC_MEMSIZE_MBF32_LE, 0.0); /* 00 00 00 00 */ - TEST_PARAMS3(test_transform_float, 0x80000000, RC_MEMSIZE_MBF32_LE, 0.5); /* 00 00 00 80 */ - TEST_PARAMS3(test_transform_float, 0x82800000, RC_MEMSIZE_MBF32_LE, -2.0); /* 00 00 80 82 */ - TEST_PARAMS3(test_transform_float, 0x813504F3, RC_MEMSIZE_MBF32_LE, 1.41421354); /* F3 04 34 81 */ - TEST_PARAMS3(test_transform_float, 0x82490FDA, RC_MEMSIZE_MBF32_LE, 3.14159256); /* DA 0F 49 82 */ - TEST_PARAMS3(test_transform_float, 0x83490FDB, RC_MEMSIZE_MBF32_LE, 6.28318548); /* DB 0F 49 83 */ -} - -static int get_memref_count(rc_parse_state_t* parse) { - return rc_memrefs_count_memrefs(parse->memrefs) + rc_memrefs_count_modified_memrefs(parse->memrefs); -} - -static void test_allocate_shared_address() { - rc_parse_state_t parse; - rc_memrefs_t memrefs; - rc_init_parse_state(&parse, NULL); - rc_init_parse_state_memrefs(&parse, &memrefs); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 1); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* differing size will not match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 2); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW); /* differing size will not match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 3); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* differing size will not match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 4); - - rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* differing address will not match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); /* match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); - - rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); - - rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* match */ - ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); - - rc_destroy_parse_state(&parse); -} - -static void test_allocate_shared_address2() { - rc_parse_state_t parse; - rc_memrefs_t memrefs; - rc_memref_t* memref1; - rc_memref_t* memref2; - rc_memref_t* memref3; - rc_memref_t* memref4; - rc_memref_t* memref5; - rc_memref_t* memrefX; - rc_init_parse_state(&parse, NULL); - rc_init_parse_state_memrefs(&parse, &memrefs); - - memref1 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(memref1->address, 1); - ASSERT_NUM_EQUALS(memref1->value.size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(memref1->value.memref_type, RC_MEMREF_TYPE_MEMREF); - ASSERT_NUM_EQUALS(memref1->value.type, RC_VALUE_TYPE_UNSIGNED); - ASSERT_NUM_EQUALS(memref1->value.value, 0); - ASSERT_NUM_EQUALS(memref1->value.changed, 0); - ASSERT_NUM_EQUALS(memref1->value.prior, 0); - - memref2 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* differing size will not match */ - memref3 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW); /* differing size will not match */ - memref4 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* differing size will not match */ - memref5 = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* differing address will not match */ - - memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); /* match */ - ASSERT_PTR_EQUALS(memrefX, memref1); - - memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_16_BITS); /* match */ - ASSERT_PTR_EQUALS(memrefX, memref2); - - memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_LOW); /* match */ - ASSERT_PTR_EQUALS(memrefX, memref3); - - memrefX = rc_alloc_memref(&parse, 1, RC_MEMSIZE_BIT_2); /* match */ - ASSERT_PTR_EQUALS(memrefX, memref4); - - memrefX = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); /* match */ - ASSERT_PTR_EQUALS(memrefX, memref5); - - rc_destroy_parse_state(&parse); -} - -static void test_allocate_shared_indirect_address() { - rc_parse_state_t parse; - rc_memrefs_t memrefs; - rc_memref_t* parent_memref1, *parent_memref2; - rc_operand_t parent1, parent2, delta1, intermediate2; - rc_modified_memref_t* child1, *child2, *child3, *child4; - rc_operand_t offset0, offset4; - rc_operand_set_const(&offset0, 0); - rc_operand_set_const(&offset4, 4); - - rc_init_parse_state(&parse, NULL); - rc_init_parse_state_memrefs(&parse, &memrefs); - - parent1.value.memref = parent_memref1 = rc_alloc_memref(&parse, 88, RC_MEMSIZE_16_BITS); - parent1.type = RC_OPERAND_ADDRESS; - parent1.size = RC_MEMSIZE_16_BITS; - parent2.value.memref = parent_memref2 = rc_alloc_memref(&parse, 99, RC_MEMSIZE_16_BITS); - parent2.type = RC_OPERAND_ADDRESS; - parent2.size = RC_MEMSIZE_16_BITS; - delta1.value.memref = parent_memref1; - delta1.type = RC_OPERAND_DELTA; - ASSERT_NUM_EQUALS(get_memref_count(&parse), 2); - - child1 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset0); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 3); - - /* differing size will not match */ - child2 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_16_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset0); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 4); - - /* differing parent will not match */ - child3 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent2, RC_OPERATOR_INDIRECT_READ, &offset0); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 5); - - /* differing parent type will not match */ - child4 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &delta1, RC_OPERATOR_INDIRECT_READ, &offset0); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 6); - - /* differing offset will not match */ - child4 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset4); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); - - /* exact match to first */ - ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset0), child1); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); - - /* exact match to differing parent */ - ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent2, RC_OPERATOR_INDIRECT_READ, &offset0), child3); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); - - /* exact match to differing offset */ - ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &parent1, RC_OPERATOR_INDIRECT_READ, &offset4), child4); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 7); - - /* intermediate parent */ - intermediate2.value.memref = &child2->memref; - intermediate2.type = RC_OPERAND_ADDRESS; - intermediate2.size = RC_MEMSIZE_32_BITS; - intermediate2.memref_access_type = RC_OPERAND_ADDRESS; - child4 = rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &intermediate2, RC_OPERATOR_INDIRECT_READ, &offset0); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 8); - - ASSERT_PTR_EQUALS(rc_alloc_modified_memref(&parse, RC_MEMSIZE_8_BITS, &intermediate2, RC_OPERATOR_INDIRECT_READ, &offset0), child4); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 8); - - rc_destroy_parse_state(&parse); -} - -static void test_sizing_mode_grow_buffer() { - int i; - rc_parse_state_t parse; - rc_memrefs_t memrefs; - rc_init_parse_state(&parse, NULL); - rc_init_parse_state_memrefs(&parse, &memrefs); - - /* memrefs are allocated 16 at a time */ - for (i = 0; i < 100; i++) { - rc_alloc_memref(&parse, i, RC_MEMSIZE_8_BITS); - } - ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); - - /* 100 have been allocated, make sure we can still access items at various addresses without allocating more */ - rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); - - rc_alloc_memref(&parse, 25, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); - - rc_alloc_memref(&parse, 50, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); - - rc_alloc_memref(&parse, 75, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); - - rc_alloc_memref(&parse, 99, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(get_memref_count(&parse), 100); - - rc_destroy_parse_state(&parse); -} - -static void test_update_memref_values() { - rc_parse_state_t parse; - rc_memrefs_t memrefs; - rc_memref_t* memref1; - rc_memref_t* memref2; - - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - memory.ram = ram; - memory.size = sizeof(ram); - - rc_init_parse_state(&parse, NULL); - rc_init_parse_state_memrefs(&parse, &memrefs); - - memref1 = rc_alloc_memref(&parse, 1, RC_MEMSIZE_8_BITS); - memref2 = rc_alloc_memref(&parse, 2, RC_MEMSIZE_8_BITS); - - rc_update_memref_values(&memrefs, peek, &memory); - - ASSERT_NUM_EQUALS(memref1->value.value, 0x12); - ASSERT_NUM_EQUALS(memref1->value.changed, 1); - ASSERT_NUM_EQUALS(memref1->value.prior, 0); - ASSERT_NUM_EQUALS(memref2->value.value, 0x34); - ASSERT_NUM_EQUALS(memref2->value.changed, 1); - ASSERT_NUM_EQUALS(memref2->value.prior, 0); - - ram[1] = 3; - rc_update_memref_values(&memrefs, peek, &memory); - - ASSERT_NUM_EQUALS(memref1->value.value, 3); - ASSERT_NUM_EQUALS(memref1->value.changed, 1); - ASSERT_NUM_EQUALS(memref1->value.prior, 0x12); - ASSERT_NUM_EQUALS(memref2->value.value, 0x34); - ASSERT_NUM_EQUALS(memref2->value.changed, 0); - ASSERT_NUM_EQUALS(memref2->value.prior, 0); - - ram[1] = 5; - rc_update_memref_values(&memrefs, peek, &memory); - - ASSERT_NUM_EQUALS(memref1->value.value, 5); - ASSERT_NUM_EQUALS(memref1->value.changed, 1); - ASSERT_NUM_EQUALS(memref1->value.prior, 3); - ASSERT_NUM_EQUALS(memref2->value.value, 0x34); - ASSERT_NUM_EQUALS(memref2->value.changed, 0); - ASSERT_NUM_EQUALS(memref2->value.prior, 0); - - ram[2] = 7; - rc_update_memref_values(&memrefs, peek, &memory); - - ASSERT_NUM_EQUALS(memref1->value.value, 5); - ASSERT_NUM_EQUALS(memref1->value.changed, 0); - ASSERT_NUM_EQUALS(memref1->value.prior, 3); - ASSERT_NUM_EQUALS(memref2->value.value, 7); - ASSERT_NUM_EQUALS(memref2->value.changed, 1); - ASSERT_NUM_EQUALS(memref2->value.prior, 0x34); - - rc_destroy_parse_state(&parse); -} - -void test_memref(void) { - TEST_SUITE_BEGIN(); - - test_shared_masks(); - test_shared_sizes(); - test_transforms(); - - TEST(test_allocate_shared_address); - TEST(test_allocate_shared_address2); - TEST(test_allocate_shared_indirect_address); - - TEST(test_sizing_mode_grow_buffer); - TEST(test_update_memref_values); - - /* rc_parse_memref is thoroughly tested by rc_parse_operand tests */ - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_operand.c b/src/rcheevos/test/rcheevos/test_operand.c deleted file mode 100644 index fbf7c42cbc..0000000000 --- a/src/rcheevos/test/rcheevos/test_operand.c +++ /dev/null @@ -1,692 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -static void _assert_parse_operand(rc_operand_t* self, char* buffer, const char** memaddr) { - rc_parse_state_t parse; - rc_memrefs_t memrefs; - int ret; - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - ret = rc_parse_operand(self, memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_GREATER_EQUALS(ret, 0); - ASSERT_NUM_EQUALS(**memaddr, 0); -} -#define assert_parse_operand(operand, buffer, memaddr_out) ASSERT_HELPER(_assert_parse_operand(operand, buffer, memaddr_out), "assert_parse_operand") - -static void _assert_operand(rc_operand_t* self, uint8_t expected_type, uint8_t expected_size, uint32_t expected_address) { - ASSERT_NUM_EQUALS(expected_type, self->type); - switch (expected_type) { - case RC_OPERAND_ADDRESS: - case RC_OPERAND_DELTA: - case RC_OPERAND_PRIOR: - ASSERT_NUM_EQUALS(expected_size, self->size); - ASSERT_UNUM_EQUALS(expected_address, self->value.memref->address); - break; - - case RC_OPERAND_CONST: - ASSERT_UNUM_EQUALS(expected_address, self->value.num); - break; - } -} -#define assert_operand(operand, expected_type, expected_size, expected_address) ASSERT_HELPER(_assert_operand(operand, expected_type, expected_size, expected_address), "assert_operand") - -static void test_parse_operand(const char* memaddr, uint8_t expected_type, uint8_t expected_size, uint32_t expected_value) { - char buffer[256]; - rc_operand_t self; - assert_parse_operand(&self, buffer, &memaddr); - assert_operand(&self, expected_type, expected_size, expected_value); -} - -static void test_parse_operand_fp(const char* memaddr, uint8_t expected_type, double expected_value) { - char buffer[256]; - rc_operand_t self; - assert_parse_operand(&self, buffer, &memaddr); - - ASSERT_NUM_EQUALS(expected_type, self.type); - switch (expected_type) { - case RC_OPERAND_CONST: - ASSERT_DBL_EQUALS(expected_value, self.value.num); - break; - case RC_OPERAND_FP: - ASSERT_DBL_EQUALS(expected_value, self.value.dbl); - break; - } -} - -static void test_parse_error_operand(const char* memaddr, int valid_chars, int expected_error) { - rc_operand_t self; - rc_parse_state_t parse; - int ret; - const char* begin = memaddr; - rc_memrefs_t memrefs; - - rc_init_parse_state(&parse, 0); - rc_init_parse_state_memrefs(&parse, &memrefs); - ret = rc_parse_operand(&self, &memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_EQUALS(expected_error, ret); - ASSERT_NUM_EQUALS(memaddr - begin, valid_chars); -} - -static uint32_t evaluate_operand(rc_operand_t* op, memory_t* memory, rc_memrefs_t* memrefs) -{ - rc_eval_state_t eval_state; - rc_typed_value_t value; - - memset(&eval_state, 0, sizeof(eval_state)); - eval_state.peek = peek; - eval_state.peek_userdata = memory; - - rc_update_memref_values(memrefs, peek, memory); - rc_evaluate_operand(&value, op, &eval_state); - return value.value.u32; -} - -static void test_evaluate_operand(const char* memaddr, memory_t* memory, uint32_t expected_value) { - rc_operand_t self; - rc_parse_state_t parse; - rc_memrefs_t memrefs; - char buffer[512]; - uint32_t value; - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - rc_parse_operand(&self, &memaddr, &parse); - rc_destroy_parse_state(&parse); - - value = evaluate_operand(&self, memory, &memrefs); - ASSERT_NUM_EQUALS(value, expected_value); -} - -static float evaluate_operand_float(rc_operand_t* op, memory_t* memory, rc_memrefs_t* memrefs) { - rc_eval_state_t eval_state; - rc_typed_value_t value; - - memset(&eval_state, 0, sizeof(eval_state)); - eval_state.peek = peek; - eval_state.peek_userdata = memory; - - rc_update_memref_values(memrefs, peek, memory); - rc_evaluate_operand(&value, op, &eval_state); - return value.value.f32; -} - -static void test_evaluate_operand_float(const char* memaddr, memory_t* memory, double expected_value) { - rc_operand_t self; - rc_parse_state_t parse; - rc_memrefs_t memrefs; - char buffer[512]; - float value; - - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - rc_parse_operand(&self, &memaddr, &parse); - rc_destroy_parse_state(&parse); - - value = evaluate_operand_float(&self, memory, &memrefs); - ASSERT_FLOAT_EQUALS(value, expected_value); -} -static void test_parse_memory_references() { - /* sizes */ - TEST_PARAMS4(test_parse_operand, "0xH1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xH1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0x 1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0x1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xW1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xX1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xL1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xU1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xM1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_0, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xN1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_1, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xO1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_2, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xP1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_3, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xQ1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_4, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xR1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_5, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xS1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_6, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xT1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_7, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xK1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BITCOUNT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xI1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xJ1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xG1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fF1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fB1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fM1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fL1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32_LE, 0x1234U); - - /* sizes (ignore case) */ - TEST_PARAMS4(test_parse_operand, "0Xh1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xx1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xl1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_LOW, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xu1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_HIGH, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xm1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_0, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xn1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_1, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xo1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_2, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xp1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_3, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xq1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_4, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xr1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_5, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xs1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_6, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xt1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BIT_7, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xk1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_BITCOUNT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xi1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xj1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_24_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "0xg1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_32_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "ff1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fb1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fm1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32, 0x1234U); - TEST_PARAMS4(test_parse_operand, "fl1234", RC_OPERAND_ADDRESS, RC_MEMSIZE_MBF32_LE, 0x1234U); - - /* addresses */ - TEST_PARAMS4(test_parse_operand, "0xH0000", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x0000U); - TEST_PARAMS4(test_parse_operand, "0xH12345678", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0x12345678U); - TEST_PARAMS4(test_parse_operand, "0xHABCD", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0xABCDU); - TEST_PARAMS4(test_parse_operand, "0xhabcd", RC_OPERAND_ADDRESS, RC_MEMSIZE_8_BITS, 0xABCDU); - TEST_PARAMS4(test_parse_operand, "fFABCD", RC_OPERAND_ADDRESS, RC_MEMSIZE_FLOAT, 0xABCDU); - - /* doubled up prefix */ - TEST_PARAMS3(test_parse_error_operand, "0x0xH1234", 0, RC_INVALID_MEMORY_OPERAND); -} - -static void test_parse_delta_memory_references() { - /* sizes */ - TEST_PARAMS4(test_parse_operand, "d0xH1234", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0x 1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0x1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xW1234", RC_OPERAND_DELTA, RC_MEMSIZE_24_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xX1234", RC_OPERAND_DELTA, RC_MEMSIZE_32_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xL1234", RC_OPERAND_DELTA, RC_MEMSIZE_LOW, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xU1234", RC_OPERAND_DELTA, RC_MEMSIZE_HIGH, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xM1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_0, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xN1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_1, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xO1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_2, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xP1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_3, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xQ1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_4, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xR1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_5, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xS1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_6, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xT1234", RC_OPERAND_DELTA, RC_MEMSIZE_BIT_7, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xK1234", RC_OPERAND_DELTA, RC_MEMSIZE_BITCOUNT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xI1234", RC_OPERAND_DELTA, RC_MEMSIZE_16_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xJ1234", RC_OPERAND_DELTA, RC_MEMSIZE_24_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "d0xG1234", RC_OPERAND_DELTA, RC_MEMSIZE_32_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "dfF1234", RC_OPERAND_DELTA, RC_MEMSIZE_FLOAT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "dfB1234", RC_OPERAND_DELTA, RC_MEMSIZE_FLOAT_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "dfM1234", RC_OPERAND_DELTA, RC_MEMSIZE_MBF32, 0x1234U); - TEST_PARAMS4(test_parse_operand, "dfL1234", RC_OPERAND_DELTA, RC_MEMSIZE_MBF32_LE, 0x1234U); - - /* ignores case */ - TEST_PARAMS4(test_parse_operand, "D0Xh1234", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x1234U); - - /* addresses */ - TEST_PARAMS4(test_parse_operand, "d0xH0000", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x0000U); - TEST_PARAMS4(test_parse_operand, "d0xH12345678", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0x12345678U); - TEST_PARAMS4(test_parse_operand, "d0xHABCD", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0xABCDU); - TEST_PARAMS4(test_parse_operand, "d0xhabcd", RC_OPERAND_DELTA, RC_MEMSIZE_8_BITS, 0xABCDU); -} - -static void test_parse_prior_memory_references() { - /* sizes */ - TEST_PARAMS4(test_parse_operand, "p0xH1234", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0x 1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0x1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xW1234", RC_OPERAND_PRIOR, RC_MEMSIZE_24_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xX1234", RC_OPERAND_PRIOR, RC_MEMSIZE_32_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xL1234", RC_OPERAND_PRIOR, RC_MEMSIZE_LOW, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xU1234", RC_OPERAND_PRIOR, RC_MEMSIZE_HIGH, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xM1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_0, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xN1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_1, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xO1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_2, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xP1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_3, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xQ1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_4, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xR1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_5, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xS1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_6, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xT1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BIT_7, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xK1234", RC_OPERAND_PRIOR, RC_MEMSIZE_BITCOUNT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xI1234", RC_OPERAND_PRIOR, RC_MEMSIZE_16_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xJ1234", RC_OPERAND_PRIOR, RC_MEMSIZE_24_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "p0xG1234", RC_OPERAND_PRIOR, RC_MEMSIZE_32_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "pfF1234", RC_OPERAND_PRIOR, RC_MEMSIZE_FLOAT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "pfB1234", RC_OPERAND_PRIOR, RC_MEMSIZE_FLOAT_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "pfM1234", RC_OPERAND_PRIOR, RC_MEMSIZE_MBF32, 0x1234U); - TEST_PARAMS4(test_parse_operand, "pfL1234", RC_OPERAND_PRIOR, RC_MEMSIZE_MBF32_LE, 0x1234U); - - /* ignores case */ - TEST_PARAMS4(test_parse_operand, "P0Xh1234", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x1234U); - - /* addresses */ - TEST_PARAMS4(test_parse_operand, "p0xH0000", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x0000U); - TEST_PARAMS4(test_parse_operand, "p0xH12345678", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0x12345678U); - TEST_PARAMS4(test_parse_operand, "p0xHABCD", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0xABCDU); - TEST_PARAMS4(test_parse_operand, "p0xhabcd", RC_OPERAND_PRIOR, RC_MEMSIZE_8_BITS, 0xABCDU); -} - -static void test_parse_bcd_memory_references() { - /* sizes */ - TEST_PARAMS4(test_parse_operand, "b0xH1234", RC_OPERAND_BCD, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0x 1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0x1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xW1234", RC_OPERAND_BCD, RC_MEMSIZE_24_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xX1234", RC_OPERAND_BCD, RC_MEMSIZE_32_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xI1234", RC_OPERAND_BCD, RC_MEMSIZE_16_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xJ1234", RC_OPERAND_BCD, RC_MEMSIZE_24_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xG1234", RC_OPERAND_BCD, RC_MEMSIZE_32_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "bfF1234", RC_OPERAND_BCD, RC_MEMSIZE_FLOAT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "bfB1234", RC_OPERAND_BCD, RC_MEMSIZE_FLOAT_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "bfM1234", RC_OPERAND_BCD, RC_MEMSIZE_MBF32, 0x1234U); - TEST_PARAMS4(test_parse_operand, "bfL1234", RC_OPERAND_BCD, RC_MEMSIZE_MBF32_LE, 0x1234U); - - /* sizes less than 8-bit technically don't need a BCD conversion */ - TEST_PARAMS4(test_parse_operand, "b0xL1234", RC_OPERAND_BCD, RC_MEMSIZE_LOW, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xU1234", RC_OPERAND_BCD, RC_MEMSIZE_HIGH, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xM1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_0, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xN1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_1, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xO1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_2, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xP1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_3, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xQ1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_4, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xR1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_5, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xS1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_6, 0x1234U); - TEST_PARAMS4(test_parse_operand, "b0xT1234", RC_OPERAND_BCD, RC_MEMSIZE_BIT_7, 0x1234U); -} - -static void test_parse_inverted_memory_references() { - TEST_PARAMS4(test_parse_operand, "~0xH1234", RC_OPERAND_INVERTED, RC_MEMSIZE_8_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0x 1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0x1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xW1234", RC_OPERAND_INVERTED, RC_MEMSIZE_24_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xX1234", RC_OPERAND_INVERTED, RC_MEMSIZE_32_BITS, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xI1234", RC_OPERAND_INVERTED, RC_MEMSIZE_16_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xJ1234", RC_OPERAND_INVERTED, RC_MEMSIZE_24_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xG1234", RC_OPERAND_INVERTED, RC_MEMSIZE_32_BITS_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~fF1234", RC_OPERAND_INVERTED, RC_MEMSIZE_FLOAT, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~fB1234", RC_OPERAND_INVERTED, RC_MEMSIZE_FLOAT_BE, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~fM1234", RC_OPERAND_INVERTED, RC_MEMSIZE_MBF32, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~fL1234", RC_OPERAND_INVERTED, RC_MEMSIZE_MBF32_LE, 0x1234U); - - TEST_PARAMS4(test_parse_operand, "~0xL1234", RC_OPERAND_INVERTED, RC_MEMSIZE_LOW, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xU1234", RC_OPERAND_INVERTED, RC_MEMSIZE_HIGH, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xM1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_0, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xN1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_1, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xO1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_2, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xP1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_3, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xQ1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_4, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xR1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_5, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xS1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_6, 0x1234U); - TEST_PARAMS4(test_parse_operand, "~0xT1234", RC_OPERAND_INVERTED, RC_MEMSIZE_BIT_7, 0x1234U); -} - -static void test_parse_unsigned_values() { - /* unsigned integers - no prefix */ - /* values don't actually have size, default is RC_MEMSIZE_8_BITS */ - TEST_PARAMS4(test_parse_operand, "123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123U); - TEST_PARAMS4(test_parse_operand, "123456", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123456U); - TEST_PARAMS4(test_parse_operand, "0", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U); - TEST_PARAMS4(test_parse_operand, "0000000000", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0U); - TEST_PARAMS4(test_parse_operand, "0123456", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 123456U); - TEST_PARAMS4(test_parse_operand, "4294967295", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); - - /* more than 32-bits (error), will be constrained to 32-bits */ - TEST_PARAMS4(test_parse_operand, "4294967296", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); -} - -static void test_parse_signed_values() { - /* signed integers - 'V' prefix */ - TEST_PARAMS4(test_parse_operand, "v100", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 100); - TEST_PARAMS4(test_parse_operand, "V100", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 100); - TEST_PARAMS4(test_parse_operand, "V+1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1); - TEST_PARAMS4(test_parse_operand, "V-1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xFFFFFFFFU); - TEST_PARAMS4(test_parse_operand, "V-2", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xFFFFFFFEU); - TEST_PARAMS4(test_parse_operand, "V9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x7FFFFFFFU); - TEST_PARAMS4(test_parse_operand, "V-9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x80000001U); - - /* no prefix, sign */ - TEST_PARAMS4(test_parse_operand, "-1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); - TEST_PARAMS4(test_parse_operand, "+1", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 1); - TEST_PARAMS4(test_parse_operand, "+9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x7FFFFFFFU); - TEST_PARAMS4(test_parse_operand, "-9876543210", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x80000001U); - - /* prefix, no value */ - TEST_PARAMS3(test_parse_error_operand, "v", 0, RC_INVALID_CONST_OPERAND); - - /* signed integer prefix, hex value */ - TEST_PARAMS3(test_parse_error_operand, "vabcd", 0, RC_INVALID_CONST_OPERAND); -} - -static void test_parse_hex_values() { - /* hex - 'H' prefix, not '0x'! */ - TEST_PARAMS4(test_parse_operand, "H123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x123U); - TEST_PARAMS4(test_parse_operand, "HABCD", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xABCDU); - TEST_PARAMS4(test_parse_operand, "h123", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0x123U); - TEST_PARAMS4(test_parse_operand, "habcd", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 0xABCDU); - TEST_PARAMS4(test_parse_operand, "HFFFFFFFF", RC_OPERAND_CONST, RC_MEMSIZE_8_BITS, 4294967295U); - - /* hex without prefix */ - TEST_PARAMS3(test_parse_error_operand, "ABCD", 0, RC_INVALID_MEMORY_OPERAND); - - /* '0x' is an address */ - TEST_PARAMS4(test_parse_operand, "0x123", RC_OPERAND_ADDRESS, RC_MEMSIZE_16_BITS, 0x123U); -} - -static void test_parse_float_values() { - /* floating point - 'F' prefix */ - TEST_PARAMS3(test_parse_operand_fp, "f0.5", RC_OPERAND_FP, 0.5); - TEST_PARAMS3(test_parse_operand_fp, "F0.5", RC_OPERAND_FP, 0.5); - TEST_PARAMS3(test_parse_operand_fp, "f+0.5", RC_OPERAND_FP, 0.5); - TEST_PARAMS3(test_parse_operand_fp, "f-0.5", RC_OPERAND_FP, -0.5); - TEST_PARAMS3(test_parse_operand_fp, "f1.0", RC_OPERAND_FP, 1.0); - TEST_PARAMS3(test_parse_operand_fp, "f1.000000", RC_OPERAND_FP, 1.0); - TEST_PARAMS3(test_parse_operand_fp, "f1.000001", RC_OPERAND_FP, 1.000001); - TEST_PARAMS3(test_parse_operand_fp, "f1", RC_OPERAND_CONST, 1.0); - TEST_PARAMS3(test_parse_operand_fp, "f0.666666", RC_OPERAND_FP, 0.666666); - TEST_PARAMS3(test_parse_operand_fp, "f0.001", RC_OPERAND_FP, 0.001); - TEST_PARAMS3(test_parse_operand_fp, "f0.100", RC_OPERAND_FP, 0.1); - TEST_PARAMS3(test_parse_operand_fp, "f.12345", RC_OPERAND_FP, 0.12345); - - /* prefix, no value */ - TEST_PARAMS3(test_parse_error_operand, "f", 0, RC_INVALID_FP_OPERAND); - - /* float prefix, hex value */ - TEST_PARAMS3(test_parse_error_operand, "fabcd", 0, RC_INVALID_FP_OPERAND); - - /* float prefix, hex value, no period, parser will stop after valid numbers */ - TEST_PARAMS3(test_parse_error_operand, "f1d", 2, RC_OK); - - /* non-numeric decimal part */ - TEST_PARAMS3(test_parse_error_operand, "f1.d", 0, RC_INVALID_FP_OPERAND); - TEST_PARAMS3(test_parse_error_operand, "f1..0", 0, RC_INVALID_FP_OPERAND); - - /* non-C locale - parser will stop at comma */ - TEST_PARAMS3(test_parse_error_operand, "f1,23", 2, RC_OK); - TEST_PARAMS3(test_parse_error_operand, "f-1,23", 3, RC_OK); - - /* no prefix - parser will stop at period */ - TEST_PARAMS3(test_parse_error_operand, "0.5", 1, RC_OK); - TEST_PARAMS3(test_parse_error_operand, "-0.5", 2, RC_OK); -} - -static void test_evaluate_memory_references() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - memory.ram = ram; - memory.size = sizeof(ram); - - /* value */ - TEST_PARAMS3(test_evaluate_operand, "0", &memory, 0x00U); - - /* eight-bit */ - TEST_PARAMS3(test_evaluate_operand, "0xh0", &memory, 0x00U); - TEST_PARAMS3(test_evaluate_operand, "0xh1", &memory, 0x12U); - TEST_PARAMS3(test_evaluate_operand, "0xh4", &memory, 0x56U); - TEST_PARAMS3(test_evaluate_operand, "0xh5", &memory, 0x00U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xh4", &memory, 0xA9U); - - /* sixteen-bit */ - TEST_PARAMS3(test_evaluate_operand, "0x 0", &memory, 0x1200U); - TEST_PARAMS3(test_evaluate_operand, "0x 3", &memory, 0x56ABU); - TEST_PARAMS3(test_evaluate_operand, "0x 4", &memory, 0x0056U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0x 3", &memory, 0xA954U); - - /* twenty-four-bit */ - TEST_PARAMS3(test_evaluate_operand, "0xw0", &memory, 0x341200U); - TEST_PARAMS3(test_evaluate_operand, "0xw2", &memory, 0x56AB34U); - TEST_PARAMS3(test_evaluate_operand, "0xw3", &memory, 0x0056ABU); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xw2", &memory, 0xA954CBU); - - /* thirty-two-bit */ - TEST_PARAMS3(test_evaluate_operand, "0xx0", &memory, 0xAB341200U); - TEST_PARAMS3(test_evaluate_operand, "0xx1", &memory, 0x56AB3412U); - TEST_PARAMS3(test_evaluate_operand, "0xx3", &memory, 0x000056ABU); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xx1", &memory, 0xA954CBEDU); - TEST_PARAMS3(test_evaluate_operand, "~0xx3", &memory, 0xFFFFA954U); /* out of range */ - - /* sixteen-bit big endian*/ - TEST_PARAMS3(test_evaluate_operand, "0xi0", &memory, 0x0012U); - TEST_PARAMS3(test_evaluate_operand, "0xi3", &memory, 0xAB56U); - TEST_PARAMS3(test_evaluate_operand, "0xi4", &memory, 0x5600U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xi3", &memory, 0x54A9U); - - /* twenty-four-bit big endian */ - TEST_PARAMS3(test_evaluate_operand, "0xj0", &memory, 0x001234U); - TEST_PARAMS3(test_evaluate_operand, "0xj1", &memory, 0x1234ABU); - TEST_PARAMS3(test_evaluate_operand, "0xj3", &memory, 0xAB5600U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xj1", &memory, 0xEDCB54U); - TEST_PARAMS3(test_evaluate_operand, "~0xj3", &memory, 0x54A9FFU); /* out of range */ - - /* thirty-two-bit big endian */ - TEST_PARAMS3(test_evaluate_operand, "0xg0", &memory, 0x001234ABU); - TEST_PARAMS3(test_evaluate_operand, "0xg1", &memory, 0x1234AB56U); - TEST_PARAMS3(test_evaluate_operand, "0xg3", &memory, 0xAB560000U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xg1", &memory, 0xEDCB54A9U); - TEST_PARAMS3(test_evaluate_operand, "~0xg3", &memory, 0x54A9FFFFU); /* out of range */ - - /* nibbles */ - TEST_PARAMS3(test_evaluate_operand, "0xu0", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "0xu1", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "0xu4", &memory, 0x5U); - TEST_PARAMS3(test_evaluate_operand, "0xu5", &memory, 0x0U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xu4", &memory, 0xAU); - - TEST_PARAMS3(test_evaluate_operand, "0xl0", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "0xl1", &memory, 0x2U); - TEST_PARAMS3(test_evaluate_operand, "0xl4", &memory, 0x6U); - TEST_PARAMS3(test_evaluate_operand, "0xl5", &memory, 0x0U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xl4", &memory, 0x9U); - - /* bits */ - TEST_PARAMS3(test_evaluate_operand, "0xm0", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "0xm3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "0xn3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "0xo3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "0xp3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "0xq3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "0xr3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "0xs3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "0xt3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "0xm5", &memory, 0x0U); /* out of range */ - TEST_PARAMS3(test_evaluate_operand, "~0xm0", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "~0xm3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "~0xn3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "~0xo3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "~0xp3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "~0xq3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "~0xr3", &memory, 0x0U); - TEST_PARAMS3(test_evaluate_operand, "~0xs3", &memory, 0x1U); - TEST_PARAMS3(test_evaluate_operand, "~0xt3", &memory, 0x0U); - - /* bit count */ - TEST_PARAMS3(test_evaluate_operand, "0xk00", &memory, 0U); /* 0 bits in 0x00 */ - TEST_PARAMS3(test_evaluate_operand, "0xk01", &memory, 2U); /* 2 bits in 0x12 */ - TEST_PARAMS3(test_evaluate_operand, "0xk02", &memory, 3U); /* 3 bits in 0x34 */ - TEST_PARAMS3(test_evaluate_operand, "0xk03", &memory, 5U); /* 5 bits in 0xAB */ - TEST_PARAMS3(test_evaluate_operand, "0xk04", &memory, 4U); /* 4 bits in 0x56 */ - - /* BCD */ - TEST_PARAMS3(test_evaluate_operand, "b0xh3", &memory, 111U); /* 0xAB not technically valid in BCD */ - - ram[3] = 0x56; /* 0xAB not valid in BCD */ - ram[4] = 0x78; - TEST_PARAMS3(test_evaluate_operand, "b0xh0", &memory, 00U); - TEST_PARAMS3(test_evaluate_operand, "b0xh1", &memory, 12U); - TEST_PARAMS3(test_evaluate_operand, "b0x 1", &memory, 3412U); - TEST_PARAMS3(test_evaluate_operand, "b0xw1", &memory, 563412U); - TEST_PARAMS3(test_evaluate_operand, "b0xx1", &memory, 78563412U); - TEST_PARAMS3(test_evaluate_operand, "b0xi1", &memory, 1234U); - TEST_PARAMS3(test_evaluate_operand, "b0xj1", &memory, 123456U); - TEST_PARAMS3(test_evaluate_operand, "b0xg1", &memory, 12345678U); -} - -static void test_evaluate_delta_memory_reference() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_operand_t op; - const char* memaddr; - rc_parse_state_t parse; - char buffer[256]; - rc_memrefs_t memrefs; - - memory.ram = ram; - memory.size = sizeof(ram); - - memaddr = "d0xh1"; - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - rc_parse_operand(&op, &memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x00); /* first call gets uninitialized value */ - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12); /* second gets current value */ - - /* RC_OPERAND_DELTA is always one frame behind */ - ram[1] = 0x13; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); - - ram[1] = 0x14; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x13U); - - ram[1] = 0x15; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x14U); - - ram[1] = 0x16; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); - - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x16U); - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x16U); -} - -void test_evaluate_prior_memory_reference() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_operand_t op; - const char* memaddr; - rc_parse_state_t parse; - char buffer[256]; - rc_memrefs_t memrefs; - - memory.ram = ram; - memory.size = sizeof(ram); - - memaddr = "p0xh1"; - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - rc_parse_operand(&op, &memaddr, &parse); - rc_destroy_parse_state(&parse); - - /* RC_OPERAND_PRIOR only updates when the memory value changes */ - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x00); /* first call gets uninitialized value */ - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x00); /* value only changes when memory changes */ - - ram[1] = 0x13; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x12U); - - ram[1] = 0x14; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x13U); - - ram[1] = 0x15; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x14U); - - ram[1] = 0x16; - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); - ASSERT_UNUM_EQUALS(evaluate_operand(&op, &memory, &memrefs), 0x15U); -} - -static void test_evaluate_memory_references_float() { - uint8_t ram[] = {0x00, 0x00, 0x80, 0x3F, 0x81, 0x00, 0x00, 0x00, 0x00, 0x81}; - memory_t memory; - memory.ram = ram; - memory.size = sizeof(ram); - - TEST_PARAMS3(test_evaluate_operand_float, "fF0", &memory, 1.0); /* IEE754 float */ - TEST_PARAMS3(test_evaluate_operand_float, "fM4", &memory, 1.0); /* MBF32 float */ - TEST_PARAMS3(test_evaluate_operand_float, "fL6", &memory, 1.0); /* MBF32_LE float */ - - /* BCD and inversion are not supported for floats - behaves as if the prefix wasn't present */ - TEST_PARAMS3(test_evaluate_operand_float, "bfF0", &memory, 1.0); /* IEE754 float */ - TEST_PARAMS3(test_evaluate_operand_float, "bfM4", &memory, 1.0); /* MBF32 float */ - TEST_PARAMS3(test_evaluate_operand_float, "bfL6", &memory, 1.0); /* MBF32_LE float */ - TEST_PARAMS3(test_evaluate_operand_float, "~fF0", &memory, 1.0); /* IEE754 float */ - TEST_PARAMS3(test_evaluate_operand_float, "~fM4", &memory, 1.0); /* MBF32 float */ - TEST_PARAMS3(test_evaluate_operand_float, "~fL6", &memory, 1.0); /* MBF32_LE float */ - - ram[2] = 0x00; ram[3] = 0x40; /* set IEE754 float to 2.0 */ - ram[4] = 0x83; ram[5] = 0x40; /* set MBF32 float to 6.0 */ - ram[9] = 0x83; ram[8] = 0x00; /* set MBF32_LE float to 4.0 */ - - TEST_PARAMS3(test_evaluate_operand_float, "fF0", &memory, 2.0); /* IEE754 float */ - TEST_PARAMS3(test_evaluate_operand_float, "fM4", &memory, 6.0); /* MBF32 float */ - TEST_PARAMS3(test_evaluate_operand_float, "fL6", &memory, 4.0); /* MBF32_LE float */ - TEST_PARAMS3(test_evaluate_operand_float, "bfF0", &memory, 2.0); /* IEE754 float */ - TEST_PARAMS3(test_evaluate_operand_float, "bfM4", &memory, 6.0); /* MBF32 float */ - TEST_PARAMS3(test_evaluate_operand_float, "bfL6", &memory, 4.0); /* MBF32_LE float */ - TEST_PARAMS3(test_evaluate_operand_float, "~fF0", &memory, 2.0); /* IEE754 float */ - TEST_PARAMS3(test_evaluate_operand_float, "~fM4", &memory, 6.0); /* MBF32 float */ - TEST_PARAMS3(test_evaluate_operand_float, "~fL6", &memory, 4.0); /* MBF32_LE float */ -} - -static void test_evaluate_delta_memory_reference_float() { - uint8_t ram[] = {0x00, 0x00, 0x80, 0x3F}; - memory_t memory; - rc_operand_t op; - const char* memaddr; - rc_parse_state_t parse; - char buffer[256]; - rc_memrefs_t memrefs; - - memory.ram = ram; - memory.size = sizeof(ram); - - memaddr = "dff0"; - rc_init_parse_state(&parse, buffer); - rc_init_parse_state_memrefs(&parse, &memrefs); - rc_parse_operand(&op, &memaddr, &parse); - rc_destroy_parse_state(&parse); - - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 0.0); /* first call gets uninitialized value */ - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 1.0); /* second gets current value */ - - /* RC_OPERAND_DELTA is always one frame behind */ - ram[3] = 0x40; - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 1.0); - - ram[3] = 0x41; - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 4.0); - - ram[3] = 0x42; - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 16.0); - - ram[3] = 0x43; - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 64.0); - - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 256.0); - ASSERT_NUM_EQUALS(evaluate_operand_float(&op, &memory, &memrefs), 256.0); -} - -void test_operand(void) { - TEST_SUITE_BEGIN(); - - test_parse_memory_references(); - test_parse_delta_memory_references(); - test_parse_prior_memory_references(); - test_parse_bcd_memory_references(); - test_parse_inverted_memory_references(); - - test_parse_unsigned_values(); - test_parse_signed_values(); - test_parse_hex_values(); - test_parse_float_values(); - - test_evaluate_memory_references(); - TEST(test_evaluate_delta_memory_reference); - TEST(test_evaluate_prior_memory_reference); - - test_evaluate_memory_references_float(); - TEST(test_evaluate_delta_memory_reference_float); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_rc_validate.c b/src/rcheevos/test/rcheevos/test_rc_validate.c deleted file mode 100644 index 53101749a7..0000000000 --- a/src/rcheevos/test/rcheevos/test_rc_validate.c +++ /dev/null @@ -1,500 +0,0 @@ -#include "rc_validate.h" - -#include "rc_consoles.h" - -#include "../rc_compat.h" -#include "../test_framework.h" - -#include -#include - -int validate_trigger(const char* trigger, char result[], const size_t result_size, uint32_t max_address) { - char* buffer; - rc_trigger_t* compiled; - int success = 0; - - int ret = rc_trigger_size(trigger); - if (ret < 0) { - snprintf(result, result_size, "%s", rc_error_str(ret)); - return 0; - } - - buffer = (char*)malloc(ret + 4); - if (!buffer) { - snprintf(result, result_size, "malloc failed"); - return 0; - } - memset(buffer + ret, 0xCD, 4); - compiled = rc_parse_trigger(buffer, trigger, NULL, 0); - if (compiled == NULL) { - snprintf(result, result_size, "parse failed"); - } - else if (*(uint32_t*)&buffer[ret] != 0xCDCDCDCD) { - snprintf(result, result_size, "write past end of buffer"); - } - else if (rc_validate_trigger(compiled, result, result_size, max_address)) { - success = 1; - } - - free(buffer); - return success; -} - -static void test_validate_trigger_max_address(const char* trigger, const char* expected_error, uint32_t max_address) { - char buffer[512]; - int valid = validate_trigger(trigger, buffer, sizeof(buffer), max_address); - - if (*expected_error) { - ASSERT_STR_EQUALS(buffer, expected_error); - ASSERT_NUM_EQUALS(valid, 0); - } - else { - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_NUM_EQUALS(valid, 1); - } -} - -static void test_validate_trigger(const char* trigger, const char* expected_error) { - test_validate_trigger_max_address(trigger, expected_error, 0xFFFFFFFF); -} - -static void test_validate_trigger_64k(const char* trigger, const char* expected_error) { - test_validate_trigger_max_address(trigger, expected_error, 0xFFFF); -} - -static void test_validate_trigger_128k(const char* trigger, const char* expected_error) { - test_validate_trigger_max_address(trigger, expected_error, 0x1FFFF); -} - -int validate_trigger_for_console(const char* trigger, char result[], const size_t result_size, uint32_t console_id) { - char* buffer; - rc_trigger_t* compiled; - int success = 0; - - int ret = rc_trigger_size(trigger); - if (ret < 0) { - snprintf(result, result_size, "%s", rc_error_str(ret)); - return 0; - } - - buffer = (char*)malloc(ret + 4); - if (!buffer) { - snprintf(result, result_size, "malloc failed"); - return 0; - } - memset(buffer + ret, 0xCD, 4); - compiled = rc_parse_trigger(buffer, trigger, NULL, 0); - if (compiled == NULL) { - snprintf(result, result_size, "parse failed"); - } - else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { - snprintf(result, result_size, "write past end of buffer"); - } - else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { - success = 1; - } - - free(buffer); - return success; -} - -static void test_validate_trigger_console(const char* trigger, const char* expected_error, uint32_t console_id) { - char buffer[512]; - int valid = validate_trigger_for_console(trigger, buffer, sizeof(buffer), console_id); - - if (*expected_error) { - ASSERT_STR_EQUALS(buffer, expected_error); - ASSERT_NUM_EQUALS(valid, 0); - } - else { - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_NUM_EQUALS(valid, 1); - } -} - -static void test_combining_conditions_at_end_of_definition() { - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_A:0xH2345=2", "Condition 2: AddSource condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_B:0xH2345=2", "Condition 2: SubSource condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_C:0xH2345=2", "Condition 2: AddHits condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_D:0xH2345=2", "Condition 2: SubHits condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_N:0xH2345=2", "Condition 2: AndNext condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_O:0xH2345=2", "Condition 2: OrNext condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_Z:0xH2345=2", "Condition 2: ResetNextIf condition type expects another condition to follow"); - - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_A:0xH2345=2S0x3456=1", "Core Condition 2: AddSource condition type expects another condition to follow"); - TEST_PARAMS2(test_validate_trigger, "0x3456=1S0xH1234=1_A:0xH2345=2", "Alt1 Condition 2: AddSource condition type expects another condition to follow"); - - /* combining conditions not at end of definition */ - TEST_PARAMS2(test_validate_trigger, "A:0xH1234=1_0xH2345=2", ""); - TEST_PARAMS2(test_validate_trigger, "B:0xH1234=1_0xH2345=2", ""); - TEST_PARAMS2(test_validate_trigger, "N:0xH1234=1_0xH2345=2", ""); - TEST_PARAMS2(test_validate_trigger, "O:0xH1234=1_0xH2345=2", ""); - TEST_PARAMS2(test_validate_trigger, "Z:0xH1234=1_0xH2345=2", ""); -} - -static void test_addhits_chain_without_target() { - TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_0xH2345=2", "Condition 2: Final condition in AddHits chain must have a hit target"); - TEST_PARAMS2(test_validate_trigger, "D:0xH1234=1_0xH2345=2", "Condition 2: Final condition in AddHits chain must have a hit target"); - TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_0xH2345=2.1.", ""); - TEST_PARAMS2(test_validate_trigger, "D:0xH1234=1_0xH2345=2.1.", ""); - - /* ResetIf at the end of a hit chain does not require a hit target. - * It's meant to reset things if some subset of conditions have been true. */ - TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_C:0xH2345=2_R:0=1.1.", ""); - TEST_PARAMS2(test_validate_trigger, "C:0xH1234=1_C:0xH2345=2_R:0=1", ""); -} - -static void test_range_comparisons() { - TEST_PARAMS2(test_validate_trigger, "0xH1234>1", ""); - - TEST_PARAMS2(test_validate_trigger, "0xH1234=255", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234!=255", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234>255", "Condition 1: Comparison is never true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234>=255", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234<255", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234<=255", "Condition 1: Comparison is always true (max 255)"); - - /* while a BCD value shouldn't exceed 99, it can reach 165: 0xFF => 15*10+15 */ - TEST_PARAMS2(test_validate_trigger, "b0xH1234<165", ""); - TEST_PARAMS2(test_validate_trigger, "b0xH1234<=165", "Condition 1: Comparison is always true (max 165)"); - - TEST_PARAMS2(test_validate_trigger, "R:0xH1234<255_0xH0000=1.1.", ""); - TEST_PARAMS2(test_validate_trigger, "R:0xH1234<=255_0xH0000=1.1.", "Condition 1: Comparison is always true (max 255)"); - - TEST_PARAMS2(test_validate_trigger, "0xH1234=256", "Condition 1: Comparison is never true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234!=256", "Condition 1: Comparison is always true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234>256", "Condition 1: Comparison is never true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234>=256", "Condition 1: Comparison is never true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234<256", "Condition 1: Comparison is always true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234<=256", "Condition 1: Comparison is always true (max 255)"); - - TEST_PARAMS2(test_validate_trigger, "0x 1234>=65535", ""); - TEST_PARAMS2(test_validate_trigger, "0x 1234>=65536", "Condition 1: Comparison is never true (max 65535)"); - - TEST_PARAMS2(test_validate_trigger, "b0x 1234>=16665", ""); - TEST_PARAMS2(test_validate_trigger, "b0x 1234>=16666", "Condition 1: Comparison is never true (max 16665)"); - - TEST_PARAMS2(test_validate_trigger, "0xW1234>=16777215", ""); - TEST_PARAMS2(test_validate_trigger, "0xW1234>=16777216", "Condition 1: Comparison is never true (max 16777215)"); - - TEST_PARAMS2(test_validate_trigger, "b0xW1234>=1666665", ""); - TEST_PARAMS2(test_validate_trigger, "b0xW1234>=1666666", "Condition 1: Comparison is never true (max 1666665)"); - - TEST_PARAMS2(test_validate_trigger, "0xX1234>=4294967295", ""); - TEST_PARAMS2(test_validate_trigger, "0xX1234>4294967295", "Condition 1: Comparison is never true (max 4294967295)"); - - TEST_PARAMS2(test_validate_trigger, "b0xX1234>=166666665", ""); - TEST_PARAMS2(test_validate_trigger, "b0xX1234>=166666666", "Condition 1: Comparison is never true (max 166666665)"); - - TEST_PARAMS2(test_validate_trigger, "0xT1234>=1", ""); - TEST_PARAMS2(test_validate_trigger, "0xT1234>1", "Condition 1: Comparison is never true (max 1)"); - - /* max for AddSource is the sum of all parts (255+255=510) */ - TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0<255", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0<=255", "Condition 2: Comparison is always true (max 255)"); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235<510", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235<=510", "Condition 2: Comparison is always true (max 510)"); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234*10_0xH1235>=2805", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234*10_0xH1235>2805", "Condition 2: Comparison is never true (max 2805)"); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234/10_0xH1235>=280", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234/10_0xH1235>280", "Condition 2: Comparison is never true (max 280)"); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234&10_0xH1235>=265", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234&10_0xH1235>265", "Condition 2: Comparison is never true (max 265)"); - TEST_PARAMS2(test_validate_trigger, "A:b0xH1234*100_b0xH1235>=16665", ""); - TEST_PARAMS2(test_validate_trigger, "A:b0xH1234*100_b0xH1235>16665", "Condition 2: Comparison is never true (max 16665)"); - TEST_PARAMS2(test_validate_trigger, "A:0xX1234_0x 1245=123456", ""); /* sum of 32-bit and 16-bit is still 32-bit */ - - /* max for SubSource is always 0xFFFFFFFF */ - TEST_PARAMS2(test_validate_trigger, "B:0xH1234_0xH1235<510", ""); - TEST_PARAMS2(test_validate_trigger, "B:0xH1234_0xH1235<=510", ""); - - /* A - da + 255 > 255 */ - TEST_PARAMS2(test_validate_trigger, "A:255_B:d0xH1234_0xH1234>255", ""); - - /* division by self results in a 0 or 1 */ - TEST_PARAMS2(test_validate_trigger, "B:0xH1241/0xH1241_B:0xH124c/0xH124c_M:2=2", ""); - TEST_PARAMS2(test_validate_trigger, "B:0xH1241/0xH1241_B:0xH124c/0xH124c_M:2>2", "Condition 3: Comparison is never true (max 2)"); -} - -void test_size_comparisons() { - TEST_PARAMS2(test_validate_trigger, "0xH1234>0xH1235", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234>0x 1235", "Condition 1: Comparing different memory sizes"); - - /* AddSource chain may compare different sizes without warning as the chain changes the - * size of the final result. */ - TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235=0xH2345", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xH1234_0xH1235=0x 2345", ""); -} - -void test_address_range() { - /* basic checks for each side */ - TEST_PARAMS2(test_validate_trigger_64k, "0xH1234>0xH1235", ""); - TEST_PARAMS2(test_validate_trigger_64k, "0xH12345>0xH1235", "Condition 1: Address 12345 out of range (max FFFF)"); - TEST_PARAMS2(test_validate_trigger_64k, "0xH1234>0xH12345", "Condition 1: Address 12345 out of range (max FFFF)"); - TEST_PARAMS2(test_validate_trigger_64k, "0xH12345>0xH12345", "Condition 1: Address 12345 out of range (max FFFF)"); - TEST_PARAMS2(test_validate_trigger_64k, "0xX1234>h12345", ""); - TEST_PARAMS2(test_validate_trigger_64k, "K:0xX1234&1073741823_K:0xX2345+{recall}_0=1", ""); - - /* support for multiple memory blocks and edge addresses */ - TEST_PARAMS2(test_validate_trigger_128k, "0xH1234>0xH1235", ""); - TEST_PARAMS2(test_validate_trigger_128k, "0xH12345>0xH1235", ""); - TEST_PARAMS2(test_validate_trigger_128k, "0xH0000>5", ""); - TEST_PARAMS2(test_validate_trigger_128k, "0xH1FFFF>5", ""); - TEST_PARAMS2(test_validate_trigger_128k, "0xH20000>5", "Condition 1: Address 20000 out of range (max 1FFFF)"); - - /* AddAddress can use really big values for negative offsets, don't flag them. */ - TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xHFFFFFF00>5", ""); - TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xH1234>5_0xHFFFFFF00>5", "Condition 3: Address FFFFFF00 out of range (max 1FFFF)"); - TEST_PARAMS2(test_validate_trigger_128k, "I:0xX1234_0xHFFFFFF00*2>5", ""); - - /* console-specific warnings */ - TEST_PARAMS3(test_validate_trigger_console, "0xH0123>23", "", RC_CONSOLE_NINTENDO); - TEST_PARAMS3(test_validate_trigger_console, "0xH07FF>23", "", RC_CONSOLE_NINTENDO); - TEST_PARAMS3(test_validate_trigger_console, "0xH0800>23", "Condition 1: Mirror RAM may not be exposed by emulator (address 0800)", RC_CONSOLE_NINTENDO); - TEST_PARAMS3(test_validate_trigger_console, "0xH1FFF>23", "Condition 1: Mirror RAM may not be exposed by emulator (address 1FFF)", RC_CONSOLE_NINTENDO); - TEST_PARAMS3(test_validate_trigger_console, "0xH2000>23", "", RC_CONSOLE_NINTENDO); - TEST_PARAMS3(test_validate_trigger_console, "0xH0123>0xH1000", "Condition 1: Mirror RAM may not be exposed by emulator (address 1000)", RC_CONSOLE_NINTENDO); - - TEST_PARAMS3(test_validate_trigger_console, "0xHC123>23", "", RC_CONSOLE_GAMEBOY); - TEST_PARAMS3(test_validate_trigger_console, "0xHDFFF>23", "", RC_CONSOLE_GAMEBOY); - TEST_PARAMS3(test_validate_trigger_console, "0xHE000>23", "Condition 1: Echo RAM may not be exposed by emulator (address E000)", RC_CONSOLE_GAMEBOY); - TEST_PARAMS3(test_validate_trigger_console, "0xHFDFF>23", "Condition 1: Echo RAM may not be exposed by emulator (address FDFF)", RC_CONSOLE_GAMEBOY); - TEST_PARAMS3(test_validate_trigger_console, "0xHFE00>23", "", RC_CONSOLE_GAMEBOY); - - TEST_PARAMS3(test_validate_trigger_console, "0xHC123>23", "", RC_CONSOLE_GAMEBOY_COLOR); - TEST_PARAMS3(test_validate_trigger_console, "0xHDFFF>23", "", RC_CONSOLE_GAMEBOY_COLOR); - TEST_PARAMS3(test_validate_trigger_console, "0xHE000>23", "Condition 1: Echo RAM may not be exposed by emulator (address E000)", RC_CONSOLE_GAMEBOY_COLOR); - TEST_PARAMS3(test_validate_trigger_console, "0xHFDFF>23", "Condition 1: Echo RAM may not be exposed by emulator (address FDFF)", RC_CONSOLE_GAMEBOY_COLOR); - TEST_PARAMS3(test_validate_trigger_console, "0xHFE00>23", "", RC_CONSOLE_GAMEBOY_COLOR); - - TEST_PARAMS3(test_validate_trigger_console, "0xH9E20=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address 9E20)", RC_CONSOLE_PLAYSTATION); - TEST_PARAMS3(test_validate_trigger_console, "0xHB8BE=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address B8BE)", RC_CONSOLE_PLAYSTATION); - TEST_PARAMS3(test_validate_trigger_console, "0xHFFFF=68", "Condition 1: Kernel RAM may not be initialized without real BIOS (address FFFF)", RC_CONSOLE_PLAYSTATION); - TEST_PARAMS3(test_validate_trigger_console, "0xH10000=68", "", RC_CONSOLE_PLAYSTATION); -} - -void test_delta_pointers() { - TEST_PARAMS2(test_validate_trigger, "I:0xX1234_0xH0000=1", ""); - TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_0xH0000=1", "Condition 1: Using pointer from previous frame"); - TEST_PARAMS2(test_validate_trigger, "I:p0xX1234_0xH0000=1", "Condition 1: Using pointer from previous frame"); - TEST_PARAMS2(test_validate_trigger, "I:0xX1234_d0xH0000=1", ""); - TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_I:d0xH0010_0xH0000=1", "Condition 1: Using pointer from previous frame"); - TEST_PARAMS2(test_validate_trigger, "I:d0xX1234_I:0xH0010_0xH0000=1", "Condition 1: Using pointer from previous frame"); - TEST_PARAMS2(test_validate_trigger, "I:0xX1234_I:d0xH0010_0xH0000=1", "Condition 2: Using pointer from previous frame"); - TEST_PARAMS2(test_validate_trigger, "I:0xX1234_I:0xH0010_0xH0000=1", ""); -} - -void test_nonsized_pointers() { - TEST_PARAMS2(test_validate_trigger, "I:Ff1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:Fb1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:Fm1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:Fh1234_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:0xH1234*f1.5_0xH0000=1", "Condition 1: Using non-integer value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:b0xH1234_0xH0000=1", "Condition 1: Using transformed value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:~0xH1234_0xH0000=1", "Condition 1: Using transformed value in AddAddress calculation"); - TEST_PARAMS2(test_validate_trigger, "I:~0xH1234*1_0xH0000=1", ""); /* don't report scaled values - assume indexed array */ - TEST_PARAMS2(test_validate_trigger, "I:~0xM1234*4096_0xH0000=1", ""); /* don't report scaled values - assume indexed array */ - TEST_PARAMS2(test_validate_trigger, "I:b0x 1234*8_0xH0000=1", ""); /* don't report scaled values - assume indexed array */ -} - -void test_float_comparisons() { - TEST_PARAMS2(test_validate_trigger, "fF1234=f2.3", ""); - TEST_PARAMS2(test_validate_trigger, "fM1234=f2.3", ""); - TEST_PARAMS2(test_validate_trigger, "fF1234=2", ""); - TEST_PARAMS2(test_validate_trigger, "0xX1234=2", ""); - TEST_PARAMS2(test_validate_trigger, "0xX1234=f2.3", "Condition 1: Comparison is never true (integer can never be 2.3)"); /* non integral comparison */ - TEST_PARAMS2(test_validate_trigger, "0xX1234!=f2.3", "Condition 1: Comparison is always true (integer can never be 2.3)"); /* non integral comparison */ - TEST_PARAMS2(test_validate_trigger, "0xX1234f2.3", ""); /* will be converted to > 2 */ - TEST_PARAMS2(test_validate_trigger, "0xX1234>=f2.3", ""); /* will be converted to >= 3 */ - TEST_PARAMS2(test_validate_trigger, "0xX1234=f2.0", ""); /* float can be converted to int without loss of data*/ - TEST_PARAMS2(test_validate_trigger, "0xH1234=f2.3", "Condition 1: Comparison is never true (integer can never be 2.3)"); - TEST_PARAMS2(test_validate_trigger, "0xH1234=f300.0", "Condition 1: Comparison is never true (max 255)"); /* value out of range */ - TEST_PARAMS2(test_validate_trigger, "f2.3=fF1234", ""); - TEST_PARAMS2(test_validate_trigger, "f2.3=0xX1234", "Condition 1: Comparison is never true (integer can never be 2.3)"); /* non integral comparison */ - TEST_PARAMS2(test_validate_trigger, "f2.0=0xX1234", ""); - TEST_PARAMS2(test_validate_trigger, "A:Ff2345_fF1234=f2.3", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xX2345_fF1234=f2.3", ""); - TEST_PARAMS2(test_validate_trigger, "A:Ff2345_0x1234=f2.3", "Condition 2: Comparison is never true (integer can never be 2.3)"); /* non integral comparison */ - TEST_PARAMS2(test_validate_trigger, "fM1234>f2.3", ""); - TEST_PARAMS2(test_validate_trigger, "fM1234>f-2.3", ""); - TEST_PARAMS2(test_validate_trigger, "I:0xX2345_fM1234>f1.0", ""); - TEST_PARAMS2(test_validate_trigger, "I:0xX2345_fM1234>f-1.0", ""); - TEST_PARAMS2(test_validate_trigger, "fF1234>=f0.0", ""); /* explicit float comparison can be negative */ - TEST_PARAMS2(test_validate_trigger, "fM1234>=f0.0", ""); - TEST_PARAMS2(test_validate_trigger, "fB1234>=f0.0", ""); - TEST_PARAMS2(test_validate_trigger, "fF1234>=0", ""); /* implicit float comparison can be negative */ - TEST_PARAMS2(test_validate_trigger, "fM1234>=0", ""); - TEST_PARAMS2(test_validate_trigger, "fB1234>=0", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234>=f0.1", ""); /* 0 can be less than 0.1 */ - TEST_PARAMS2(test_validate_trigger, "0xH1234>=f255.1", "Condition 1: Comparison is never true (max 255)"); /* 255 cannot be >= 255.1 */ - TEST_PARAMS2(test_validate_trigger, "f0.1<=0xH1234", ""); /* 0 can be less than 0.1 */ - TEST_PARAMS2(test_validate_trigger, "f255.1<=0xH1234", "Condition 1: Comparison is never true (max 255)"); /* 255 cannot be >= 255.1 */ -} - -void test_dependent_conditions() { - TEST_PARAMS2(test_validate_trigger, "M:0xH1234=30_Q:0xH2345!=0", ""); - TEST_PARAMS2(test_validate_trigger, "G:0xH1234=30_Q:0xH2345!=0", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234=30_Q:0xH2345!=0", "Condition 2: MeasuredIf without Measured"); - TEST_PARAMS2(test_validate_trigger, "M:0xH1234=30SQ:0xH2345!=0", "Alt1 Condition 1: MeasuredIf without Measured"); -} - -void test_conflicting_conditions() { - TEST_PARAMS2(test_validate_trigger, "0xH0000=1_0xH0000=2", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000>5", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000=5", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0001=5", ""); /* ignore differing address */ - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0x 0000=5", ""); /* ignore differing size */ - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_d0xH0000=5", ""); /* ignore differing type */ - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000=5.1.", ""); /* ignore anything with a hit target */ - TEST_PARAMS2(test_validate_trigger, "O:0xH0000<5_0xH0000=5", ""); /* ignore combining conditions */ - TEST_PARAMS2(test_validate_trigger, "A:0xH0000<5_0xH0000=5", ""); /* ignore combining conditions */ - TEST_PARAMS2(test_validate_trigger, "N:0xH0000<5_R:0xH0001=8_T:0xH0000=0_0xH0002=1.1.", ""); /* ignore combining conditions */ - TEST_PARAMS2(test_validate_trigger, "T:0xH0000=8_N:d0xH0000=0_R:0xH0000=8_0xH0001=1.1.", ""); /* ignore combining conditions - individually, third conditions is conflicting (second allowed because of delta) */ - TEST_PARAMS2(test_validate_trigger, "0xH0001=58.1._N:0xH0001!=58_N:0xH0001!=4_R:0xH0001!=18", ""); /* ignore combining conditions */ - TEST_PARAMS2(test_validate_trigger, "0xH0000<=5_0xH0000>=5", ""); - TEST_PARAMS2(test_validate_trigger, "0xH0000>1_0xH0000<3", ""); - TEST_PARAMS2(test_validate_trigger, "1=1S0xH0000=1S0xH0000=2", ""); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=2", "Alt1 Condition 1: Conflicts with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000=1S0xH0000=1_0xH0001=1.1.", "Alt1 Condition 1: Conflicts with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1SR:0xH0000=1_0xH0001=1.1.", "Alt1 Condition 1: Conflicts with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_0xH0000=1", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1S0xH0000=1", ""); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SP:0xH0000!=1", "Alt1 Condition 1: Conflicts with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_P:0xH0000!=1", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000=1_0xH0001=1.1.", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1SP:0xH0000=5", ""); - TEST_PARAMS2(test_validate_trigger, "M:0xH0000=5_Q:0xH0000=255", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_Q:0xH0000=2_M:0xH0001=1", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_0xH0000=2_M:0xH0001=1", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1_Q:0xH0000=2_M:0xH0001=1", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_0xH0000<5_A:0xX0004_0xH0000>5", "Condition 4: Conflicts with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "A:d0xX0004_d0xH0000<5_A:0xX0004_0xH0000>5", ""); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_d0xH0000<5_A:0xX0004_0xH0000>5", ""); - - /* PauseIf prevents hits from incrementing. ResetIf clears all hits. If both exist and are conflicting, the group - * will only ever be paused or reset, and therefore will never be true */ - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000!=1_0xH0001=1.1.", "Condition 2: Conflicts with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=1_P:0xH0000=1_0xH0001=1.1.", "Condition 2: Conflicts with Condition 1"); - /* if the PauseIf is less restrictive than the ResetIf, it's just a guard. ignore it*/ - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_R:0xH0000!=6_0xH0001=1.1.", ""); - /* PauseIf in alternate group does not affect the ResetIf*/ - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SR:0xH0000!=1_0xH0001=1.1.", ""); - - /* cannot determine OrNext conflicts */ - TEST_PARAMS2(test_validate_trigger, "O:0xH0000=1_0xH0001=1_O:0xH0000=2_0xH0001=2", ""); - TEST_PARAMS2(test_validate_trigger, "O:0xH0000=1_0xH0001=1_O:0xH0000=1_0xH0001=2", ""); - - /* cannot determine AddSource conflicts */ - TEST_PARAMS2(test_validate_trigger, "d0xH1234>0_A:0xH2345_0>d0xH1234", ""); - - /* AndNext conflicts are limited to matching the last condition after exactly matching the others */ - TEST_PARAMS2(test_validate_trigger, "N:0xH0000=1_0xH0001=1_N:0xH0000=2_0xH0001=2", ""); - TEST_PARAMS2(test_validate_trigger, "N:0xH0000=1_0xH0001=1_N:0xH0000=2_0xH0001=1", ""); /* technically conflicting, but hard to detect */ - TEST_PARAMS2(test_validate_trigger, "N:0xH0000=1_0xH0001=1_N:0xH0000=1_0xH0001=2", "Condition 4: Conflicts with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=0_N:0xH0000!=0_0xH0000=2", ""); -} - -void test_redundant_conditions() { - TEST_PARAMS2(test_validate_trigger, "0xH0000=1_0xH0000=1", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000<3_0xH0000<5", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000<5_0xH0000<3", "Condition 1: Redundant with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=1", "Alt1 Condition 1: Redundant with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1_0xH0000!=1", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0x 0001=50.1._0x 0000>=300_R:0x 0000<2", ""); /* ResetIf not redundant if hit target available */ - TEST_PARAMS2(test_validate_trigger, "0x 0001=50.1._R:0x 0000>=300_R:0x 0000<2", ""); - TEST_PARAMS2(test_validate_trigger, "0x 0001=50.1._R:0x 0000<300_R:0x 0000<2", "Condition 2: Redundant with Condition 3"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1_P:0xH0000!=1", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000!=1S0xH0000!=1", "Alt1 Condition 1: Redundant with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000=1", "Alt1 Condition 1: Redundant with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "P:0xH0000=1SP:0xH0000=1", ""); /* same pauseif can appear in different groups */ - TEST_PARAMS2(test_validate_trigger, "0xH0000=4.1._0xH0000=5_P:0xH0000<4", ""); - TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=5_Q:0xH0000!=255_M:0xH0001=1", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "M:0xH0000=5_Q:0xH0000!=255", ""); /* measuredif not redundant measured */ - TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_0xH0000=1_M:0xH0001=1", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_Q:0xH0000=1_M:0xH0001=1", "Condition 2: Redundant with Condition 1"); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", "Condition 4: Redundant with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_0xH0000=1_M:0xH0001=1", "Condition 4: Redundant with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", "Condition 2: Redundant with Condition 4"); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0005_Q:0xH0000=1_M:0xH0001=1", ""); /* different chains */ - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0x 0004_Q:0xH0000=1_M:0xH0001=1", ""); /* different sizes */ - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", "Condition 4: Redundant with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_A:0xX0008_Q:0xH0000=1_A:0xX0004_Q:0xH0000=1_M:0xH0001=1", ""); /* longer first chain */ - TEST_PARAMS2(test_validate_trigger, "A:0xX0004_Q:0xH0000=1_A:0xX0004_A:0xX0008_Q:0xH0000=1_M:0xH0001=1", ""); /* longer second chain */ - TEST_PARAMS2(test_validate_trigger, "Q:0xH0000=1_M:0xH0001=1SQ:0xH0000=1_M:0xH0002=1", ""); /* same measuredif can appear in different groups */ - TEST_PARAMS2(test_validate_trigger, "T:0xH0000!=0_T:0xH0000=6", "Condition 1: Redundant with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "0xH0000!=0_T:0xH0000=6", ""); /* trigger more restrictive than non-trigger */ - TEST_PARAMS2(test_validate_trigger, "T:0xH0000!=0_0xH0000=6", "Condition 1: Redundant with Condition 2"); /* trigger less restrictive than non-trigger */ - TEST_PARAMS2(test_validate_trigger, "0xH0000=6_T:0xH0000=6", "Condition 2: Redundant with Condition 1"); /* trigger same as non-trigger */ - TEST_PARAMS2(test_validate_trigger, "0xH0000=1_Q:0xH0000=1_M:0xH0001=1", "Condition 1: Redundant with Condition 2"); - TEST_PARAMS2(test_validate_trigger, "0xH0000=1S0xH0000!=0S0xH0001=2", "Alt1 Condition 1: Redundant with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000!=0S0xH0000=1S0xH0001=2", ""); /* more restrictive alt 1 is not redundant with core */ - TEST_PARAMS2(test_validate_trigger, "0xH0000!=0S0xH0000!=0S0xH0001=2", "Alt1 Condition 1: Redundant with Core Condition 1"); - TEST_PARAMS2(test_validate_trigger, "0xH0000<10_N:0xH0001=2_0xH0000<20", ""); /* AndNext should prevent condition 3 being compared directly to condition 1 */ - TEST_PARAMS2(test_validate_trigger, "0xH0001=7.1._R:0xH0000=0_T:0xH0000=1", ""); -} - -void test_resetif_hittargets() { - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0", "Condition 1: No captured hits to reset"); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0_0xH0001=1.1.", ""); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0_M:0xH0001=1.1.", ""); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0.1._0xH0001=1.1.", "Condition 1: Hit target of 1 is redundant on ResetIf"); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0.2._0xH0001=1.1.", ""); - TEST_PARAMS2(test_validate_trigger, "I:0xG1234&536870911_R:0xG0000=4294967294_0xH2222=1.1.", ""); - TEST_PARAMS2(test_validate_trigger, "N:0xH0001=4.1._T:0xH0001=5_R:0xH0001<2", ""); - TEST_PARAMS2(test_validate_trigger, "R:0xH0000!=0S0xH0001=1.1.S0xH0002=1", ""); -} - -void test_variable_operand_errors() { - TEST_PARAMS2(test_validate_trigger, "K:4_M:{thingy}", "Unknown variable name"); /* variable that does not exist */ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{th$ingy}", "Invalid variable name"); /* separator in name */ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{th*ingy}", "Invalid variable name"); /* invalid character in name */ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{2things}", "Invalid variable name"); /* variable name begins with number*/ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{recall_P:0xH01=18", "Invalid variable name"); /* missing closing curly brace */ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{thisvariablenameistoolong}_P:0xH01=18", "Invalid variable name"); /*too long */ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{}_P:0xH01=18", "Invalid variable name"); /* no name */ - TEST_PARAMS2(test_validate_trigger, "K:4_M:{recall}=4", ""); /* recognized as recall operand */ -} - -void test_remember_recall_errors() { - TEST_PARAMS2(test_validate_trigger, "{recall}=5", "Condition 1: Recall used before Remember"); /* No value ever remembered */ - TEST_PARAMS2(test_validate_trigger, "{recall}=5_K:0xH1234&1023_K:{recall}*8_{recall}=100", "Condition 1: Recall used before Remember"); /* First remember is after first recall. */ - TEST_PARAMS2(test_validate_trigger, "K:0xH1234&1023_K:{recall}*8_{recall}=100", ""); /* Recall used after Remember */ - TEST_PARAMS2(test_validate_trigger, "{recall}=5_K:0xH1234*2_P:{recall}>6", ""); /* Remember sets recall in pause - no warning */ - TEST_PARAMS2(test_validate_trigger, "K:0xH1234*2_{recall}=5_P:{recall}>6", "Condition 3: Recall used before Remember"); /* Pause happens before remembered value. */ -} - -void test_error_priorities() { - TEST_PARAMS2(test_validate_trigger, "R:0xH1234=1_0xH2345=500", "Condition 2: Comparison is never true (max 255)"); /* impossible condition more important than unnecessary reset */ - TEST_PARAMS2(test_validate_trigger, "0xH1234>5_0xH1234!=0_A:0xH2345", "Condition 3: AddSource condition type expects another condition to follow"); /* impotent condition more important than redundant */ - TEST_PARAMS2(test_validate_trigger, "0xH1234!=d0x 1234_I:d0x2345_0=6", "Condition 2: Using pointer from previous frame"); /* pointer math more important that potential logic errors */ - TEST_PARAMS2(test_validate_trigger, "R:0xH1234=1.1.S0xH2345=500", "Alt1 Condition 1: Comparison is never true (max 255)"); /* impossible condition in alt more important than redundant condition in core */ -} - -void test_rc_validate(void) { - TEST_SUITE_BEGIN(); - - /* positive baseline test cases */ - TEST_PARAMS2(test_validate_trigger, "", ""); - TEST_PARAMS2(test_validate_trigger, "0xH1234=1_0xH2345=2S0xH3456=1S0xH3456=2", ""); - TEST_PARAMS2(test_validate_trigger, "S0xH3456=1S0xH3456=2", ""); /* empty core */ - - test_combining_conditions_at_end_of_definition(); - test_addhits_chain_without_target(); - test_range_comparisons(); - test_size_comparisons(); - test_address_range(); - test_delta_pointers(); - test_nonsized_pointers(); - test_float_comparisons(); - test_dependent_conditions(); - test_conflicting_conditions(); - test_redundant_conditions(); - test_resetif_hittargets(); - test_variable_operand_errors(); - test_remember_recall_errors(); - test_error_priorities(); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_richpresence.c b/src/rcheevos/test/rcheevos/test_richpresence.c deleted file mode 100644 index 941dbfee79..0000000000 --- a/src/rcheevos/test/rcheevos/test_richpresence.c +++ /dev/null @@ -1,1542 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -#include "../src/rc_compat.h" - -static void _assert_parse_richpresence(rc_richpresence_t** richpresence, void* buffer, size_t buffer_size, const char* script) { - int size; - unsigned* overflow; - *richpresence = NULL; - - size = rc_richpresence_size(script); - ASSERT_NUM_GREATER(size, 0); - ASSERT_NUM_LESS_EQUALS(size + 4, buffer_size); - - overflow = (unsigned*)(((char*)buffer) + size); - *overflow = 0xCDCDCDCD; - - *richpresence = rc_parse_richpresence(buffer, script, NULL, 0); - ASSERT_PTR_NOT_NULL(*richpresence); - - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } -} -#define assert_parse_richpresence(richpresence_out, buffer, script) ASSERT_HELPER(_assert_parse_richpresence(richpresence_out, buffer, sizeof(buffer), script), "assert_parse_richpresence") - -static void _assert_richpresence_output(rc_richpresence_t* richpresence, memory_t* memory, const char* expected_display_string) { - char output[256]; - int result; - - result = rc_evaluate_richpresence(richpresence, output, sizeof(output), peek, memory, NULL); - ASSERT_STR_EQUALS(output, expected_display_string); - ASSERT_NUM_EQUALS(result, strlen(expected_display_string)); -} -#define assert_richpresence_output(richpresence, memory, expected_display_string) ASSERT_HELPER(_assert_richpresence_output(richpresence, memory, expected_display_string), "assert_richpresence_output") - -static void test_empty_script() { - int lines; - int result = rc_richpresence_size_lines("", &lines); - ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); - ASSERT_NUM_EQUALS(lines, 1); -} - -static void test_simple_richpresence(const char* script, const char* expected_display_string) { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, script); - assert_richpresence_output(richpresence, &memory, expected_display_string); -} - -static void assert_buffer_boundary(rc_richpresence_t* richpresence, memory_t* memory, int buffersize, int expected_result, const char* expected_display_string) { - char output[256]; - int result; - unsigned* overflow = (unsigned*)(&output[buffersize]); - *overflow = 0xCDCDCDCD; - - result = rc_evaluate_richpresence(richpresence, output, buffersize, peek, memory, NULL); - ASSERT_NUM_EQUALS(result, expected_result); - - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } - - ASSERT_STR_EQUALS(output, expected_display_string); -} - -static void test_buffer_boundary() { - uint8_t ram[] = { 0x00, 0x00, 0x00, 0x01, 0x00 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[2048]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* static strings */ - assert_parse_richpresence(&richpresence, buffer, "Display:\nABCDEFGH"); - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "ABCDEF"); /* only 6 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "ABCDEFG"); /* only 7 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "ABCDEFGH"); /* all 8 chars written */ - - /* number formatting */ - assert_parse_richpresence(&richpresence, buffer, "Format:V\nFormatType=VALUE\n\nDisplay:\n@V(0xX0000)"); - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 10, "16,777,2"); /* only 8 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 10, "16,777,21"); /* only 8 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 11, 10, "16,777,216"); /* all 10 chars written */ - - /* lookup */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:L\n1=ABCDEFGH\n\nDisplay:\n@L(0xH0003)"); - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 8, "ABCDEF"); /* only 6 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 8, "ABCDEFG"); /* only 7 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 8, "ABCDEFGH"); /* all 8 chars written */ - - /* unknown macro - "[Unknown macro]L(0xH0003)" = 25 chars */ - assert_parse_richpresence(&richpresence, buffer, "Display:\n@L(0xH0003)"); - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 7, 25, "[Unkno"); /* only 6 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 25, 25, "[Unknown macro]L(0xH0003"); /* only 24 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 26, 25, "[Unknown macro]L(0xH0003)"); /* all 25 chars written */ - - /* multipart */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:L\n0=\n1=A\n4=ABCD\n8=ABCDEFGH\n\nFormat:V\nFormatType=VALUE\n\nDisplay:\n@L(0xH0000)--@L(0xH0001)--@V(0xH0002)"); - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 5, "----0"); /* initial value fits */ - ram[1] = 4; - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 8, 9, "--ABCD-"); /* only 7 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 9, 9, "--ABCD--"); /* only 8 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 9, "--ABCD--0"); /* all 9 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 5, 9, "--AB"); /* only 7 chars written */ - ram[2] = 123; - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 10, 11, "--ABCD--1"); /* only 9 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 11, 11, "--ABCD--12"); /* only 10 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 12, 11, "--ABCD--123"); /* all 11 chars written */ - TEST_PARAMS5(assert_buffer_boundary, richpresence, &memory, 2, 11, "-"); /* only 1 char written */ -} - -static void test_conditional_display_simple() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?Zero\n?0xH0000=1?One\nOther"); - assert_richpresence_output(richpresence, &memory, "Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "One"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "Other"); -} - -static void test_conditional_display_after_default() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\nOther\n?0xH0000=0?Zero\n?0xH0000=1?One"); - assert_richpresence_output(richpresence, &memory, "Other"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Other"); -} - -static void test_conditional_display_no_default() { - int lines; - int result = rc_richpresence_size_lines("Display:\n?0xH0000=0?Zero", &lines); - ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); - ASSERT_NUM_EQUALS(lines, 3); -} - -static void test_conditional_display_common_condition() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* condition for Second is a sub-clause of First */ - assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0_0xH0001=18?First\n?0xH0000=0?Second\nThird"); - assert_richpresence_output(richpresence, &memory, "First"); - - /* secondary part of first condition is false, will match second condition */ - ram[1] = 1; - assert_richpresence_output(richpresence, &memory, "Second"); - - /* common condition is false, will use default */ - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Third"); - - /* ================================================================ */ - /* == reverse the conditions so the First is a sub-clause of Second */ - assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?First\n?0xH0000=0_0xH0001=18?Second\nThird"); - - /* reset the memory so it matches the first test, First clause will be matched before even looking at Second */ - ram[0] = 0; - ram[1] = 18; - assert_richpresence_output(richpresence, &memory, "First"); - - /* secondary part of second condition is false, will still match first condition */ - ram[1] = 1; - assert_richpresence_output(richpresence, &memory, "First"); - - /* common condition is false, will use default */ - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Third"); -} - -static void test_conditional_display_duplicated_condition() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0?First\n?0xH0000=0?Second\nThird"); - assert_richpresence_output(richpresence, &memory, "First"); - - /* cannot activate Second */ - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Third"); -} - -static void test_conditional_display_invalid_condition_logic() { - int lines; - int result = rc_richpresence_size_lines("Display:\n?BANANA?Zero\nDefault", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); - ASSERT_NUM_EQUALS(lines, 2); -} - -static void test_conditional_display_shared_lookup() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0001=1?One @Points(0xL0002)\n?0xH0001=0?Zero @Points(0xL0002)\nDefault @Points(0xL0002)"); - assert_richpresence_output(richpresence, &memory, "Default 4"); - - ram[1] = 1; - ram[2] = 24; - assert_richpresence_output(richpresence, &memory, "One 8"); - - ram[1] = 0; - assert_richpresence_output(richpresence, &memory, "Zero 8"); -} - -static void test_conditional_display_whitespace_text() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n?0xH0000=0? \n?0xH0000=1?One\nOther"); - assert_richpresence_output(richpresence, &memory, " "); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "One"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "Other"); -} - -static void test_macro_value() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); - assert_richpresence_output(richpresence, &memory, "13,330 Points"); - - ram[1] = 20; - assert_richpresence_output(richpresence, &memory, "13,332 Points"); -} - -static void test_macro_value_nibble() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* nibble first, see if byte overwrites */ - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xL0001)@Points(0xH0001) Points"); - assert_richpresence_output(richpresence, &memory, "218 Points"); - - ram[1] = 20; - assert_richpresence_output(richpresence, &memory, "420 Points"); - - /* put byte first, see if nibble overwrites */ - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH0001)@Points(0xL0001) Points"); - assert_richpresence_output(richpresence, &memory, "204 Points"); -} - -static void test_macro_value_bcd() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(b0xH0001) Points"); - assert_richpresence_output(richpresence, &memory, "12 Points"); - - ram[1] = 20; - assert_richpresence_output(richpresence, &memory, "14 Points"); -} - -static void test_macro_value_bitcount() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Bits\nFormatType=VALUE\n\nDisplay:\n@Bits(0xK0001) Bits"); - assert_richpresence_output(richpresence, &memory, "2 Bits"); - - ram[1] = 0x76; - assert_richpresence_output(richpresence, &memory, "5 Bits"); -} - -static void test_conditional_display_indirect() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n?I:0xH0000_0xH0002=h01?True\nFalse\n"); - assert_richpresence_output(richpresence, &memory, "False"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "False"); - - ram[3] = 1; - assert_richpresence_output(richpresence, &memory, "True"); -} - -static void test_conditional_display_unnecessary_measured() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n?M:0xH0000=0?Zero\n?0xH0000=1?One\nOther"); - assert_richpresence_output(richpresence, &memory, "Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "One"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "Other"); -} - -static void test_conditional_display_unnecessary_measured_indirect() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n?I:0xH0000_M:0xH0002=h01?True\nFalse\n"); - assert_richpresence_output(richpresence, &memory, "False"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "False"); - - ram[3] = 1; - assert_richpresence_output(richpresence, &memory, "True"); -} - -static void test_conditional_display_invalid() { - int lines_read = 0; - ASSERT_NUM_EQUALS(rc_richpresence_size_lines("Display:\n?I:0x0x0000=1?True\nFalse\n", &lines_read), RC_INVALID_MEMORY_OPERAND); - ASSERT_NUM_EQUALS(lines_read, 2); - - ASSERT_NUM_EQUALS(rc_richpresence_size_lines("Display:\n?0x0000=1 0x0001=2?True\nFalse\n", &lines_read), RC_INVALID_OPERATOR); - ASSERT_NUM_EQUALS(lines_read, 2); -} - -static void test_macro_value_adjusted_negative() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001_V-10000) Points"); - assert_richpresence_output(richpresence, &memory, "3,330 Points"); - - ram[2] = 7; - assert_richpresence_output(richpresence, &memory, "-8,190 Points"); -} - -static void test_macro_value_from_formula() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH0001*100_0xH0002) Points"); - assert_richpresence_output(richpresence, &memory, "1,852 Points"); - - ram[1] = 32; - assert_richpresence_output(richpresence, &memory, "3,252 Points"); -} - -static void test_macro_value_from_hits() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Hits\nFormatType=VALUE\n\nDisplay:\n@Hits(M:0xH01=1) Hits"); - assert_richpresence_output(richpresence, &memory, "0 Hits"); - - ram[1] = 1; - assert_richpresence_output(richpresence, &memory, "1 Hits"); - assert_richpresence_output(richpresence, &memory, "2 Hits"); - assert_richpresence_output(richpresence, &memory, "3 Hits"); -} - -static void test_macro_value_from_indirect() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nPointing at @Value(I:0xH00_M:0xH01)"); - assert_richpresence_output(richpresence, &memory, "Pointing at 18"); - - /* pointed at data changes */ - ram[1] = 99; - assert_richpresence_output(richpresence, &memory, "Pointing at 99"); - - /* pointer changes */ - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Pointing at 52"); -} - -static void test_macro_value_divide_by_zero() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(0xH02/0xH00)"); - assert_richpresence_output(richpresence, &memory, "Result is 0"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Result is 52"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "Result is 26"); -} - -static void test_macro_value_divide_by_self() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* sneaky trick to turn any non-zero value into 1 */ - assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(0xH02/0xH02)"); - assert_richpresence_output(richpresence, &memory, "Result is 1"); - - ram[2] = 1; - assert_richpresence_output(richpresence, &memory, "Result is 1"); - - ram[2] = 32; - assert_richpresence_output(richpresence, &memory, "Result is 1"); - - ram[2] = 255; - assert_richpresence_output(richpresence, &memory, "Result is 1"); - - ram[2] = 0; - assert_richpresence_output(richpresence, &memory, "Result is 0"); -} - -static void test_macro_value_remember_recall() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* sneaky trick to turn any non-zero value into 1 */ - assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=VALUE\n\nDisplay:\nResult is @Value(A:0xH02*2_K:1_M:{recall}*3)"); - assert_richpresence_output(richpresence, &memory, "Result is 315"); - - ram[2] = 1; - assert_richpresence_output(richpresence, &memory, "Result is 9"); - - ram[2] = 0; - assert_richpresence_output(richpresence, &memory, "Result is 3"); -} - -static void test_macro_value_invalid() { - ASSERT_NUM_EQUALS(rc_richpresence_size("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x0x0001) Points"), RC_INVALID_MEMORY_OPERAND); -} - -static void test_macro_value_measured_if() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(Q:0xH000=0_M:0x 0001) Points"); - assert_richpresence_output(richpresence, &memory, "13,330 Points"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "0 Points"); - - ram[1] = 20; - assert_richpresence_output(richpresence, &memory, "0 Points"); - - ram[0] = 0; - assert_richpresence_output(richpresence, &memory, "13,332 Points"); -} - -static void test_multiple_macros() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Number\nFormatType=VALUE\n\nDisplay:\n@Number(0x 0001) Points | @Number(A:0xH0002_A:0xH0003_M:0xH0004) Items"); - assert_richpresence_output(richpresence, &memory, "13,330 Points | 309 Items"); - - ram[2] = 0x05; - assert_richpresence_output(richpresence, &memory, "1,298 Points | 262 Items"); - - /* both should map to memrefs, so no helper values will be created */ - ASSERT_PTR_NULL(richpresence->values); -} - -static void test_macro_hundreds() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Value\nFormatType=HUNDREDS\n\nDisplay:\nResult is @Value(0xH00)"); - assert_richpresence_output(richpresence, &memory, "Result is 0"); - - ram[0] = 18; - assert_richpresence_output(richpresence, &memory, "Result is 1,800"); - - ram[0] = 255; - assert_richpresence_output(richpresence, &memory, "Result is 25,500"); - - ram[0] = 0; - assert_richpresence_output(richpresence, &memory, "Result is 0"); -} - -static void test_macro_frames() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Frames\nFormatType=FRAMES\n\nDisplay:\n@Frames(0x 0001)"); - assert_richpresence_output(richpresence, &memory, "3:42.16"); - - ram[1] = 20; - assert_richpresence_output(richpresence, &memory, "3:42.20"); -} - -static void test_macro_float(const char* format, uint32_t value, const char* expected) { - uint8_t ram[4]; - memory_t memory; - rc_richpresence_t* richpresence; - char script[128]; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - ram[0] = (value & 0xFF); - ram[1] = (value >> 8) & 0xFF; - ram[2] = (value >> 16) & 0xFF; - ram[3] = (value >> 24) & 0xFF; - - snprintf(script, sizeof(script), "Format:N\nFormatType=%s\n\nDisplay:\n@N(fF0000)", format); - - assert_parse_richpresence(&richpresence, buffer, script); - assert_richpresence_output(richpresence, &memory, expected); -} - -static void test_macro_lookup_simple() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_macro_lookup_with_inline_comment() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n// Zero\n0=Zero\n// One\n1=One\n//2=Two\n\nDisplay:\nAt @Location(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_macro_lookup_hex_keys() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0x00=Zero\n0x01=One\n\nDisplay:\nAt @Location(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_macro_lookup_default() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n*=Star\n\nDisplay:\nAt @Location(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At Star"); -} - -static void test_macro_lookup_crlf() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\r\n0=Zero\r\n1=One\r\n\r\nDisplay:\r\nAt @Location(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_macro_lookup_after_display() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\nAt @Location(0xH0000)\n\nLookup:Location\n0=Zero\n1=One"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_macro_lookup_from_formula() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000*0.5)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At One"); -} - -static void test_macro_lookup_from_indirect() { - uint8_t ram[] = { 0x00, 0x00, 0x01, 0x00, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(I:0xH0000=0_M:0xH0001)"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At Zero"); -} - -static void test_macro_lookup_repeated() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* same lookup can be used for the same address */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000), Near @Location(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero, Near Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One, Near One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At , Near "); -} - -static void test_macro_lookup_shared() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* same lookup can be used for multiple addresses */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000), Near @Location(0xH0001)"); - assert_richpresence_output(richpresence, &memory, "At Zero, Near "); - - ram[1] = 1; - assert_richpresence_output(richpresence, &memory, "At Zero, Near One"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One, Near One"); -} - -static void test_macro_lookup_multiple() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* multiple lookups can be used for same address */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nLookup:Location2\n0=zero\n1=one\n\nDisplay:\nAt @Location(0xH0000), Near @Location2(0xH0000)"); - assert_richpresence_output(richpresence, &memory, "At Zero, Near zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One, Near one"); - - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At , Near "); -} - -static void test_macro_lookup_and_value() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0=Zero\n1=One\n\nFormat:Location2\nFormatType=VALUE\n\nDisplay:\nAt @Location(0xH0000), Near @Location2(0xH0001)"); - assert_richpresence_output(richpresence, &memory, "At Zero, Near 18"); - - ram[1] = 1; - assert_richpresence_output(richpresence, &memory, "At Zero, Near 1"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One, Near 1"); -} - -static void test_macro_lookup_negative_value() { - uint8_t ram[] = { 0x00, 0x00, 0x00, 0x00, 0x00 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* lookup keys are signed 32-bit values. the -1 will become 0xFFFFFFFF */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:Diff\n0=Zero\n1=One\n-1=Negative One\n\nDisplay:\nDiff=@Diff(B:0xH0000_M:0xH0001)"); - assert_richpresence_output(richpresence, &memory, "Diff=Zero"); - - ram[1] = 1; - assert_richpresence_output(richpresence, &memory, "Diff=One"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "Diff=Zero"); - - ram[1] = 0; - assert_richpresence_output(richpresence, &memory, "Diff=Negative One"); -} - -static void test_macro_lookup_value_with_whitespace() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Location\n0= Zero \n1= One \n\nDisplay:\nAt '@Location(0xH0000)' "); - assert_richpresence_output(richpresence, &memory, "At ' Zero ' "); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At ' One ' "); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At '' "); -} - -static void test_macro_lookup_mapping_repeated() { - uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* same lookup can be used for the same address */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:OddOrEven\n0=Even\n1=Odd\n2=Even\n3=Odd\n4=Even\n5=Odd\n\nDisplay:\nFirst:@OddOrEven(0xH0000), Second:@OddOrEven(0xH0001)"); - assert_richpresence_output(richpresence, &memory, "First:Even, Second:Even"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "First:Odd, Second:Even"); - - ram[0] = 2; - ram[1] = 3; - assert_richpresence_output(richpresence, &memory, "First:Even, Second:Odd"); -} - -static void test_macro_lookup_mapping_repeated_csv() { - uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* same lookup can be used for the same address */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:OddOrEven\n0,2,4=Even\n1,3,5=Odd\n\nDisplay:\nFirst:@OddOrEven(0xH0000), Second:@OddOrEven(0xH0001)"); - assert_richpresence_output(richpresence, &memory, "First:Even, Second:Even"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "First:Odd, Second:Even"); - - ram[0] = 2; - ram[1] = 3; - assert_richpresence_output(richpresence, &memory, "First:Even, Second:Odd"); -} - -static void test_macro_lookup_mapping_merged() { - uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* same lookup can be used for the same address */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:Place\n0=First\n1=First\n2=First\n3=Second\n4=Second\n5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)"); - assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); - - ram[0] = 5; - ram[1] = 2; - assert_richpresence_output(richpresence, &memory, "First:Second, Second:First"); - - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->first, 0); - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->last, 2); - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->first, 3); - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->last, 5); - ASSERT_PTR_NULL(richpresence->first_lookup->root->right->right); -} - -static void test_macro_lookup_mapping_range() { - uint8_t ram[] = { 0x00, 0x04, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* same lookup can be used for the same address */ - assert_parse_richpresence(&richpresence, buffer, "Lookup:Place\n0-2=First\n5,3-4=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)"); - assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "First:First, Second:Second"); - - ram[0] = 5; - ram[1] = 2; - assert_richpresence_output(richpresence, &memory, "First:Second, Second:First"); - - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->first, 0); - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->last, 2); - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->first, 3); - ASSERT_NUM_EQUALS(richpresence->first_lookup->root->right->last, 5); - ASSERT_PTR_NULL(richpresence->first_lookup->root->right->right); -} - -static void test_macro_lookup_mapping_range_overlap() { - int result; - int lines; - - /* 2 appears in the first range (2) and the second (2) */ - result = rc_richpresence_size_lines("Lookup:Place\n2=First\n2=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); - ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); - ASSERT_NUM_EQUALS(lines, 3); - - /* 2 appears in the first range (0-2) and the second (2-5) */ - result = rc_richpresence_size_lines("Lookup:Place\n0-2=First\n2-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); - ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); - ASSERT_NUM_EQUALS(lines, 3); - - /* 2 appears in the first range (0-2) and the second (2,4-5) */ - result = rc_richpresence_size_lines("Lookup:Place\n0-2=First\n2,4-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); - ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); - ASSERT_NUM_EQUALS(lines, 3); - - /* 1 and 2 appear in the first range (0-2) and the second (1,2,4-5) */ - result = rc_richpresence_size_lines("Lookup:Place\n0-2=First\n1,2,4-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); - ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); - ASSERT_NUM_EQUALS(lines, 3); - - /* 2 appears in the first range (2) and the second (1-5) */ - result = rc_richpresence_size_lines("Lookup:Place\n2=First\n1-5=Second\n\nDisplay:\nFirst:@Place(0xH0000), Second:@Place(0xH0001)", &lines); - ASSERT_NUM_EQUALS(result, RC_DUPLICATED_VALUE); - ASSERT_NUM_EQUALS(lines, 3); -} - -static void test_macro_lookup_invalid() { - int result; - int lines; - - /* lookup value starts with Ox instead of 0x */ - result = rc_richpresence_size_lines("Lookup:Location\nOx0=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); - ASSERT_NUM_EQUALS(lines, 2); - - /* lookup value contains invalid hex character */ - result = rc_richpresence_size_lines("Lookup:Location\n0xO=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); - ASSERT_NUM_EQUALS(lines, 2); - - /* lookup value is not numeric */ - result = rc_richpresence_size_lines("Lookup:Location\nZero=Zero\n1=One\n\nDisplay:\nAt @Location(0xH0000)", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_CONST_OPERAND); - ASSERT_NUM_EQUALS(lines, 2); -} - -static void test_macro_escaped() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ensures @ can be used in the display string by escaping it */ - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n\\@Points(0x 0001) \\@@Points(0x 0001) Points"); - assert_richpresence_output(richpresence, &memory, "@Points(0x 0001) @13,330 Points"); -} - -static void test_macro_undefined() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001) Points"); - assert_richpresence_output(richpresence, &memory, "[Unknown macro]Points(0x 0001) Points"); -} - -static void test_macro_undefined_at_end_of_line() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* adding [Unknown macro] to the output effectively makes the script larger than it started. - * since we don't detect unknown macros in `rc_richpresence_size`, this was causing a - * write-past-end-of-buffer memory corruption error. this test recreated that error. */ - assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001)"); - assert_richpresence_output(richpresence, &memory, "[Unknown macro]Points(0x 0001)"); -} - -static void test_macro_unterminated() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* valid macro with no closing parenthesis should just be dumped as-is */ - assert_parse_richpresence(&richpresence, buffer, "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001"); - assert_richpresence_output(richpresence, &memory, "@Points(0x 0001"); - - /* adding [Unknown macro] to the output effectively makes the script larger than it started. - * since we don't detect unknown macros in `rc_richpresence_size`, this was causing a - * write-past-end-of-buffer memory corruption error. this test recreated that error. */ - assert_parse_richpresence(&richpresence, buffer, "Display:\n@Points(0x 0001"); - assert_richpresence_output(richpresence, &memory, "@Points(0x 0001"); -} - -static void test_macro_without_parameter() { - int result; - int lines; - - result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points Points", &lines); - ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); - ASSERT_NUM_EQUALS(lines, 5); - - result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points() Points", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); - ASSERT_NUM_EQUALS(lines, 5); -} - -static void test_macro_without_parameter_conditional_display() { - int result; - int lines; - - result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n?0x0h0001=1?@Points Points\nDefault", &lines); - ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); - ASSERT_NUM_EQUALS(lines, 5); - - result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n?0x0h0001=1?@Points() Points\nDefault", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); - ASSERT_NUM_EQUALS(lines, 5); -} - -static void test_macro_non_numeric_parameter() { - int lines; - int result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(Zero) Points", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_MEMORY_OPERAND); - ASSERT_NUM_EQUALS(lines, 5); -} - -static void test_macro_mathematic_chain() { - int lines; - int result = rc_richpresence_size_lines("Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0xH1234 + 5) Points", &lines); - ASSERT_NUM_EQUALS(result, RC_INVALID_OPERATOR); - ASSERT_NUM_EQUALS(lines, 5); -} - -static void test_builtin_macro(const char* macro, const char* expected) { - uint8_t ram[] = { 0x39, 0x30 }; - memory_t memory; - rc_richpresence_t* richpresence; - char script[128]; - char buffer[256]; - - memory.ram = ram; - memory.size = sizeof(ram); - - snprintf(script, sizeof(script), "Display:\n@%s(0x 0)", macro); - - assert_parse_richpresence(&richpresence, buffer, script); - assert_richpresence_output(richpresence, &memory, expected); -} - -static void test_builtin_macro_float(const char* macro, const char* expected) { - uint8_t ram[] = { 0x92, 0x44, 0x9A, 0x42 }; /* 77.133926 */ - memory_t memory; - rc_richpresence_t* richpresence; - char script[128]; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - snprintf(script, sizeof(script), "Display:\n@%s(fF0000)", macro); - - assert_parse_richpresence(&richpresence, buffer, script); - assert_richpresence_output(richpresence, &memory, expected); -} - -static void test_builtin_macro_unsigned_large() { - uint8_t ram[] = { 0x85, 0xE2, 0x59, 0xC7 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[256]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Display:\n@Unsigned(0xX0)"); - assert_richpresence_output(richpresence, &memory, "3,344,556,677"); -} - -static void test_builtin_macro_override() { - uint8_t ram[] = { 0x39, 0x30 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Number\nFormatType=SECS\n\nDisplay:\n@Number(0x 0)"); - assert_richpresence_output(richpresence, &memory, "3h25:45"); -} - -static void test_unformatted_legacy() { - uint8_t ram[] = { 0x39, 0x30 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Format:Unformatted\nFormatType=VALUE\n\nDisplay:\n@Unformatted(0x 0)"); - assert_richpresence_output(richpresence, &memory, "12345"); -} - -static void test_asciichar() { - uint8_t ram[] = { 'K', 'e', 'n', '\0', 'V', 'e', 'g', 'a', 1 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Lookup:Round\n0= (Round 1)\n1= (Round 2)\n\n" - "Display:\n@ASCIIChar(0xH0)@ASCIIChar(0xH1)@ASCIIChar(0xH2)@ASCIIChar(0xH3) vs @ASCIIChar(0xH4)@ASCIIChar(0xH5)@ASCIIChar(0xH6)@ASCIIChar(0xH7)@Round(0xH8)"); - assert_richpresence_output(richpresence, &memory, "Ken vs Vega (Round 2)"); - - ram[0] = 'R'; ram[1] = 'o'; ram[2] = 's'; ram[3] = 'e'; - ram[4] = 'K'; ram[5] = 'e'; ram[6] = 'n'; ram[7] = '\0'; - ram[8] = 0; - assert_richpresence_output(richpresence, &memory, "Rose vs Ken (Round 1)"); -} - -static void test_ascii8(unsigned char c1, unsigned char c2, unsigned char c3, unsigned char c4, - unsigned char c5, unsigned char c6, unsigned char c7, unsigned char c8, char* expected) { - uint8_t ram[9]; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - ram[0] = c1; ram[1] = c2; ram[2] = c3; ram[3] = c4; - ram[4] = c5; ram[5] = c6; ram[6] = c7; ram[7] = c8; - ram[8] = '~'; - - assert_parse_richpresence(&richpresence, buffer, "Display:\n@ASCIIChar(0xH0)@ASCIIChar(0xH1)@ASCIIChar(0xH2)@ASCIIChar(0xH3)@ASCIIChar(0xH4)@ASCIIChar(0xH5)@ASCIIChar(0xH6)@ASCIIChar(0xH7)"); - assert_richpresence_output(richpresence, &memory, expected); -} - -static void test_unicode4(unsigned short c1, unsigned short c2, unsigned short c3, unsigned short c4, char* expected) { - uint8_t ram[10]; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - ram[0] = c1 & 0xFF; ram[1] = (c1 >> 8) & 0xFF; - ram[2] = c2 & 0xFF; ram[3] = (c2 >> 8) & 0xFF; - ram[4] = c3 & 0xFF; ram[5] = (c3 >> 8) & 0xFF; - ram[6] = c4 & 0xFF; ram[7] = (c4 >> 8) & 0xFF; - ram[8] = '~'; ram[9] = '\0'; - - assert_parse_richpresence(&richpresence, buffer, "Display:\n@UnicodeChar(0x 0)@UnicodeChar(0x 2)@UnicodeChar(0x 4)@UnicodeChar(0x 6)"); - assert_richpresence_output(richpresence, &memory, expected); -} - -static void test_random_text_between_sections() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "Locations are fun!\nLookup:Location\n0=Zero\n1=One\n\nDisplay goes here\nDisplay:\nAt @Location(0xH0000)\n\nWritten by User3"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_comments() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "// Locations are fun!\nLookup:Location // lookup\n0=Zero // 0\n1=One // 1\n\n//Display goes here\nDisplay: // display\nAt @Location(0xH0000) // text\n\n//Written by User3"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_comments_between_lines() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_richpresence_t* richpresence; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_richpresence(&richpresence, buffer, "// Locations are fun!\nLookup:Location\n// lookup\n0=Zero\n// 0\n1=One\n// 1\n\n//Display goes here\nDisplay:\n// display\nAt @Location(0xH0000)\n// text\n\n//Written by User3"); - assert_richpresence_output(richpresence, &memory, "At Zero"); - - ram[0] = 1; - assert_richpresence_output(richpresence, &memory, "At One"); - - /* no entry - default to empty string */ - ram[0] = 2; - assert_richpresence_output(richpresence, &memory, "At "); -} - -static void test_display_string_comment_only() { - int lines; - int result = rc_richpresence_size_lines("Display:\n// This is a comment\n// And another\n// And some whitespace", &lines); - ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); - ASSERT_NUM_EQUALS(lines, 5); /* end of file reached */ -} - -static void test_display_string_comment_with_blank_line() { - int lines; - int result = rc_richpresence_size_lines("Display:\n// This is a comment\n// And another\n\n// And some whitespace", &lines); - ASSERT_NUM_EQUALS(result, RC_MISSING_DISPLAY_STRING); - ASSERT_NUM_EQUALS(lines, 4); /* line 4 was blank */ -} - -void test_richpresence(void) { - TEST_SUITE_BEGIN(); - - TEST(test_empty_script); - - /* static display string */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nHello, world!", "Hello, world!"); - - /* static display string with trailing whitespace */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat ", "What "); - - /* static display string whitespace only*/ - TEST_PARAMS2(test_simple_richpresence, "Display:\n ", " "); - - /* static display string with comment (trailing whitespace will be trimmed) */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat // Where", "What"); - - /* static display string with escaped comment */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\// Where", "What // Where"); - - /* static display string with escaped backslash */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\\\ Where", "What \\ Where"); - - /* static display string with partially escaped comment */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\/// Where", "What /"); - - /* static display string with trailing backslash (backslash will be ignored) */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat \\", "What "); - - /* static display string with trailing text */ - TEST_PARAMS2(test_simple_richpresence, "Display:\nWhat\n\nWhere", "What"); - - /* buffer boundary */ - test_buffer_boundary(); - - /* condition display */ - TEST(test_conditional_display_simple); - TEST(test_conditional_display_after_default); - TEST(test_conditional_display_no_default); - TEST(test_conditional_display_common_condition); - TEST(test_conditional_display_duplicated_condition); - TEST(test_conditional_display_invalid_condition_logic); - TEST(test_conditional_display_shared_lookup); - TEST(test_conditional_display_whitespace_text); - TEST(test_conditional_display_indirect); - TEST(test_conditional_display_unnecessary_measured); - TEST(test_conditional_display_unnecessary_measured_indirect); - TEST(test_conditional_display_invalid); - - /* value macros */ - TEST(test_macro_value); - TEST(test_macro_value_nibble); - TEST(test_macro_value_bcd); - TEST(test_macro_value_bitcount); - TEST(test_macro_value_adjusted_negative); - TEST(test_macro_value_from_formula); - TEST(test_macro_value_from_hits); - TEST(test_macro_value_from_indirect); - TEST(test_macro_value_divide_by_zero); - TEST(test_macro_value_divide_by_self); - TEST(test_macro_value_remember_recall); - TEST(test_macro_value_invalid); - TEST(test_macro_value_measured_if); - TEST(test_multiple_macros); - - /* hundreds macro */ - TEST(test_macro_hundreds); - - /* frames macros */ - TEST(test_macro_frames); - - /* float macros */ - TEST_PARAMS3(test_macro_float, "VALUE", 0x429A4492, "77"); /* 77.133926 */ - TEST_PARAMS3(test_macro_float, "FLOAT1", 0x429A4492, "77.1"); - TEST_PARAMS3(test_macro_float, "FLOAT2", 0x429A4492, "77.13"); - TEST_PARAMS3(test_macro_float, "FLOAT3", 0x429A4492, "77.134"); /* rounded up */ - TEST_PARAMS3(test_macro_float, "FLOAT4", 0x429A4492, "77.1339"); - TEST_PARAMS3(test_macro_float, "FLOAT5", 0x429A4492, "77.13393"); /* rounded up */ - TEST_PARAMS3(test_macro_float, "FLOAT6", 0x429A4492, "77.133926"); - TEST_PARAMS3(test_macro_float, "VALUE", 0xC0000000, "-2"); /* -2.0 */ - TEST_PARAMS3(test_macro_float, "FLOAT1", 0xC0000000, "-2.0"); - TEST_PARAMS3(test_macro_float, "FLOAT6", 0xC0000000, "-2.000000"); - TEST_PARAMS3(test_macro_float, "SECS", 0x429A4492, "1:17"); /* 77.133926 */ - - /* lookup macros */ - TEST(test_macro_lookup_simple); - TEST(test_macro_lookup_with_inline_comment); - TEST(test_macro_lookup_hex_keys); - TEST(test_macro_lookup_default); - TEST(test_macro_lookup_crlf); - TEST(test_macro_lookup_after_display); - TEST(test_macro_lookup_from_formula); - TEST(test_macro_lookup_from_indirect); - TEST(test_macro_lookup_repeated); - TEST(test_macro_lookup_shared); - TEST(test_macro_lookup_multiple); - TEST(test_macro_lookup_and_value); - TEST(test_macro_lookup_negative_value); - TEST(test_macro_lookup_value_with_whitespace); - TEST(test_macro_lookup_mapping_repeated); - TEST(test_macro_lookup_mapping_repeated_csv); - TEST(test_macro_lookup_mapping_merged); - TEST(test_macro_lookup_mapping_range); - TEST(test_macro_lookup_mapping_range_overlap); - TEST(test_macro_lookup_invalid); - - /* escaped macro */ - TEST(test_macro_escaped); - - /* macro errors */ - TEST(test_macro_undefined); - TEST(test_macro_undefined_at_end_of_line); - TEST(test_macro_unterminated); - TEST(test_macro_without_parameter); - TEST(test_macro_without_parameter_conditional_display); - TEST(test_macro_non_numeric_parameter); - TEST(test_macro_mathematic_chain); - - /* builtin macros */ - TEST_PARAMS2(test_builtin_macro, "Number", "12,345"); - TEST_PARAMS2(test_builtin_macro, "Score", "012345"); - TEST_PARAMS2(test_builtin_macro, "Centiseconds", "2:03.45"); - TEST_PARAMS2(test_builtin_macro, "Seconds", "3h25:45"); - TEST_PARAMS2(test_builtin_macro, "Minutes", "205h45"); - TEST_PARAMS2(test_builtin_macro, "SecondsAsMinutes", "3h25"); - TEST_PARAMS2(test_builtin_macro, "ASCIIChar", "?"); /* 0x3039 is not a single ASCII char */ - TEST_PARAMS2(test_builtin_macro, "UnicodeChar", "\xe3\x80\xb9"); - TEST_PARAMS2(test_builtin_macro_float, "Float1", "77.1"); - TEST_PARAMS2(test_builtin_macro_float, "Float2", "77.13"); - TEST_PARAMS2(test_builtin_macro_float, "Float3", "77.134"); - TEST_PARAMS2(test_builtin_macro_float, "Float4", "77.1339"); - TEST_PARAMS2(test_builtin_macro_float, "Float5", "77.13393"); - TEST_PARAMS2(test_builtin_macro_float, "Float6", "77.133926"); - TEST_PARAMS2(test_builtin_macro, "Fixed1", "1,234.5"); - TEST_PARAMS2(test_builtin_macro, "Fixed2", "123.45"); - TEST_PARAMS2(test_builtin_macro, "Fixed3", "12.345"); - TEST_PARAMS2(test_builtin_macro, "Unsigned", "12,345"); - TEST_PARAMS2(test_builtin_macro, "Unformatted", "12345"); - TEST(test_builtin_macro_unsigned_large); - TEST(test_builtin_macro_override); - TEST(test_unformatted_legacy); - - /* asciichar */ - TEST(test_asciichar); - TEST_PARAMS9(test_ascii8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ""); - TEST_PARAMS9(test_ascii8, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, "ABCDEFGH"); - TEST_PARAMS9(test_ascii8, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, "Test"); - TEST_PARAMS9(test_ascii8, 0x54, 0x65, 0x73, 0x74, 0x00, 0x46, 0x6F, 0x6F, "Test"); - TEST_PARAMS9(test_ascii8, 0x00, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, ""); - TEST_PARAMS9(test_ascii8, 0x31, 0x7E, 0x32, 0x7F, 0x33, 0x20, 0x34, 0x5E, "1~2?3 4^"); /* 7F out of range */ - TEST_PARAMS9(test_ascii8, 0x54, 0x61, 0x62, 0x09, 0x31, 0x0D, 0x0E, 0x00, "Tab?1??"); /* control characters */ - - /* unicodechar */ - TEST_PARAMS5(test_unicode4, 0x0000, 0x0000, 0x0000, 0x0000, ""); - TEST_PARAMS5(test_unicode4, 0x0054, 0x0065, 0x0073, 0x0074, "Test"); - TEST_PARAMS5(test_unicode4, 0x0000, 0x0065, 0x0073, 0x0074, ""); - TEST_PARAMS5(test_unicode4, 0x00A9, 0x0031, 0x0032, 0x0033, "\xc2\xa9\x31\x32\x33"); /* two-byte unicode char */ - TEST_PARAMS5(test_unicode4, 0x2260, 0x0020, 0x0040, 0x0040, "\xe2\x89\xa0 @@"); /* three-byte unicode char */ - TEST_PARAMS5(test_unicode4, 0xD83D, 0xDEB6, 0x005F, 0x007A, "\xef\xbf\xbd\xef\xbf\xbd_z"); /* four-byte unicode pair */ - TEST_PARAMS5(test_unicode4, 0x0009, 0x003E, 0x000D, 0x000A, "\xef\xbf\xbd>\xef\xbf\xbd\xef\xbf\xbd"); /* control characters */ - - /* comments */ - TEST(test_random_text_between_sections); /* before official comments extra text was ignored, so was occassionally used to comment */ - TEST(test_comments); - TEST(test_comments_between_lines); - TEST(test_display_string_comment_only); - TEST(test_display_string_comment_with_blank_line); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_runtime.c b/src/rcheevos/test/rcheevos/test_runtime.c deleted file mode 100644 index e5edb8f5c0..0000000000 --- a/src/rcheevos/test/rcheevos/test_runtime.c +++ /dev/null @@ -1,1667 +0,0 @@ -#include "rc_runtime.h" -#include "rc_internal.h" - -#include "mock_memory.h" - -#include "../test_framework.h" - -static rc_runtime_event_t events[16]; -static int event_count = 0; - -static void event_handler(const rc_runtime_event_t* e) -{ - memcpy(&events[event_count++], e, sizeof(rc_runtime_event_t)); -} - -static void _assert_event(uint8_t type, uint32_t id, int32_t value) -{ - int i; - - for (i = 0; i < event_count; ++i) { - if (events[i].id == id && events[i].type == type && events[i].value == value) - return; - } - - ASSERT_FAIL("expected event not found"); -} -#define assert_event(type, id, value) ASSERT_HELPER(_assert_event(type, id, value), "assert_event") - -static void _assert_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr) -{ - int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") - -static void _assert_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr) -{ - int result = rc_runtime_activate_lboard(runtime, id, memaddr, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_lboard(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_lboard(runtime, id, memaddr), "assert_activate_lboard") - -static void _assert_activate_richpresence(rc_runtime_t* runtime, const char* script) -{ - int result = rc_runtime_activate_richpresence(runtime, script, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_richpresence(runtime, script) ASSERT_HELPER(_assert_activate_richpresence(runtime, script), "assert_activate_richpresence") - -static void assert_do_frame(rc_runtime_t* runtime, memory_t* memory) -{ - event_count = 0; - rc_runtime_do_frame(runtime, event_handler, peek, memory, NULL); -} - -static void _assert_richpresence_display_string(rc_runtime_t* runtime, memory_t* memory, const char* expected) -{ - char buffer[512]; - const int expected_len = (int)strlen(expected); - const int result = rc_runtime_get_richpresence(runtime, buffer, sizeof(buffer), peek, memory, NULL); - ASSERT_STR_EQUALS(buffer, expected); - ASSERT_NUM_EQUALS(result, expected_len); -} -#define assert_richpresence_display_string(runtime, memory, expected) ASSERT_HELPER(_assert_richpresence_display_string(runtime, memory, expected), "assert_richpresence_display_string") - -static void test_two_achievements_activate_and_trigger(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 2, "0xH0002=10"); - - /* both achievements are true, should remain in waiting state */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both achievements are false, should activate */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); - - /* second achievement is true, should trigger */ - ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); - - /* first achievement is true, should trigger. second is already triggered */ - ram[1] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* reset second achievement, should go back to WAITING and stay there */ - rc_reset_trigger(runtime.triggers[1].trigger); - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both achievements are false again. second should active, first should be ignored */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_deactivate_achievements(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 2, "0xH0002=10"); - - /* both achievements are true, should remain in waiting state */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* deactivate the first. */ - rc_runtime_deactivate_achievement(&runtime, 1); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* both achievements are false, deactivated one should not activate */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); - - /* both achievements are true, deactivated one should not trigger */ - ram[1] = ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); - - /* reactivate achievement. */ - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - ASSERT_NUM_EQUALS(runtime.trigger_count, 2); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* reactivated achievement is waiting and should not trigger */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both achievements are false. first should activate, second should be ignored */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_achievement_measured(void) -{ - /* bytes 3-7 are the float value for 16*pi */ - uint8_t ram[] = { 0, 10, 10, 0xDB, 0x0F, 0x49, 0x41 }; - char buffer[32]; - memory_t memory; - rc_runtime_t runtime; - uint32_t value, target; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* use equality so we can test values greater than the target */ - assert_activate_achievement(&runtime, 1, "0xH0002==10"); - assert_activate_achievement(&runtime, 2, "M:0xH0002==10"); - assert_activate_achievement(&runtime, 3, "G:0xH0002==10"); - assert_activate_achievement(&runtime, 4, "M:fF0003==f12.56637"); - - /* achievements are true, should remain in waiting state with no measured value */ - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 10); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "0/10"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 10); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "0%"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 12); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "0/12"); - ASSERT_FALSE(rc_runtime_get_achievement_measured(&runtime, 5, &value, &target)); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 5, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - - /* achievements are false, should activate */ - ram[2] = 9; - ram[6] = 0x40; - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 9); - ASSERT_NUM_EQUALS(target, 10); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "9/10"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 9); - ASSERT_NUM_EQUALS(target, 10); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "90%"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); - ASSERT_NUM_EQUALS(value, 3); - ASSERT_NUM_EQUALS(target, 12); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "3/12"); /* captured measured value and target are integers, so 3.14/12.56 -> 3/12 */ - - /* value greater than target (i.e. "6 >= 5" should report maximum "5/5" or "100%" */ - ram[2] = 12; - ram[6] = 0x42; - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 12); - ASSERT_NUM_EQUALS(target, 10); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "10/10"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 12); - ASSERT_NUM_EQUALS(target, 10); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "100%"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); - ASSERT_NUM_EQUALS(value, 50); - ASSERT_NUM_EQUALS(target, 12); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "12/12"); - - /* achievements are true, should trigger - triggered achievement is not measurable */ - ram[2] = 10; - ram[6] = 0x41; - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 1, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 1, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 4, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 4, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - - rc_runtime_destroy(&runtime); -} - -static void test_achievement_measured_maxint(void) -{ - uint8_t ram[] = { 0xFF, 0xFF, 0xFF, 0xFF }; - char buffer[32]; - memory_t memory; - rc_runtime_t runtime; - uint32_t value, target; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 2, "M:0xX0000==hFFFFFFFF"); - assert_activate_achievement(&runtime, 3, "G:0xX0000==hFFFFFFFF"); - - /* achievements are true, should remain in waiting state */ - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "0/4294967295"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "0%"); - - /* achievements are false (value fits in 31-bits), should activate */ - ram[1] = ram[3] = 0x7F; - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 0x7FFF7FFF); - ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "2147450879/4294967295"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 0x7FFF7FFF); - ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "49%"); - - /* achievements are false (value requires 32-bits) */ - ram[1] = ram[3] = 0xFE; - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 0xFEFFFEFF); - ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "4278189823/4294967295"); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 0xFEFFFEFF); - ASSERT_NUM_EQUALS(target, 0xFFFFFFFF); - ASSERT_TRUE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, "99%"); - - /* achievements are true, should trigger - triggered achievement is not measurable */ - ram[1] = ram[3] = 0xFF; - assert_do_frame(&runtime, &memory); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 2, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 2, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - ASSERT_TRUE(rc_runtime_get_achievement_measured(&runtime, 3, &value, &target)); - ASSERT_NUM_EQUALS(value, 0); - ASSERT_NUM_EQUALS(target, 0); - ASSERT_FALSE(rc_runtime_format_achievement_measured(&runtime, 3, buffer, sizeof(buffer))); - ASSERT_STR_EQUALS(buffer, ""); - - rc_runtime_destroy(&runtime); -} - -static void test_two_achievements_differing_resets_in_alts(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10S1=1SR:0xH0000!=0"); - assert_activate_achievement(&runtime, 2, "0xH0001=10S1=1SR:0xH0000!=1"); - - /* first achievement true (stays waiting), second not true because of reset */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); - - /* both achievements are false, should activate */ - ram[1] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* first should fire, second prevented by reset */ - ram[1] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* second can fire, reset first which will be activated due to reset */ - ram[0] = 1; - rc_reset_trigger(runtime.triggers[0].trigger); - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); - - /* both achievements are false again. second should active, first should be ignored */ - ram[0] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_shared_memref(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - rc_memref_t* memref1; - rc_memref_t* memref2; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 2, "0xH0001=12"); - - memref1 = runtime.triggers[0].trigger->requirement->conditions->operand1.value.memref; - memref2 = runtime.triggers[1].trigger->requirement->conditions->operand1.value.memref; - ASSERT_PTR_EQUALS(memref1, memref2); - - /* first is true, should remain waiting. second should activate */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); - - /* deactivate second one. it doesn't have any unique memrefs, so can be free'd */ - rc_runtime_deactivate_achievement(&runtime, 2); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_PTR_NOT_NULL(runtime.triggers[0].trigger); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* second is true, but no longer in runtime. first should activate, expect nothing from second */ - ram[1] = 12; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* first is true and should trigger */ - ram[1] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* reactivate achievement. old definition was free'd so should be recreated */ - assert_activate_achievement(&runtime, 2, "0xH0001=12"); - ASSERT_NUM_EQUALS(runtime.trigger_count, 2); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* reactivated achievement is waiting and false. should activate */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 2, 0); - - /* deactivate first achievement. */ - rc_runtime_deactivate_achievement(&runtime, 1); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - - /* second achievement is true. should trigger using memrefs from first */ - ram[1] = 12; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_replace_active_trigger(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 1, "0xH0002=10"); - - /* both are true, but first should have been overwritten by second */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both are false. only second should be getting processed, expect single event */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* first is true, but should not trigger */ - ram[1] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second is true and should trigger */ - ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* switch back to original definition. */ - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - - rc_runtime_destroy(&runtime); -} - -static rc_runtime_t* discarding_event_handler_runtime = NULL; -static void discarding_event_handler(const rc_runtime_event_t* e) -{ - event_handler(e); - rc_runtime_deactivate_achievement(discarding_event_handler_runtime, e->id); -} - -static void test_trigger_deactivation(void) -{ - uint8_t ram[] = { 0, 9, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* three identical achievements that should trigger when $1 changes from 9 to 10 */ - assert_activate_achievement(&runtime, 1, "0xH0001=10_d0xH0001=9"); - assert_activate_achievement(&runtime, 2, "0xH0001=10_d0xH0001=9"); - assert_activate_achievement(&runtime, 3, "0xH0001=10_d0xH0001=9"); - - /* prep the delta and make sure the achievements are active */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.trigger_count, 3); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_ACTIVE); - - /* trigger all three */ - ram[1] = 10; - event_count = 0; - discarding_event_handler_runtime = &runtime; - rc_runtime_do_frame(&runtime, discarding_event_handler, peek, &memory, NULL); - discarding_event_handler_runtime = NULL; - - ASSERT_NUM_EQUALS(event_count, 3); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 2, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 3, 0); - - /* triggers are no longer active and should have been removed from the runtime */ - ASSERT_NUM_EQUALS(runtime.trigger_count, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_trigger_with_resetif() { - uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* never(byte(3)==1) && once(byte(4)==1) && trigger_when(byte(0)==1) */ - assert_activate_achievement(&runtime, 1, "R:0xH0003=1_0xH0004=1.1._T:0xH0000=1"); - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* non-trigger condition is true */ - ram[4] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - /* ResetIf is true */ - ram[3] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); - - /* ResetIf no longer true */ - ram[3] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_trigger_with_resetnextif() { - uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* once(byte(4)==1 && never(repeated(2, byte(3)==1 && never(byte(1)==1 || byte(2)==1))) && trigger_when(byte(0)==1) */ - assert_activate_achievement(&runtime, 1, "O:0xH0001=1_Z:0xH0002=1_Z:0xH0003=1.2._0xH0004=1.1._T:0xH0000=1"); - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.trigger_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* non-trigger condition is true */ - ram[4] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - /* second ResetNextIf is true */ - ram[3] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(event_count, 0); - - /* OrNext resets second ResetNextIf */ - ram[1] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); - - /* OrNext no longer true */ - ram[1] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second ResetNextIf fires */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_reset_event(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - rc_condition_t* cond; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10.2._R:0xH0002=10"); - cond = runtime.triggers[0].trigger->requirement->conditions; - - /* reset is true, so achievement is false and should activate, but not notify reset */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - ASSERT_NUM_EQUALS(cond->current_hits, 0); - - /* reset is still true, but since no hits were accumulated there shouldn't be a reset event */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* reset is not true, hits should increment */ - ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(cond->current_hits, 1); - - /* reset is true. hits will reset. expect event */ - ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); - ASSERT_NUM_EQUALS(cond->current_hits, 0); - - /* reset is still true, but since hits were previously reset there shouldn't be a reset event */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* reset is not true, hits should increment */ - ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(cond->current_hits, 1); - - /* reset is not true, hits should increment, causing achievement to trigger */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - ASSERT_NUM_EQUALS(cond->current_hits, 2); - - /* reset is true, but hits shouldn't reset as achievement is no longer active */ - ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(cond->current_hits, 2); - - rc_runtime_destroy(&runtime); -} - -static void test_paused_event(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=10.2._P:0xH0002=10"); - - /* pause is true, so achievement is false and should activate, but only notify pause */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); - - /* pause is still true, but previously paused, so no event */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* pause is not true, expect activate event */ - ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* pause is true. expect event */ - ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); - - /* pause is still true, but previously paused, so no event */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* pause is not true, expect trigger*/ - ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* pause is true, but shouldn't notify as achievement is no longer active */ - ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_primed_event(void) -{ - uint8_t ram[] = { 0, 1, 0, 1, 0, 0 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* byte(0)==1 && trigger(byte(1)==1) && byte(2)==1 && trigger(byte(3)==1) && unless(byte(4)==1) && never(byte(5) == 1) */ - assert_activate_achievement(&runtime, 1, "0xH0000=1_T:0xH0001=1_0xH0002=1_T:0xH0003=1_P:0xH0004=1_R:0xH0005=1"); - - /* trigger conditions are true, but nothing else */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, 1, 0); - - /* primed */ - ram[1] = ram[3] = 0; - ram[0] = ram[2] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - /* no longer primed */ - ram[0] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); - - /* primed */ - ram[0] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - /* paused */ - ram[4] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED, 1, 0); - - /* unpaused */ - ram[4] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - /* reset */ - ram[5] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_RESET, 1, 0); - - /* not reset */ - ram[5] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED, 1, 0); - - /* all conditions are true */ - ram[1] = ram[3] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, 1, 0); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_progress_event(void) -{ - uint8_t ram[] = { 0, 1 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* measured(byte(0x0001) >= 10) */ - assert_activate_achievement(&runtime, 1, "M:0xH0001>=10"); - runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* should not receive notification when initialized first measured value */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* unchanged */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* increased */ - ram[1] = 2; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 2); - - /* unchanged */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* decreased */ - ram[1] = 0; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 0); - - /* increased */ - ram[1] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 9); - - /* triggered. should not receive change event */ - ram[1] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* no longer active */ - ram[1] = 11; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_progress_event_as_percent(void) -{ - uint8_t ram[] = { 0, 1 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* measured(byte(0x0001) >= 200, format='percent') */ - assert_activate_achievement(&runtime, 1, "G:0xH0001>=200"); - runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* should not receive notification when initialized first measured value */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* unchanged */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* increased (0% -> 1%) */ - ram[1] = 2; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 1); - - /* unchanged */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* increased (1% -> 1%, no event) */ - ram[1] = 3; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - /* increased (1% -> 2%) */ - ram[1] = 4; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 2); - - /* decreased */ - ram[1] = 1; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 0); - - /* increased */ - ram[1] = 199; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED, 1, 99); - - /* triggered. should not receive change event */ - ram[1] = 200; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED, 1, 0); - - /* no longer active */ - ram[1] = 40; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(event_count, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_lboard(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0000"); - assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0000*2"); - - /* both start conditions are true, leaderboards will not be active */ - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both start conditions are false, leaderboards will activate */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both start conditions are true, leaderboards will start */ - ram[1] = ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 1, 2); - assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 2, 4); - - /* start condition no longer true, leaderboard should continue processing */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(event_count, 0); - - /* value changed */ - ram[0] = 3; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_LBOARD_UPDATED, 1, 3); - assert_event(RC_RUNTIME_EVENT_LBOARD_UPDATED, 2, 6); - - /* value changed; first leaderboard submit, second canceled - expect events for submit and cancel, none for update */ - ram[0] = 4; - ram[1] = 11; - ram[2] = 12; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_LBOARD_TRIGGERED, 1, 4); - assert_event(RC_RUNTIME_EVENT_LBOARD_CANCELED, 2, 0); - - /* both start conditions are true, leaderboards will not be active */ - ram[1] = ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_CANCELED); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both start conditions are false, leaderboards will re-activate */ - ram[1] = ram[2] = 9; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event_count, 0); - - /* both start conditions are true, leaderboards will start */ - ram[1] = ram[2] = 10; - assert_do_frame(&runtime, &memory); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(event_count, 2); - assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 1, 4); - assert_event(RC_RUNTIME_EVENT_LBOARD_STARTED, 2, 8); - - rc_runtime_destroy(&runtime); -} - -static void test_format_lboard_value(int format, int value, const char* expected) { - char buffer[64]; - int result; - - result = rc_runtime_format_lboard_value(buffer, sizeof(buffer), value, format); - ASSERT_STR_EQUALS(buffer, expected); - ASSERT_NUM_EQUALS(result, strlen(expected)); -} - -static void test_richpresence(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* initial value */ - assert_richpresence_display_string(&runtime, &memory, ""); - - /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\nScore is @Points(0x 0001) Points"); - assert_richpresence_display_string(&runtime, &memory, "Score is 0 Points"); - - /* first frame should update display string with correct memrfs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Score is 2,570 Points"); - - /* calling rc_runtime_get_richpresence without calling rc_runtime_do_frame should return the same string as memrefs aren't updated */ - ram[1] = 20; - assert_richpresence_display_string(&runtime, &memory, "Score is 2,570 Points"); - - /* call rc_runtime_do_frame to update memrefs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_starts_with_macro(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,570 Points"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_macro_only(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001)"); - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,570"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_conditional(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0000=2?@Points(0x 0001) points\nScore is @Points(0x 0001) Points"); - assert_richpresence_display_string(&runtime, &memory, "Score is 0 Points"); - - /* first frame should update display string with correct memrfs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,570 points"); - - /* update display string */ - ram[0] = 0; - ram[1] = 20; - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_conditional_with_hits(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0000=1.2.?Score is @Points(0x 0001) Points\n@Points(0x 0001) points"); - assert_richpresence_display_string(&runtime, &memory, "0 points"); - - /* first frame should update display string with correct memrfs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,570 points"); - - /* one hit is not enough to switch display strings, but the memref does get updated */ - ram[0] = 1; - ram[1] = 20; - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,580 points"); - - /* second hit is enough */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); - - /* no more hits are accumulated */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Score is 2,580 Points"); - - /* same test without intermediary evaluation of display string */ - rc_runtime_reset(&runtime); - ram[0] = 2; - ram[1] = 30; - assert_do_frame(&runtime, &memory); /* no hits */ - - ram[0] = 1; - assert_do_frame(&runtime, &memory); /* one hit */ - assert_do_frame(&runtime, &memory); /* two hits */ - assert_richpresence_display_string(&runtime, &memory, "Score is 2,590 Points"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_conditional_with_hits_after_match(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n?0xH0002=10?It's @Points(0x 0001)\n?0xH0000=1.2.?Score is @Points(0x 0001) Points\n@Points(0x 0001) points"); - assert_richpresence_display_string(&runtime, &memory, "0 points"); - - /* first frame should update display string with correct memrfs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "It's 2,570"); - - /* first condition is true, but one hit should still be tallied on the second conditional */ - ram[0] = 1; - ram[1] = 20; - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "It's 2,580"); - - /* first conditio no longer true, second condtion will get it's second hit, which is enough */ - ram[2] = 20; - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Score is 5,140 Points"); - - /* same test without intermediary evaluation of display string */ - rc_runtime_reset(&runtime); - ram[0] = 2; - ram[1] = 10; - ram[2] = 10; - assert_do_frame(&runtime, &memory); /* no hits */ - - ram[0] = 1; - ram[1] = 20; - assert_do_frame(&runtime, &memory); /* one hit */ - ram[2] = 20; - assert_do_frame(&runtime, &memory); /* two hits */ - assert_richpresence_display_string(&runtime, &memory, "Score is 5,140 Points"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_reload(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Points"); - assert_richpresence_display_string(&runtime, &memory, "0 Points"); - - /* first frame should update display string with correct memrfs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,570 Points"); - - /* reloading should generate display string with current memrefs */ - ram[1] = 20; - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(0x 0001) Bananas"); - assert_richpresence_display_string(&runtime, &memory, "2,570 Bananas"); - - /* first frame after reloading should update display string */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,580 Bananas"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_reload_addaddress(void) -{ - /* ram[1] must be non-zero */ - uint8_t ram[] = { 1, 10, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* loading generates a display string with uninitialized memrefs, which ensures a non-empty display string */ - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(I:0xH0000_M:0x 0001) Points"); - assert_richpresence_display_string(&runtime, &memory, "0 Points"); - - /* first frame should update display string with correct memrfs */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,570 Points"); - - /* reloading should generate display string with current memrefs */ - /* the entire AddAddress expression will be a single variable, which will have a current value. */ - ram[2] = 20; - assert_activate_richpresence(&runtime, - "Format:Points\nFormatType=VALUE\n\nDisplay:\n@Points(I:0xH0000_M:0x 0001) Bananas"); - assert_richpresence_display_string(&runtime, &memory, "2,570 Bananas"); - - /* first frame after reloading should update display string */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "2,580 Bananas"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_static(void) -{ - uint8_t ram[] = { 2, 10, 10 }; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_richpresence(&runtime, "Display:\nHello, world!"); - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Hello, world!"); - - /* first frame won't affect the display string */ - assert_do_frame(&runtime, &memory); - assert_richpresence_display_string(&runtime, &memory, "Hello, world!"); - - rc_runtime_destroy(&runtime); -} - -static void test_richpresence_addsource_chain(void) -{ - rc_runtime_t runtime; - rc_runtime_init(&runtime); - - /* large number of AddSources will exceed the runtime buffer of 32 memrefs and 16 modified memrefs */ - assert_activate_richpresence(&runtime, - "Display:\n" - "@Number(0xH0000_0xH0001_0xH0002_0xH0003_0xH0004_0xH0005_0xH0006_0xH0007_" - "0xH0010_0xH0011_0xH0012_0xH0013_0xH0014_0xH0015_0xH0016_0xH0017_" - "0xH0020_0xH0021_0xH0022_0xH0023_0xH0024_0xH0025_0xH0026_0xH0027_" - "0xH0030_0xH0031_0xH0032_0xH0033_0xH0034_0xH0035_0xH0036_0xH0037_" - "0xH0040_0xH0041_0xH0042_0xH0043)\n"); - - rc_runtime_destroy(&runtime); -} - -typedef struct { - memory_t memory; - rc_runtime_t* runtime; - uint32_t invalid_address; -} -memory_invalid_t; - -static uint32_t peek_invalid(uint32_t address, uint32_t num_bytes, void* ud) -{ - memory_invalid_t* memory = (memory_invalid_t*)ud; - if (memory->invalid_address != address) - return peek(address, num_bytes, &memory->memory); - - rc_runtime_invalidate_address(memory->runtime, address); - return 0; -} - -static void assert_do_frame_invalid(rc_runtime_t* runtime, memory_invalid_t* memory, uint32_t invalid_address) -{ - event_count = 0; - memory->runtime = runtime; - memory->invalid_address = invalid_address; - rc_runtime_do_frame(runtime, event_handler, peek_invalid, memory, NULL); -} - -static void test_invalidate_address(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_invalid_t memory; - rc_runtime_t runtime; - - memory.memory.ram = ram; - memory.memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - event_count = 0; - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 2, "0xH0002=10"); - - /* achievements should start in waiting state */ - assert_do_frame_invalid(&runtime, &memory, 0); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* nothing depends on address 3 */ - assert_do_frame_invalid(&runtime, &memory, 3); - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* second achievement depends on address 2 */ - assert_do_frame_invalid(&runtime, &memory, 2); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 2); - - /* second achievement already disabled, don't raise event again */ - assert_do_frame_invalid(&runtime, &memory, 2); - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); - - rc_runtime_destroy(&runtime); -} - -static void test_invalidate_address_no_memrefs(void) -{ - rc_runtime_t runtime; - rc_runtime_init(&runtime); - - /* simple test to ensure a null reference doesn't occur when no memrefs are present */ - rc_runtime_invalidate_address(&runtime, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_invalidate_address_shared_memref(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_invalid_t memory; - rc_runtime_t runtime; - - memory.memory.ram = ram; - memory.memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - event_count = 0; - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 2, "0xH0002=10"); - assert_activate_achievement(&runtime, 3, "0xH0001=10S0xH0002=10S0xH0003=10"); - - /* achievements should start in waiting state */ - assert_do_frame_invalid(&runtime, &memory, 0); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); - - /* second and third achievements depend on address 2 */ - assert_do_frame_invalid(&runtime, &memory, 2); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); - ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_DISABLED); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 2); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 3, 2); - - rc_runtime_destroy(&runtime); -} - -static void test_invalidate_address_leaderboard(void) -{ - uint8_t ram[] = { 0, 10, 10 }; - memory_invalid_t memory; - rc_runtime_t runtime; - - memory.memory.ram = ram; - memory.memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - event_count = 0; - - assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0001"); - assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0002*2"); - - /* leaderboards should start in waiting state */ - assert_do_frame_invalid(&runtime, &memory, 0); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second leaderboard depends on address 2 */ - assert_do_frame_invalid(&runtime, &memory, 2); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_DISABLED); - assert_event(RC_RUNTIME_EVENT_LBOARD_DISABLED, 2, 2); - - rc_runtime_destroy(&runtime); -} - -static int validate_address_handler(uint32_t address) -{ - return (address & 1) == 0; /* all even addresses are valid */ -} - -static void test_validate_addresses(void) -{ - rc_runtime_t runtime; - - rc_runtime_init(&runtime); - event_count = 0; - - assert_activate_achievement(&runtime, 1, "0xH0001=10"); - assert_activate_achievement(&runtime, 2, "0xH0003=10"); /* put two invalid memrefs next to each other */ - assert_activate_achievement(&runtime, 3, "0xH0002=10"); - assert_activate_achievement(&runtime, 4, "0xH0001=10"); /* shared reference to invalid memref */ - assert_activate_lboard(&runtime, 1, "STA:0xH0001=10::SUB:0xH0001=11::CAN:0xH0001=12::VAL:0xH0001"); - assert_activate_lboard(&runtime, 2, "STA:0xH0002=10::SUB:0xH0002=11::CAN:0xH0002=12::VAL:0xH0002*2"); - - /* everything should start in waiting state */ - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[3].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); - - /* validate_addresses should immediately disable the achievements and raise the event */ - rc_runtime_validate_addresses(&runtime, event_handler, validate_address_handler); - ASSERT_NUM_EQUALS(runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_DISABLED); - ASSERT_NUM_EQUALS(runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_DISABLED); - ASSERT_NUM_EQUALS(runtime.triggers[2].trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(runtime.triggers[3].trigger->state, RC_TRIGGER_STATE_DISABLED); - ASSERT_NUM_EQUALS(runtime.lboards[0].lboard->state, RC_LBOARD_STATE_DISABLED); - ASSERT_NUM_EQUALS(runtime.lboards[1].lboard->state, RC_LBOARD_STATE_WAITING); - - ASSERT_NUM_EQUALS(event_count, 4); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 1, 1); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 2, 3); - assert_event(RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, 4, 1); - assert_event(RC_RUNTIME_EVENT_LBOARD_DISABLED, 1, 1); - - rc_runtime_destroy(&runtime); -} - -void test_runtime(void) { - TEST_SUITE_BEGIN(); - - /* achievements */ - TEST(test_two_achievements_activate_and_trigger); - TEST(test_deactivate_achievements); - TEST(test_achievement_measured); - TEST(test_achievement_measured_maxint); - TEST(test_two_achievements_differing_resets_in_alts); - - TEST(test_shared_memref); - TEST(test_replace_active_trigger); - TEST(test_trigger_deactivation); - TEST(test_trigger_with_resetif); - TEST(test_trigger_with_resetnextif); - - /* achievement events */ - TEST(test_reset_event); - TEST(test_paused_event); - TEST(test_primed_event); - TEST(test_progress_event); - TEST(test_progress_event_as_percent); - - /* leaderboards */ - TEST(test_lboard); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, 12345, "12,345"); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, -12345, "-12,345"); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_VALUE, 0xFFFFFFFF, "-1"); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_SCORE, 12345, "012345"); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_SECONDS, 345, "5:45"); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_CENTISECS, 12345, "2:03.45"); - TEST_PARAMS3(test_format_lboard_value, RC_FORMAT_FRAMES, 12345, "3:25.75"); - - /* rich presence */ - TEST(test_richpresence); - TEST(test_richpresence_starts_with_macro); - TEST(test_richpresence_macro_only); - TEST(test_richpresence_conditional); - TEST(test_richpresence_conditional_with_hits); - TEST(test_richpresence_conditional_with_hits_after_match); - TEST(test_richpresence_reload); - TEST(test_richpresence_reload_addaddress); - TEST(test_richpresence_static); - TEST(test_richpresence_addsource_chain); - - /* invalidate address */ - TEST(test_invalidate_address); - TEST(test_invalidate_address_no_memrefs); - TEST(test_invalidate_address_shared_memref); - TEST(test_invalidate_address_leaderboard); - - TEST(test_validate_addresses); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_runtime_progress.c b/src/rcheevos/test/rcheevos/test_runtime_progress.c deleted file mode 100644 index 8303aa291c..0000000000 --- a/src/rcheevos/test/rcheevos/test_runtime_progress.c +++ /dev/null @@ -1,1821 +0,0 @@ -#include "rc_runtime.h" -#include "rc_internal.h" - -#include "../test_framework.h" -#include "../rhash/md5.h" -#include "mock_memory.h" - -static void _assert_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr) -{ - int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") - -static void _assert_activate_leaderboard(rc_runtime_t* runtime, uint32_t id, const char* script) -{ - int result = rc_runtime_activate_lboard(runtime, id, script, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_leaderboard(runtime, id, script) ASSERT_HELPER(_assert_activate_leaderboard(runtime, id, script), "assert_activate_leaderboard") - -static void _assert_activate_rich_presence(rc_runtime_t* runtime, const char* script) -{ - int result = rc_runtime_activate_richpresence(runtime, script, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_rich_presence(runtime, script) ASSERT_HELPER(_assert_activate_rich_presence(runtime, script), "assert_activate_rich_presence") - -static void _assert_richpresence_output(rc_runtime_t* runtime, memory_t* memory, const char* expected_display_string) { - char output[256]; - int result; - - result = rc_runtime_get_richpresence(runtime, output, sizeof(output), peek, memory, NULL); - ASSERT_STR_EQUALS(output, expected_display_string); - ASSERT_NUM_EQUALS(result, strlen(expected_display_string)); -} -#define assert_richpresence_output(runtime, memory, expected_display_string) ASSERT_HELPER(_assert_richpresence_output(runtime, memory, expected_display_string), "assert_richpresence_output") - -static void event_handler(const rc_runtime_event_t* e) -{ - (void)e; -} - -static void assert_do_frame(rc_runtime_t* runtime, memory_t* memory) -{ - rc_runtime_do_frame(runtime, event_handler, peek, memory, NULL); -} - -static void _assert_serialize(rc_runtime_t* runtime, uint8_t* buffer, size_t buffer_size) -{ - int result; - unsigned* overflow; - - uint32_t size = rc_runtime_progress_size(runtime, NULL); - ASSERT_NUM_LESS(size, buffer_size); - - overflow = (unsigned*)(buffer + size); - *overflow = 0xCDCDCDCD; - - result = rc_runtime_serialize_progress(buffer, runtime, NULL); - ASSERT_NUM_EQUALS(result, RC_OK); - - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } -} -#define assert_serialize(runtime, buffer, buffer_size) ASSERT_HELPER(_assert_serialize(runtime, buffer, buffer_size), "assert_serialize") - -static void _assert_deserialize(rc_runtime_t* runtime, uint8_t* buffer) -{ - int result = rc_runtime_deserialize_progress(runtime, buffer, NULL); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_deserialize(runtime, buffer) ASSERT_HELPER(_assert_deserialize(runtime, buffer), "assert_deserialize") - -static void _assert_sized_memref(rc_runtime_t* runtime, uint32_t address, uint8_t size, uint32_t value, uint32_t prev, uint32_t prior) -{ - rc_memref_list_t* memref_list = &runtime->memrefs->memrefs; - for (; memref_list; memref_list = memref_list->next) { - const rc_memref_t* memref = memref_list->items; - const rc_memref_t* memref_end = memref + memref_list->count; - for (; memref < memref_end; ++memref) { - if (memref->address == address && memref->value.size == size) { - ASSERT_NUM_EQUALS(memref->value.value, value); - ASSERT_NUM_EQUALS(memref->value.prior, prior); - - if (value == prior) { - ASSERT_NUM_EQUALS(memref->value.changed, 0); - } - else { - ASSERT_NUM_EQUALS(memref->value.changed, (prev == prior) ? 1 : 0); - } - - return; - } - } - } - - ASSERT_FAIL("could not find memref for address %u", address); -} -#define assert_sized_memref(runtime, address, size, value, prev, prior) ASSERT_HELPER(_assert_sized_memref(runtime, address, size, value, prev, prior), "assert_sized_memref") -#define assert_memref(runtime, address, value, prev, prior) ASSERT_HELPER(_assert_sized_memref(runtime, address, RC_MEMSIZE_8_BITS, value, prev, prior), "assert_memref") - -static rc_trigger_t* find_trigger(rc_runtime_t* runtime, uint32_t ach_id) -{ - uint32_t i; - for (i = 0; i < runtime->trigger_count; ++i) { - if (runtime->triggers[i].id == ach_id && runtime->triggers[i].trigger) - return runtime->triggers[i].trigger; - } - - ASSERT_MESSAGE("could not find trigger for achievement %u", ach_id); - return NULL; -} - -static rc_condition_t* find_trigger_cond(rc_trigger_t* trigger, uint32_t group_idx, uint32_t cond_idx) -{ - rc_condset_t* condset; - rc_condition_t* cond; - - if (!trigger) - return NULL; - - condset = trigger->requirement; - if (group_idx > 0) { - condset = trigger->alternative; - while (condset && --group_idx != 0) - condset = condset->next; - } - - if (!condset) - return NULL; - - cond = condset->conditions; - while (cond && cond_idx > 0) { - --cond_idx; - cond = cond->next; - } - - return cond; -} - -static void _assert_hitcount(rc_runtime_t* runtime, uint32_t ach_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) -{ - rc_trigger_t* trigger = find_trigger(runtime, ach_id); - rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); -} -#define assert_hitcount(runtime, ach_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_hitcount(runtime, ach_id, group_idx, cond_idx, expected_hits), "assert_hitcount") - -static void _assert_cond_memref(rc_runtime_t* runtime, uint32_t ach_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_value, uint32_t expected_prior, uint8_t expected_changed) -{ - rc_trigger_t* trigger = find_trigger(runtime, ach_id); - rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.value, expected_value); - ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.prior, expected_prior); - ASSERT_NUM_EQUALS(cond->operand1.value.memref->value.changed, expected_changed); -} -#define assert_cond_memref(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed) \ - ASSERT_HELPER(_assert_cond_memref(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed), "assert_cond_memref") - -static void _assert_cond_memref2(rc_runtime_t* runtime, uint32_t ach_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_value, uint32_t expected_prior, uint8_t expected_changed) -{ - rc_trigger_t* trigger = find_trigger(runtime, ach_id); - rc_condition_t* cond = find_trigger_cond(trigger, group_idx, cond_idx); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.value, expected_value); - ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.prior, expected_prior); - ASSERT_NUM_EQUALS(cond->operand2.value.memref->value.changed, expected_changed); -} -#define assert_cond_memref2(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed) \ - ASSERT_HELPER(_assert_cond_memref2(runtime, ach_id, group_idx, cond_idx, expected_value, expected_prior, expected_changed), "assert_cond_memref2") - -static void _assert_achievement_state(rc_runtime_t* runtime, uint32_t ach_id, uint8_t state) -{ - rc_trigger_t* trigger = find_trigger(runtime, ach_id); - ASSERT_PTR_NOT_NULL(trigger); - - ASSERT_NUM_EQUALS(trigger->state, state); -} -#define assert_achievement_state(runtime, ach_id, state) ASSERT_HELPER(_assert_achievement_state(runtime, ach_id, state), "assert_achievement_state") - -static rc_lboard_t* find_lboard(rc_runtime_t* runtime, uint32_t lboard_id) -{ - uint32_t i; - for (i = 0; i < runtime->lboard_count; ++i) { - if (runtime->lboards[i].id == lboard_id && runtime->lboards[i].lboard) - return runtime->lboards[i].lboard; - } - - ASSERT_MESSAGE("could not find leaderboard %u", lboard_id); - return NULL; -} - -static void _assert_sta_hitcount(rc_runtime_t* runtime, uint32_t lboard_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) -{ - rc_lboard_t* lboard = find_lboard(runtime, lboard_id); - rc_condition_t* cond = find_trigger_cond(&lboard->start, group_idx, cond_idx); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); -} -#define assert_sta_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_sta_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_sta_hitcount") - -static void _assert_sub_hitcount(rc_runtime_t* runtime, uint32_t lboard_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) -{ - rc_lboard_t* lboard = find_lboard(runtime, lboard_id); - rc_condition_t* cond = find_trigger_cond(&lboard->submit, group_idx, cond_idx); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); -} -#define assert_sub_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_sub_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_sub_hitcount") - -static void _assert_can_hitcount(rc_runtime_t* runtime, uint32_t lboard_id, uint32_t group_idx, uint32_t cond_idx, uint32_t expected_hits) -{ - rc_lboard_t* lboard = find_lboard(runtime, lboard_id); - rc_condition_t* cond = find_trigger_cond(&lboard->cancel, group_idx, cond_idx); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->current_hits, expected_hits); -} -#define assert_can_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits) ASSERT_HELPER(_assert_can_hitcount(runtime, lboard_id, group_idx, cond_idx, expected_hits), "assert_can_hitcount") - -static void update_md5(uint8_t* buffer) -{ - md5_state_t state; - - uint8_t* ptr = buffer; - while (ptr[0] != 'D' || ptr[1] != 'O' || ptr[2] != 'N' || ptr[3] != 'E') - ++ptr; - - ptr += 8; - - md5_init(&state); - md5_append(&state, buffer, (int)(ptr - buffer)); - md5_finish(&state, ptr); -} - -static void reset_runtime(rc_runtime_t* runtime) -{ - rc_memref_list_t* memref_list; - rc_memref_t* memref; - rc_trigger_t* trigger; - rc_condition_t* cond; - rc_condset_t* condset; - uint32_t i; - - for (memref_list = &runtime->memrefs->memrefs; memref_list; memref_list = memref_list->next) { - const rc_memref_t* memref_end; - memref = memref_list->items; - memref_end = memref + memref_list->count; - for (; memref < memref_end; ++memref) { - memref->value.value = 0xFF; - memref->value.changed = 0; - memref->value.prior = 0xFF; - } - } - - for (i = 0; i < runtime->trigger_count; ++i) { - trigger = runtime->triggers[i].trigger; - if (trigger) { - trigger->measured_value = 0xFF; - trigger->measured_target = 0xFF; - - if (trigger->requirement) { - cond = trigger->requirement->conditions; - while (cond) { - cond->current_hits = 0xFF; - cond = cond->next; - } - } - - condset = trigger->alternative; - while (condset) { - cond = condset->conditions; - while (cond) { - cond->current_hits = 0xFF; - cond = cond->next; - } - - condset = condset->next; - } - } - } -} - -static void test_empty() -{ - uint8_t buffer[2048]; - rc_runtime_t runtime; - - rc_runtime_init(&runtime); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - ASSERT_NUM_EQUALS(runtime.memrefs->memrefs.count, 0); - ASSERT_NUM_EQUALS(runtime.memrefs->modified_memrefs.count, 0); - ASSERT_NUM_EQUALS(runtime.trigger_count, 0); - ASSERT_NUM_EQUALS(runtime.lboard_count, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_single_achievement() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_invalid_marker() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* invalid header prevents anything from being deserialized */ - buffer[0] = 0x40; - update_md5(buffer); - - reset_runtime(&runtime); - ASSERT_NUM_EQUALS(rc_runtime_deserialize_progress(&runtime, buffer, NULL), RC_INVALID_STATE); - - assert_memref(&runtime, 1, 0xFF, 0xFF, 0xFF); - assert_memref(&runtime, 2, 0xFF, 0xFF, 0xFF); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_invalid_memref_chunk_id() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* invalid chunk is ignored, achievement hits will still be read */ - buffer[5] = 0x40; - update_md5(buffer); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 1, 0xFF, 0xFF, 0xFF); - assert_memref(&runtime, 2, 0xFF, 0xFF, 0xFF); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_modified_data() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* this changes the current hits for the test condition to 6, but doesn't update the checksum, so should be ignored */ - ASSERT_NUM_EQUALS(buffer[84], 3); - buffer[84] = 6; - - reset_runtime(&runtime); - ASSERT_NUM_EQUALS(rc_runtime_deserialize_progress(&runtime, buffer, NULL), RC_INVALID_STATE); - - /* memrefs will have been processed and cannot be "reset" */ - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - - /* deserialization failure causes all hits to be reset */ - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_single_achievement_deactivated() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - - /* disabled achievement */ - rc_runtime_deactivate_achievement(&runtime, 1); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - - /* reactivate */ - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_WAITING); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 0); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_single_achievement_md5_changed() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - - /* new achievement definition - rack up a couple hits */ - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5.1."); - ram[1] = 3; - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_hitcount(&runtime, 1, 0, 0, 2); - assert_hitcount(&runtime, 1, 0, 1, 0); - assert_memref(&runtime, 1, 4, 4, 3); - - assert_deserialize(&runtime, buffer); - - /* memrefs should be restored */ - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - - /* achievement definition changed, achievement should be reset */ - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void setup_multiple_achievements(rc_runtime_t* runtime, memory_t* memory) -{ - rc_runtime_init(runtime); - - assert_activate_achievement(runtime, 1, "0xH0001=4_0xH0000=1"); - assert_activate_achievement(runtime, 2, "0xH0002=7_0xH0000=2"); - assert_activate_achievement(runtime, 3, "0xH0003=9_0xH0000=3"); - assert_activate_achievement(runtime, 4, "0xH0004=1_0xH0000=4"); - assert_do_frame(runtime, memory); - memory->ram[1] = 4; - assert_do_frame(runtime, memory); - memory->ram[2] = 7; - assert_do_frame(runtime, memory); - memory->ram[3] = 9; - assert_do_frame(runtime, memory); - memory->ram[4] = 1; - assert_do_frame(runtime, memory); - - assert_memref(runtime, 0, 0, 0, 0); - assert_memref(runtime, 1, 4, 4, 1); - assert_memref(runtime, 2, 7, 7, 2); - assert_memref(runtime, 3, 9, 9, 3); - assert_memref(runtime, 4, 1, 4, 4); - assert_hitcount(runtime, 1, 0, 0, 4); - assert_hitcount(runtime, 2, 0, 0, 3); - assert_hitcount(runtime, 3, 0, 0, 2); - assert_hitcount(runtime, 4, 0, 0, 1); -} - -static void test_single_achievement_sized() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - uint32_t size; - int result; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - size = rc_runtime_progress_size(&runtime, NULL); - ASSERT_NUM_LESS(size, sizeof(buffer)); - - result = rc_runtime_serialize_progress_sized(buffer, size - 1, &runtime, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - result = rc_runtime_serialize_progress_sized(buffer, size, &runtime, NULL); - ASSERT_NUM_EQUALS(result, RC_OK); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size - 1, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - assert_memref(&runtime, 1, 5, 5, 4); /* memrefs don't get reset on failure */ - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 0); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 16, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 0, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - assert_memref(&runtime, 1, 5, 5, 4); /* memrefs don't get reset on failure */ - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 0); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size, NULL); - ASSERT_NUM_EQUALS(result, RC_OK); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_empty_sized() -{ - uint8_t buffer[2048]; - rc_runtime_t runtime; - uint32_t size; - int result; - - rc_runtime_init(&runtime); - - size = rc_runtime_progress_size(&runtime, NULL); - ASSERT_NUM_LESS(size, sizeof(buffer)); - - result = rc_runtime_serialize_progress_sized(buffer, size - 1, &runtime, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - result = rc_runtime_serialize_progress_sized(buffer, size, &runtime, NULL); - ASSERT_NUM_EQUALS(result, RC_OK); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size - 1, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 16, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, 0, NULL); - ASSERT_NUM_EQUALS(result, RC_INSUFFICIENT_BUFFER); - - result = rc_runtime_deserialize_progress_sized(&runtime, buffer, size, NULL); - ASSERT_NUM_EQUALS(result, RC_OK); - - rc_runtime_destroy(&runtime); -} - -static void test_no_core_group() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "S0xH0001=4_0xH0002=5"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 1, 0, 3); - assert_hitcount(&runtime, 1, 1, 1, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_hitcount(&runtime, 1, 1, 0, 3); - assert_hitcount(&runtime, 1, 1, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_memref_shared_address() -{ - uint8_t ram[] = { 2, 3, 0, 0, 0 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0x 0001=5_0xX0001=6"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 6; - assert_do_frame(&runtime, &memory); - - assert_sized_memref(&runtime, 1, RC_MEMSIZE_8_BITS, 6, 5, 5); - assert_sized_memref(&runtime, 1, RC_MEMSIZE_16_BITS, 6, 5, 5); - assert_sized_memref(&runtime, 1, RC_MEMSIZE_32_BITS, 6, 5, 5); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 2); - assert_hitcount(&runtime, 1, 0, 2, 1); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_sized_memref(&runtime, 1, RC_MEMSIZE_8_BITS, 6, 5, 5); - assert_sized_memref(&runtime, 1, RC_MEMSIZE_16_BITS, 6, 5, 5); - assert_sized_memref(&runtime, 1, RC_MEMSIZE_32_BITS, 6, 5, 5); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 1, 0, 1, 2); - assert_hitcount(&runtime, 1, 0, 2, 1); - - rc_runtime_destroy(&runtime); -} - -static void test_memref_addsource() { - uint8_t ram[] = { 1, 2, 3, 4, 5 }; - uint8_t buffer1[512]; - uint8_t buffer2[512]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* byte(2) + byte(1) == 5 - third condition just prevents the achievement from triggering*/ - assert_activate_achievement(&runtime, 1, "A:0xH0002_0xH0001=3_0xH0004=99"); - assert_do_frame(&runtime, &memory); /* $2 = 3, $1 = 2, 3 + 2 = 5 */ - ram[1] = 3; - ram[2] = 0; - assert_do_frame(&runtime, &memory); /* $2 = 0, $1 = 3, 0 + 3 = 3 */ - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 4); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - - assert_serialize(&runtime, buffer1, sizeof(buffer1)); - - assert_do_frame(&runtime, &memory); - ram[1] = 6; - ram[2] = 1; - assert_do_frame(&runtime, &memory); /* $2 = 1, $1 = 6, 1 + 6 = 7 */ - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 5); - assert_cond_memref(&runtime, 1, 0, 1, 7, 3, 1); - - assert_serialize(&runtime, buffer2, sizeof(buffer2)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer1); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 4); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer2); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 5); - assert_cond_memref(&runtime, 1, 0, 1, 7, 3, 1); - - rc_runtime_destroy(&runtime); -} - -static void test_memref_indirect() -{ - uint8_t ram[] = { 1, 2, 3, 4, 5 }; - uint8_t buffer1[512]; - uint8_t buffer2[512]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* byte(byte(2) + 1) == 5 - third condition just prevents the achievement from triggering*/ - assert_activate_achievement(&runtime, 1, "I:0xH0002_0xH0001=3_0xH0004=99"); - assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5 */ - ram[1] = 3; - ram[2] = 0; - assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3 */ - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 4); - assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - - assert_serialize(&runtime, buffer1, sizeof(buffer1)); - - assert_do_frame(&runtime, &memory); - ram[1] = 6; - ram[2] = 1; - assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1 */ - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 5); - assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); - assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); - - assert_serialize(&runtime, buffer2, sizeof(buffer2)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer1); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 4); - assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer2); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 5); - assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); - assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); - - rc_runtime_destroy(&runtime); -} - -static void test_memref_indirect_delta() -{ - uint8_t ram[] = { 1, 2, 3, 4, 5 }; - uint8_t buffer1[512]; - uint8_t buffer2[512]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* byte(byte(2) + 1) == 5 - third condition just prevents the achievement from triggering*/ - assert_activate_achievement(&runtime, 1, "I:0xH0002_d0xH0001=3_0xH0004=99"); - assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5, delta = 0 */ - ram[1] = 3; - ram[2] = 0; - assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, delta = 5 */ - assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, delta = 3 */ - - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 1); - assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - - assert_serialize(&runtime, buffer1, sizeof(buffer1)); - - assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, delta = 3 */ - ram[1] = 6; - ram[2] = 1; - assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, delta = 3 */ - assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, delta = 1 */ - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 3); - assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 0); - assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 0); - - assert_serialize(&runtime, buffer2, sizeof(buffer2)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer1); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 1); - assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer2); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 3); - assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 0); - assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_memref_double_indirect() -{ - uint8_t ram[] = { 1, 2, 3, 4, 5 }; - uint8_t buffer1[512]; - uint8_t buffer2[512]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - /* byte(byte(2) + 1) == byte(byte(2)) - third condition just prevents the achievement from triggering*/ - assert_activate_achievement(&runtime, 1, "I:0xH0002_0xH0001=0xH0000_0xH0004=99"); - assert_do_frame(&runtime, &memory); /* $2 = 3, $(3+1) = 5, $(3+0) = 4 */ - ram[0] = 3; - ram[1] = 3; - ram[2] = 0; - assert_do_frame(&runtime, &memory); /* $2 = 0, $(0+1) = 3, $(0+0) = 3 */ - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 4); - assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - assert_cond_memref2(&runtime, 1, 0, 1, 3, 4, 0); - - assert_serialize(&runtime, buffer1, sizeof(buffer1)); - - assert_do_frame(&runtime, &memory); - ram[1] = 6; - ram[2] = 1; - assert_do_frame(&runtime, &memory); /* $2 = 1, $(1+1) = 1, $(1+0) = 6 */ - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 5); - assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); - assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); - assert_cond_memref2(&runtime, 1, 0, 1, 6, 3, 1); - - assert_serialize(&runtime, buffer2, sizeof(buffer2)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer1); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 4); - assert_cond_memref(&runtime, 1, 0, 0, 0, 3, 0); - assert_cond_memref(&runtime, 1, 0, 1, 3, 5, 0); - assert_cond_memref2(&runtime, 1, 0, 1, 3, 4, 0); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer2); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 1, 0, 1, 5); - assert_cond_memref(&runtime, 1, 0, 0, 1, 0, 1); - assert_cond_memref(&runtime, 1, 0, 1, 1, 3, 1); - assert_cond_memref2(&runtime, 1, 0, 1, 6, 3, 1); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - setup_multiple_achievements(&runtime, &memory); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 0, 0, 0, 0); - assert_memref(&runtime, 1, 4, 4, 1); - assert_memref(&runtime, 2, 7, 7, 2); - assert_memref(&runtime, 3, 9, 9, 3); - assert_memref(&runtime, 4, 1, 4, 4); - assert_hitcount(&runtime, 1, 0, 0, 4); - assert_hitcount(&runtime, 1, 0, 1, 0); - assert_hitcount(&runtime, 2, 0, 0, 3); - assert_hitcount(&runtime, 2, 0, 1, 0); - assert_hitcount(&runtime, 3, 0, 0, 2); - assert_hitcount(&runtime, 3, 0, 1, 0); - assert_hitcount(&runtime, 4, 0, 0, 1); - assert_hitcount(&runtime, 4, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements_ignore_triggered_and_inactive() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - setup_multiple_achievements(&runtime, &memory); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* trigger achievement 3 */ - ram[0] = 3; - assert_do_frame(&runtime, &memory); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_TRIGGERED); - - /* reset achievement 2 to inactive */ - find_trigger(&runtime, 2)->state = RC_TRIGGER_STATE_INACTIVE; - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 0, 0, 0, 0); - assert_memref(&runtime, 1, 4, 4, 1); - assert_memref(&runtime, 2, 7, 7, 2); - assert_memref(&runtime, 3, 9, 9, 3); - assert_memref(&runtime, 4, 1, 4, 4); - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_INACTIVE); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_TRIGGERED); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); - assert_hitcount(&runtime, 1, 0, 0, 4); - assert_hitcount(&runtime, 1, 0, 1, 0); - assert_hitcount(&runtime, 2, 0, 0, 0xFF); /* inactive achievement should be ignored */ - assert_hitcount(&runtime, 2, 0, 1, 0xFF); - assert_hitcount(&runtime, 3, 0, 0, 0xFF); /* triggered achievement should be ignored */ - assert_hitcount(&runtime, 3, 0, 1, 0xFF); - assert_hitcount(&runtime, 4, 0, 0, 1); - assert_hitcount(&runtime, 4, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements_overwrite_waiting() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - setup_multiple_achievements(&runtime, &memory); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* reset achievement 2 to waiting */ - rc_reset_trigger(find_trigger(&runtime, 2)); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); - assert_hitcount(&runtime, 1, 0, 0, 4); - assert_hitcount(&runtime, 1, 0, 1, 0); - assert_hitcount(&runtime, 2, 0, 0, 3); /* waiting achievement should be set back to active */ - assert_hitcount(&runtime, 2, 0, 1, 0); - assert_hitcount(&runtime, 3, 0, 0, 2); - assert_hitcount(&runtime, 3, 0, 1, 0); - assert_hitcount(&runtime, 4, 0, 0, 1); - assert_hitcount(&runtime, 4, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements_reactivate_waiting() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - setup_multiple_achievements(&runtime, &memory); - - /* reset achievement 2 to waiting */ - rc_reset_trigger(find_trigger(&runtime, 2)); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* reactivate achievement 2 */ - assert_do_frame(&runtime, &memory); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); - assert_hitcount(&runtime, 1, 0, 0, 4); - assert_hitcount(&runtime, 1, 0, 1, 0); - assert_hitcount(&runtime, 2, 0, 0, 0); /* active achievement should be set back to waiting */ - assert_hitcount(&runtime, 2, 0, 1, 0); - assert_hitcount(&runtime, 3, 0, 0, 2); - assert_hitcount(&runtime, 3, 0, 1, 0); - assert_hitcount(&runtime, 4, 0, 0, 1); - assert_hitcount(&runtime, 4, 0, 1, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements_paused_and_primed() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - uint8_t buffer2[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); - assert_activate_achievement(&runtime, 2, "0xH0002=7_0xH0000=2_P:0xH0005=4"); - assert_activate_achievement(&runtime, 3, "0xH0003=9_0xH0000=3"); - assert_activate_achievement(&runtime, 4, "0xH0004=1_T:0xH0000=4"); - - assert_do_frame(&runtime, &memory); - ram[1] = 4; - ram[2] = 7; - ram[3] = 9; - ram[4] = 1; - ram[5] = 4; - assert_do_frame(&runtime, &memory); - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_PAUSED); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_PRIMED); - ASSERT_TRUE(find_trigger(&runtime, 2)->requirement->is_paused); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* unpause achievement 2 and unprime achievement 4 */ - ram[5] = 2; - ram[4] = 2; - assert_do_frame(&runtime, &memory); - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); - ASSERT_FALSE(find_trigger(&runtime, 2)->requirement->is_paused); - - assert_serialize(&runtime, buffer2, sizeof(buffer2)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_PAUSED); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_PRIMED); - ASSERT_TRUE(find_trigger(&runtime, 2)->requirement->is_paused); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer2); - - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 4, RC_TRIGGER_STATE_ACTIVE); - ASSERT_FALSE(find_trigger(&runtime, 2)->requirement->is_paused); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements_deactivated_memrefs() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); - assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); - assert_activate_achievement(&runtime, 3, "0xH0001=6_0xH0000=3"); - - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 6; - assert_do_frame(&runtime, &memory); - - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 2, 0, 0, 2); - assert_hitcount(&runtime, 3, 0, 0, 1); - - /* deactivate an achievement with memrefs */ - ASSERT_NUM_EQUALS(runtime.trigger_count, 3); - rc_runtime_deactivate_achievement(&runtime, 1); - ASSERT_NUM_EQUALS(runtime.trigger_count, 2); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* reactivate achievement 1 */ - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=2"); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_WAITING); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_hitcount(&runtime, 1, 0, 0, 0); - assert_hitcount(&runtime, 2, 0, 0, 2); - assert_hitcount(&runtime, 3, 0, 0, 1); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_achievements_deactivated_no_memrefs() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0001=4_0xH0000=1"); - assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); - assert_activate_achievement(&runtime, 3, "0xH0001=6_0xH0000=3"); - - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 6; - assert_do_frame(&runtime, &memory); - - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 2, 0, 0, 2); - assert_hitcount(&runtime, 3, 0, 0, 1); - - /* deactivate an achievement without memrefs - trigger should be removed */ - ASSERT_NUM_EQUALS(runtime.trigger_count, 3); - rc_runtime_deactivate_achievement(&runtime, 2); - ASSERT_NUM_EQUALS(runtime.trigger_count, 2); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - /* reactivate achievement 2 */ - assert_activate_achievement(&runtime, 2, "0xH0001=5_0xH0000=2"); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_achievement_state(&runtime, 1, RC_TRIGGER_STATE_ACTIVE); - assert_achievement_state(&runtime, 2, RC_TRIGGER_STATE_WAITING); - assert_achievement_state(&runtime, 3, RC_TRIGGER_STATE_ACTIVE); - assert_hitcount(&runtime, 1, 0, 0, 3); - assert_hitcount(&runtime, 2, 0, 0, 0); - assert_hitcount(&runtime, 3, 0, 0, 1); - - rc_runtime_destroy(&runtime); -} - -static void test_single_leaderboard() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_sta_hitcount(&runtime, 1, 0, 0, 3); - assert_sub_hitcount(&runtime, 1, 0, 0, 2); - assert_can_hitcount(&runtime, 1, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - assert_memref(&runtime, 1, 5, 5, 4); - assert_memref(&runtime, 2, 6, 6, 0); - assert_sta_hitcount(&runtime, 1, 0, 0, 3); - assert_sub_hitcount(&runtime, 1, 0, 0, 2); - assert_can_hitcount(&runtime, 1, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_leaderboards() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); - assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_sta_hitcount(&runtime, 1, 0, 0, 3); - assert_sub_hitcount(&runtime, 1, 0, 0, 2); - assert_sta_hitcount(&runtime, 2, 0, 0, 2); - assert_sub_hitcount(&runtime, 2, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_deserialize(&runtime, buffer); - - assert_sta_hitcount(&runtime, 1, 0, 0, 3); - assert_sub_hitcount(&runtime, 1, 0, 0, 2); - assert_sta_hitcount(&runtime, 2, 0, 0, 2); - assert_sub_hitcount(&runtime, 2, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_leaderboards_ignore_inactive() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); - assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - find_lboard(&runtime, 1)->state = RC_LBOARD_STATE_DISABLED; - assert_sta_hitcount(&runtime, 1, 0, 0, 3); - assert_sub_hitcount(&runtime, 1, 0, 0, 2); - assert_sta_hitcount(&runtime, 2, 0, 0, 2); - assert_sub_hitcount(&runtime, 2, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_DISABLED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - find_lboard(&runtime, 1)->state = RC_LBOARD_STATE_ACTIVE; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_deserialize(&runtime, buffer); - - /* non-serialized leaderboard should be reset */ - assert_sta_hitcount(&runtime, 1, 0, 0, 0); - assert_sub_hitcount(&runtime, 1, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - - /* serialized leaderboard should be restored */ - assert_sta_hitcount(&runtime, 2, 0, 0, 2); - assert_sub_hitcount(&runtime, 2, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_multiple_leaderboards_ignore_modified() -{ - uint8_t ram[] = { 2, 3, 6 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - - rc_runtime_init(&runtime); - - assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.4.::VAL:0xH0002"); - assert_activate_leaderboard(&runtime, 2, "STA:0xH0001=5::SUB:0xH0002=5::CAN:0xH0001=0::VAL:0xH0000"); - assert_do_frame(&runtime, &memory); - ram[1] = 4; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - ram[1] = 5; - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - - assert_sta_hitcount(&runtime, 1, 0, 0, 3); - assert_sub_hitcount(&runtime, 1, 0, 0, 2); - assert_sta_hitcount(&runtime, 2, 0, 0, 2); - assert_sub_hitcount(&runtime, 2, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 6); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - assert_activate_leaderboard(&runtime, 1, "STA:0xH0001=4::SUB:0xH0001=5.4.::CAN:0xH0001=0.3.::VAL:0xH0002"); - assert_do_frame(&runtime, &memory); - assert_do_frame(&runtime, &memory); - assert_deserialize(&runtime, buffer); - - /* modified leaderboard should be reset */ - assert_sta_hitcount(&runtime, 1, 0, 0, 0); - assert_sub_hitcount(&runtime, 1, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->state, RC_LBOARD_STATE_WAITING); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.value, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 1)->value.value.prior, 0); - - /* serialized leaderboard should be restored */ - assert_sta_hitcount(&runtime, 2, 0, 0, 2); - assert_sub_hitcount(&runtime, 2, 0, 0, 0); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->state, RC_LBOARD_STATE_STARTED); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.value, 2); - ASSERT_NUM_EQUALS(find_lboard(&runtime, 2)->value.value.prior, 0); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_none() -{ - uint8_t buffer[2048]; - rc_runtime_t runtime; - rc_runtime_init(&runtime); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_static() -{ - uint8_t buffer[2048]; - rc_runtime_t runtime; - rc_runtime_init(&runtime); - - assert_activate_rich_presence(&runtime, "Display:\nTest"); - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_simple_lookup() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_rich_presence(&runtime, "Display:\n@Number(p0xH02)"); - assert_do_frame(&runtime, &memory); /* prev[2] = 0 */ - - ram[2] = 4; - assert_do_frame(&runtime, &memory); /* prev[2] = 2 */ - - ram[2] = 8; - assert_do_frame(&runtime, &memory); /* prev[2] = 4 */ - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - ram[2] = 12; - assert_do_frame(&runtime, &memory); /* prev[2] = 8 */ - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - /* deserialized should remember prev[2] = 4 */ - assert_richpresence_output(&runtime, &memory, "4"); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_tracked_hits() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)"); - assert_do_frame(&runtime, &memory); /* count = 1 */ - assert_do_frame(&runtime, &memory); /* count = 2 */ - assert_do_frame(&runtime, &memory); /* count = 3 */ - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - assert_do_frame(&runtime, &memory); /* count = 4 */ - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - /* deserialized should remember count = 3 */ - assert_richpresence_output(&runtime, &memory, "3"); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_tracked_hits_md5_changed() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)"); - assert_do_frame(&runtime, &memory); /* count = 1 */ - assert_do_frame(&runtime, &memory); /* count = 2 */ - assert_do_frame(&runtime, &memory); /* count = 3 */ - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - assert_do_frame(&runtime, &memory); /* count = 4 */ - - assert_activate_rich_presence(&runtime, "Display:\n@Number(M:0xH02=2)!"); - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - /* md5 changed, but variable is stored external to RP, 3 should be remembered */ - assert_richpresence_output(&runtime, &memory, "3!"); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_conditional_display() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three\nLess"); - assert_do_frame(&runtime, &memory); /* count = 1 */ - assert_do_frame(&runtime, &memory); /* count = 2 */ - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - assert_do_frame(&runtime, &memory); /* count = 3 */ - assert_do_frame(&runtime, &memory); /* count = 4 */ - assert_richpresence_output(&runtime, &memory, "Three"); - - reset_runtime(&runtime); - assert_deserialize(&runtime, buffer); - - /* deserialized should remember count = 2 */ - assert_richpresence_output(&runtime, &memory, "Less"); - - rc_runtime_destroy(&runtime); -} - -static void test_rich_presence_conditional_display_md5_changed() -{ - uint8_t ram[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; - uint8_t buffer[2048]; - memory_t memory; - rc_runtime_t runtime; - - memory.ram = ram; - memory.size = sizeof(ram); - rc_runtime_init(&runtime); - - assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three\nLess"); - assert_do_frame(&runtime, &memory); /* count = 1 */ - assert_do_frame(&runtime, &memory); /* count = 2 */ - - assert_serialize(&runtime, buffer, sizeof(buffer)); - - assert_do_frame(&runtime, &memory); /* count = 3 */ - assert_do_frame(&runtime, &memory); /* count = 4 */ - assert_richpresence_output(&runtime, &memory, "Three"); - - reset_runtime(&runtime); - assert_activate_rich_presence(&runtime, "Display:\n?0xH02=2.3.?Three!\nLess"); - assert_deserialize(&runtime, buffer); - - /* md5 changed, hit count should be discarded */ - assert_richpresence_output(&runtime, &memory, "Less"); - - assert_do_frame(&runtime, &memory); /* count = 1 */ - assert_do_frame(&runtime, &memory); /* count = 2 */ - assert_richpresence_output(&runtime, &memory, "Less"); - - assert_do_frame(&runtime, &memory); /* count = 3 */ - assert_richpresence_output(&runtime, &memory, "Three!"); - - rc_runtime_destroy(&runtime); -} - -/* ======================================================== */ - -void test_runtime_progress(void) { - TEST_SUITE_BEGIN(); - - TEST(test_empty); - TEST(test_single_achievement); - TEST(test_invalid_marker); - TEST(test_invalid_memref_chunk_id); - TEST(test_modified_data); - TEST(test_single_achievement_deactivated); - TEST(test_single_achievement_md5_changed); - TEST(test_single_achievement_sized); - TEST(test_empty_sized); - - TEST(test_no_core_group); - TEST(test_memref_shared_address); - TEST(test_memref_addsource); - TEST(test_memref_indirect); - TEST(test_memref_indirect_delta); - TEST(test_memref_double_indirect); - - TEST(test_multiple_achievements); - TEST(test_multiple_achievements_ignore_triggered_and_inactive); - TEST(test_multiple_achievements_overwrite_waiting); - TEST(test_multiple_achievements_reactivate_waiting); - TEST(test_multiple_achievements_paused_and_primed); - TEST(test_multiple_achievements_deactivated_memrefs); - TEST(test_multiple_achievements_deactivated_no_memrefs); - - TEST(test_single_leaderboard); - TEST(test_multiple_leaderboards); - TEST(test_multiple_leaderboards_ignore_inactive); - TEST(test_multiple_leaderboards_ignore_modified); - - TEST(test_rich_presence_none); - TEST(test_rich_presence_static); - TEST(test_rich_presence_simple_lookup); - TEST(test_rich_presence_tracked_hits); - TEST(test_rich_presence_tracked_hits_md5_changed); - TEST(test_rich_presence_conditional_display); - TEST(test_rich_presence_conditional_display_md5_changed); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_timing.c b/src/rcheevos/test/rcheevos/test_timing.c deleted file mode 100644 index 00973db637..0000000000 --- a/src/rcheevos/test/rcheevos/test_timing.c +++ /dev/null @@ -1,166 +0,0 @@ -#include "rc_runtime.h" -#include "rc_internal.h" - -#include "mock_memory.h" - -#include "../test_framework.h" - -static rc_runtime_t runtime; -static int trigger_count[128]; - -static void event_handler(const rc_runtime_event_t* e) -{ - if (e->type == RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED) - { - rc_trigger_t* trigger = rc_runtime_get_achievement(&runtime, e->id); - trigger_count[e->id]++; - - rc_reset_trigger(trigger); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - } -} - -static void _assert_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr) -{ - int result = rc_runtime_activate_achievement(runtime, id, memaddr, NULL, 0); - ASSERT_NUM_EQUALS(result, RC_OK); -} -#define assert_activate_achievement(runtime, id, memaddr) ASSERT_HELPER(_assert_activate_achievement(runtime, id, memaddr), "assert_activate_achievement") - -static void do_timing(void) -{ - uint8_t ram[256], last, next, bitcount; - memory_t memory; - int i, j, mask; - clock_t total_clocks = 0, start, end; - double elapsed, average; - - memory.ram = ram; - memory.size = sizeof(ram); - memset(&ram[0], 0, sizeof(ram)); - for (i = 0; i < 0x3F; i++) - ram[0xC0 + i] = i; - - memset(&trigger_count, 0, sizeof(trigger_count)); - - rc_runtime_init(&runtime); - - assert_activate_achievement(&runtime, 1, "0xH0000=1"); - assert_activate_achievement(&runtime, 2, "0xH0001=1"); - assert_activate_achievement(&runtime, 3, "0xH0001=0"); - assert_activate_achievement(&runtime, 4, "0xH0002!=d0xH0002"); - assert_activate_achievement(&runtime, 5, - "A:0xH0000_A:0xH0001_A:0xH0002_A:0xH0003_A:0xH0004_A:0xH0005_A:0xH0006_A:0xH0007_" - "A:0xH0008_A:0xH0009_A:0xH000A_A:0xH000B_A:0xH000C_A:0xH000D_A:0xH000E_0xH000F=0xH0084"); - assert_activate_achievement(&runtime, 6, - "A:0xH0000_A:0xH0001_A:0xH0002_A:0xH0003_A:0xH0004_A:0xH0005_A:0xH0006_0xH0007=0xK0080_" - "A:0xH0008_A:0xH0009_A:0xH000A_A:0xH000B_A:0xH000C_A:0xH000D_A:0xH000E_0xH000F=0xK0081"); - assert_activate_achievement(&runtime, 7, "I:0xH0024_0xL00C0>8"); - assert_activate_achievement(&runtime, 8, "O:0xH0000=1_O:0xH0001=1_O:0xH0002=1_O:0xH0003=1_O:0xH0004=1_O:0xH0005=1_O:0xH0006=1_0xH0007=1"); - assert_activate_achievement(&runtime, 9, "N:0xH0000=1_N:0xH0001=1_N:0xH0002=1_N:0xH0003=1_N:0xH0004=1_N:0xH0005=1_N:0xH0006=1_0xH0007=1"); - assert_activate_achievement(&runtime, 10, "0xH008A>0xH008B_0xH008A<0xH0089"); - assert_activate_achievement(&runtime, 11, - "0xH0040=0xH0060_0xH0041=0xH0061_0xH0042=0xH0062_0xH0043=0xH0063_" - "0xH0044=0xH0064_0xH0045=0xH0065_0xH0046=0xH0066_0xH0047=0xH0067_" - "0xH0048=0xH0068_0xH0049=0xH0069_0xH004A=0xH006A_0xH004B=0xH006B_" - "0xH004C=0xH006C_0xH004D=0xH006D_0xH004E=0xH006E_0xH004F=0xH006F"); - assert_activate_achievement(&runtime, 12, "0xH0003=1.3._R:0xH0004=1_P:0xH0005=1"); - assert_activate_achievement(&runtime, 13, "0xH0003=1.3.SR:0xH0004=1_P:0xH0005=1"); - assert_activate_achievement(&runtime, 14, "I:0xH0024_0xH0040>d0xH0040"); - assert_activate_achievement(&runtime, 15, "b0xH0085=0xH0080"); - assert_activate_achievement(&runtime, 16, "0xH0026=0xH0028S0xH0023=0xH0027S0xH0025=0xH0022"); - assert_activate_achievement(&runtime, 17, - "C:0xH0000!=0_C:0xH0001!=0_C:0xH0002!=0_C:0xH0003!=0_C:0xH0004!=0_C:0xH0005!=0_C:0xH0006!=0_C:0xH0007!=0_" - "C:0xH0008!=0_C:0xH0009!=0_C:0xH000A!=0_C:0xH000B!=0_C:0xH000C!=0_C:0xH000D!=0_C:0xH000E!=0_0xH000F!=0.20."); - assert_activate_achievement(&runtime, 18, "A:0xL0087*10000_A:0xU0086*1000_A:0xL0086*100_A:0xU0085*10_0xL0x0085=0x 0080"); - - /* we expect these to only be true initially, so forcibly change them from WAITING to ACTIVE */ - rc_runtime_get_achievement(&runtime, 5)->state = RC_TRIGGER_STATE_ACTIVE; - rc_runtime_get_achievement(&runtime, 6)->state = RC_TRIGGER_STATE_ACTIVE; - rc_runtime_get_achievement(&runtime, 15)->state = RC_TRIGGER_STATE_ACTIVE; - rc_runtime_get_achievement(&runtime, 18)->state = RC_TRIGGER_STATE_ACTIVE; - - for (i = 0; i < 65536; i++) - { - /* - * $00-$1F = individual bits of i - * $20-$3F = number of times individual bits changed - * $40-$5F = number of times individual bits were 0 - * $60-$7F = number of times individual bits were 1 - * $80-$83 = i - * $84 = bitcount i - * $85-$87 = bcd i (LE) - * $88-$8C = ASCII i - * $C0-$FF = 0-63 - */ - mask = 1; - bitcount = 0; - for (j = 0; j < 32; j++) - { - next = ((i & mask) != 0) * 1; - mask <<= 1; - last = ram[j]; - - ram[j] = next; - ram[j + 0x60] += next; - bitcount += next; - ram[j + 0x40] += (1 - next); - ram[j + 0x20] += (next != last) * 1; - } - ram[0x80] = i & 0xFF; - ram[0x81] = (i >> 8) & 0xFF; - ram[0x82] = (i >> 16) & 0xFF; - ram[0x83] = (i >> 24) & 0xFF; - ram[0x84] = bitcount; - ram[0x85] = (i % 10) | ((i / 10) % 10) << 4; - ram[0x86] = ((i / 100) % 10) | ((i / 1000) % 10) << 4; - ram[0x87] = ((i / 10000) % 10); - ram[0x88] = ((i / 10000) % 10) + '0'; - ram[0x89] = ((i / 1000) % 10) + '0'; - ram[0x8A] = ((i / 100) % 10) + '0'; - ram[0x8B] = ((i / 10) % 10) + '0'; - ram[0x8C] = (i % 10) + '0'; - - start = clock(); - rc_runtime_do_frame(&runtime, event_handler, peek, &memory, NULL); - end = clock(); - - total_clocks += (end - start); - } - - elapsed = (double)total_clocks * 1000 / CLOCKS_PER_SEC; - average = elapsed * 1000 / i; - printf("\n%0.6fms elapsed, %0.6fus average", elapsed, average); - - ASSERT_NUM_EQUALS(trigger_count[ 0], 0); /* no achievement 0 */ - ASSERT_NUM_EQUALS(trigger_count[ 1], 32768); /* 0xH0000 = 1 every other frame */ - ASSERT_NUM_EQUALS(trigger_count[ 2], 32768); /* 0xH0001 = 1 2 / 4 frames */ - ASSERT_NUM_EQUALS(trigger_count[ 3], 32766); /* 0xH0001 = 0 2 / 4 frames (won't trigger until after first time it's 1) */ - ASSERT_NUM_EQUALS(trigger_count[ 4], 16383); /* 0xH0002!=d0xH0002 every two frames (no delta for first frame) */ - ASSERT_NUM_EQUALS(trigger_count[ 5], 65536); /* sum of bits = bitcount (16-bit) */ - ASSERT_NUM_EQUALS(trigger_count[ 6], 65536); /* sum of bits = bitcount (8-bit) */ - ASSERT_NUM_EQUALS(trigger_count[ 7], 6912); /* I:0xH0024_0xL00C0>8 pointer advances every eight frames */ - ASSERT_NUM_EQUALS(trigger_count[ 8], 65280); /* number of times any bit is set in lower byte */ - ASSERT_NUM_EQUALS(trigger_count[ 9], 256); /* number of times all bits are set in lower byte */ - ASSERT_NUM_EQUALS(trigger_count[10], 7400); /* hundreds digit less than thousands, but greater than tens */ - ASSERT_NUM_EQUALS(trigger_count[11], 256); /* number of times individual bits were 0 or 1 is equal */ - ASSERT_NUM_EQUALS(trigger_count[12], 2048); /* number of times bit3 was set without bit 4 or 5 */ - ASSERT_NUM_EQUALS(trigger_count[13], 3071); /* number of times bit3 was set without bit 4 xor 5 */ - ASSERT_NUM_EQUALS(trigger_count[14], 10350); /* I:0xH0024_0xH0040>d0xH0040 - delta increase across moving target */ - ASSERT_NUM_EQUALS(trigger_count[15], 1100); /* BCD check (only valid for two digits) */ - ASSERT_NUM_EQUALS(trigger_count[16], 24); /* random collection of bit changes being equal */ - ASSERT_NUM_EQUALS(trigger_count[17], 22187); /* number of times 20 bits or more are set across frames */ - ASSERT_NUM_EQUALS(trigger_count[18], 65536); /* BCD reconstruction */ - - rc_runtime_destroy(&runtime); -} - -void test_timing(void) { - TEST_SUITE_BEGIN(); - TEST(do_timing); - TEST(do_timing); - TEST(do_timing); - TEST(do_timing); - TEST(do_timing); - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_trigger.c b/src/rcheevos/test/rcheevos/test_trigger.c deleted file mode 100644 index a2db7d8870..0000000000 --- a/src/rcheevos/test/rcheevos/test_trigger.c +++ /dev/null @@ -1,2492 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -static void _assert_parse_trigger(rc_trigger_t** trigger, void* buffer, size_t buffer_size, const char* memaddr) -{ - int size; - unsigned* overflow; - - size = rc_trigger_size(memaddr); - ASSERT_NUM_GREATER(size, 0); - ASSERT_NUM_LESS_EQUALS(size + 4, buffer_size); - - overflow = (unsigned*)(((char*)buffer) + size); - *overflow = 0xCDCDCDCD; - - *trigger = rc_parse_trigger(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(*trigger); - - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } -} -#define assert_parse_trigger(trigger, buffer, memaddr) ASSERT_HELPER(_assert_parse_trigger(trigger, buffer, sizeof(buffer), memaddr), "assert_parse_trigger") - -static void _assert_evaluate_trigger(rc_trigger_t* trigger, memory_t* memory, int expected_result) { - int result = rc_test_trigger(trigger, peek, memory, NULL); - ASSERT_NUM_EQUALS(result, expected_result); -} -#define assert_evaluate_trigger(trigger, memory, expected_result) ASSERT_HELPER(_assert_evaluate_trigger(trigger, memory, expected_result), "assert_evaluate_trigger") - -static rc_condition_t* trigger_get_cond(rc_trigger_t* trigger, int group_index, int cond_index) { - rc_condset_t* condset = trigger->requirement; - rc_condition_t* cond; - - if (group_index != 0) { - --group_index; - condset = trigger->alternative; - while (group_index-- != 0) { - if (condset == NULL) - break; - - condset = condset->next; - } - } - - if (condset == NULL) - return NULL; - - cond = condset->conditions; - while (cond_index-- != 0) { - if (cond == NULL) - break; - - cond = cond->next; - } - - return cond; -} - -static void _assert_hit_count(rc_trigger_t* trigger, int group_index, int cond_index, uint32_t expected_hit_count) { - rc_condition_t* cond = trigger_get_cond(trigger, group_index, cond_index); - ASSERT_PTR_NOT_NULL(cond); - - ASSERT_NUM_EQUALS(cond->current_hits, expected_hit_count); -} -#define assert_hit_count(trigger, group_index, cond_index, expected_hit_count) ASSERT_HELPER(_assert_hit_count(trigger, group_index, cond_index, expected_hit_count), "assert_hit_count") - -static int evaluate_trigger(rc_trigger_t* self, memory_t* memory) { - return rc_evaluate_trigger(self, peek, memory, NULL); -} - -/* ======================================================== */ - -static void test_alt_groups() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=16S0xH0002=52S0xL0004=6"); - - /* core not true, both alts are */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 1, 0, 1); - assert_hit_count(trigger, 2, 0, 1); - - /* core and both alts true */ - ram[1] = 16; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1); - assert_hit_count(trigger, 1, 0, 2); - assert_hit_count(trigger, 2, 0, 2); - - /* core and first alt true */ - ram[4] = 0; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 2); - assert_hit_count(trigger, 1, 0, 3); - assert_hit_count(trigger, 2, 0, 2); - - /* core true, but neither alt */ - ram[2] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 3); - assert_hit_count(trigger, 1, 0, 3); - assert_hit_count(trigger, 2, 0, 2); - - /* core and second alt true */ - ram[4] = 6; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 4); - assert_hit_count(trigger, 1, 0, 3); - assert_hit_count(trigger, 2, 0, 3); -} - -static void test_empty_core() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "S0xH0002=2S0xL0004=4"); - - /* core implicitly true, neither alt true */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 0); - assert_hit_count(trigger, 2, 0, 0); - - /* first alt true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 1); - assert_hit_count(trigger, 2, 0, 0); - - /* both alts true */ - ram[4] = 4; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 2); - assert_hit_count(trigger, 2, 0, 1); - - /* second alt true */ - ram[2] = 0; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 2); - assert_hit_count(trigger, 2, 0, 2); -} - -static void test_empty_alt() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0002=2SS0xL0004=4"); - - /* core false, first alt implicitly true */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 2, 0, 0); - - /* core true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1); - assert_hit_count(trigger, 2, 0, 0); - - /* core and both alts true */ - ram[4] = 4; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 2); - assert_hit_count(trigger, 2, 0, 1); -} - -static void test_empty_last_alt() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0002=2S0xL0004=4S"); - - /* core false, second alt implicitly true */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 1, 0, 0); - - /* core true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1); - assert_hit_count(trigger, 1, 0, 0); - - /* core and both alts true */ - ram[4] = 4; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 2); - assert_hit_count(trigger, 1, 0, 1); -} - -static void test_empty_all_alts() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0002=2SS"); - - /* core false, all alts implicitly true */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - - /* core true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1); -} - -static void test_resetif_in_alt_group() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18(1)_R:0xH0000=1S0xH0002=52(1)S0xL0004=6(1)_R:0xH0000=2"); - - /* all conditions true, no resets */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - - /* reset in core group resets everything */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 0, 0U); - - /* all conditions true, no resets */ - ram[0] = 0; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - - /* reset in alt group resets everything */ - ram[0] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 0, 0U); -} - -static void test_pauseif_in_alt_group() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_P:0xH0000=1S0xH0002=52S0xL0004=6_P:0xH0000=2"); - - /* all conditions true, no resets */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - - /* pause in core group only pauses core group */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 1, 0, 2U); - assert_hit_count(trigger, 2, 0, 2U); - - /* unpaused */ - ram[0] = 0; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 2U); - assert_hit_count(trigger, 1, 0, 3U); - assert_hit_count(trigger, 2, 0, 3U); - - /* pause in alt group only pauses alt group */ - ram[0] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - assert_hit_count(trigger, 1, 0, 4U); - assert_hit_count(trigger, 2, 0, 3U); -} - -static void test_pauseif_resetif_in_alt_group() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0000=0.1._0xH0003=2SP:0xH0001=18_R:0xH0002=52"); - - /* capture hitcount */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - - /* prevent future hit counts */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - - /* unpause alt group, hit count should be reset */ - ram[1] = 16; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - - /* repause alt group, capture hitcount */ - ram[0] = 0; - ram[1] = 18; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - - /* trigger condition. alt group is paused, so should be considered false */ - ram[3] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - - /* trigger condition. alt group is unpaused, so reset will prevent trigger */ - ram[1] = 16; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - - /* trigger condition. alt group is unpaused, and not resetting, so allow trigger */ - ram[2] = 30; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1U); -} - -static void test_pauseif_hitcount_with_reset() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_P:0xH0002=52.1._R:0xH0003=1SR:0xH0003=2"); - - /* pauseif triggered, non-pauseif conditions ignored */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 0, 1, 1); - assert_hit_count(trigger, 0, 2, 0); - assert_hit_count(trigger, 1, 0, 0); - - /* pause condition is no longer true, but hitcount keeps it paused */ - ram[2] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 0, 1, 1); - assert_hit_count(trigger, 0, 2, 0); - assert_hit_count(trigger, 1, 0, 0); - - /* resetif in paused group is ignored */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 0, 1, 1); - assert_hit_count(trigger, 0, 2, 0); - assert_hit_count(trigger, 1, 0, 0); - - /* resetif in alternate group is honored, active resetif prevents trigger */ - ram[3] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0); - assert_hit_count(trigger, 0, 1, 0); - assert_hit_count(trigger, 0, 2, 0); - assert_hit_count(trigger, 1, 0, 0); - - /* resetif no longer active, pause not active, first condition true, trigger activates */ - ram[3] = 3; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1); - assert_hit_count(trigger, 0, 1, 0); - assert_hit_count(trigger, 0, 2, 0); - assert_hit_count(trigger, 1, 0, 0); -} - -static void test_measured() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[256]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)"); - ASSERT_NUM_EQUALS(trigger->measured_as_percent, 0); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented to reach target */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - target previously met */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_measured_as_percent() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[256]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) */ - assert_parse_trigger(&trigger, buffer, "G:0xH0002=52(3)"); - ASSERT_NUM_EQUALS(trigger->measured_as_percent, 1); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented to reach target */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - target previously met */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_measured_comparison() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[256]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(byte(2) >= 80) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002>=80"); - - /* condition is not true */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0x34U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition is still not true */ - ram[2] = 79; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 79U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition is true - value matches */ - ram[2] = 80; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 80U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition is true - value exceeds */ - ram[2] = 255; - trigger->state = RC_TRIGGER_STATE_ACTIVE; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 255U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); -} - -static void test_measured_addhits() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(5, byte(1) == 10 || byte(2) == 10)) */ - assert_parse_trigger(&trigger, buffer, "C:0xH0001=10_M:0xH0002=10(5)"); - - /* neither is true - hit count should not be captured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - assert_hit_count(trigger, 0, 1, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* second is true - hit count should be incremented by one */ - ram[2] = 10; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - assert_hit_count(trigger, 0, 1, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* both are true - hit count should be incremented by two */ - ram[1] = 10; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* only first is true - hit count should be incremented by one */ - ram[2] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 4U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* neither is true - hit count should not increment */ - ram[1] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 4U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* first is true - hit count should be incremented by one and trigger */ - ram[1] = 10; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 5U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); -} - -static void test_measured_indirect() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[384]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(byte(0) + 2) == 52)) */ - assert_parse_trigger(&trigger, buffer, "I:0xH0000_M:0xH0002=52(3)"); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is no longer true - hit count should not be incremented */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - ram[0] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is no longer true - hit count should not be incremented */ - ram[2] = 30; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_measured_multiple() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* multiple measured conditions are only okay if they all have the same target, in which - * case, the maximum of all the measured values is returned */ - - /* measured(repeated(3, byte(2) == 52)) || measured(repeated(3, byte(3) == 17)) */ - assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(3)SM:0xH0003=17(3)"); - - /* first condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* second condition is true - second hit count should be incremented - both will be the same */ - ram[2] = 9; - ram[3] = 17; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* second condition still true - second hit count should be incremented and become prominent */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* switch back to first condition */ - ram[2] = 52; - ram[3] = 8; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 2U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* first hit count will be incremented and target met */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 3U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* both true, only second will increment as first target is met */ - ram[3] = 17; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 3U); - assert_hit_count(trigger, 2, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* both true, both targets met */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 3U); - assert_hit_count(trigger, 2, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_measured_multiple_with_hitcount_in_core() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* multiple measured conditions are only okay if they all have the same target, in which - * case, the maximum of all the measured values is returned */ - - /* repeated(7, byte(1) == 18) && (measured(repeated(3, byte(2) == 52)) || measured(repeated(3, byte(3) == 17))) */ - assert_parse_trigger(&trigger, buffer, "0xH0001=18(7)SM:0xH0002=52(3)SM:0xH0003=17(3)"); - - /* first condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* second condition is true - second hit count should be incremented - both will be the same */ - /* core hit target is greater than any measured value, but should not be measured */ - ram[2] = 9; - ram[3] = 17; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_measured_while_paused() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1"); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* paused - hit count should not be incremented */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* unpaused - hit count should be incremented */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); -} - -static void test_measured_while_paused_multiple() { - uint8_t ram[] = {0x00, 0x00, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* (measured(repeated(6, byte(2) == 52)) && unless(bit0(1) == 1)) || (measured(repeated(6, byte(0) == 0)) && unless(bit1(1) == 1)) */ - assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(6)_P:0xM0001=1SM:0xH0000=0(6)_P:0xN0001=1"); - - /* both alts should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); - - /* first alt paused - second should update */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); - - /* first still paused - second should update again */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); - - /* both paused - neither should update - expect last measured value to be kept */ - ram[1] = 3; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); - - /* first unpaused - it will update, measured will use the active value */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 2U); - assert_hit_count(trigger, 2, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); - - /* both paused - neither should update - expect last measured value to be kept */ - ram[1] = 3; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 2U); - assert_hit_count(trigger, 2, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); - - /* both unpaused - both updated, will use higher */ - ram[1] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 3U); - assert_hit_count(trigger, 2, 0, 4U); - ASSERT_NUM_EQUALS(trigger->measured_value, 4U); - ASSERT_NUM_EQUALS(trigger->measured_target, 6U); -} - -static void test_measured_while_paused_reset_alt() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1SR:0xH0003=1"); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* paused - hit count should not be incremented */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* reset - hit count should be cleared */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* unpaused, reset still true, hit count should remain cleared */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* reset not true - hit count should be incremented */ - ram[3] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); -} - -static void test_measured_while_paused_reset_core() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ - assert_parse_trigger(&trigger, buffer, "R:0xH0003=1SM:0xH0002=52(3)_P:0xH0001=1"); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* paused - hit count should not be incremented */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* reset - hit count should be cleared */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* unpaused, reset still true, hit count should remain cleared */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* reset not true - hit count should be incremented */ - ram[3] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); -} - -static void test_measured_while_paused_reset_non_hitcount() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && (never(byte(3) == 1)) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=99_P:0xH0001=1SR:0xH0003=1"); - - /* initial value */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 52U); - ASSERT_NUM_EQUALS(trigger->measured_target, 99U); - - /* paused - capture value and return that*/ - ram[1] = 1; - ram[2] = 60; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 52U); - - /* reset - captured value should not be cleared */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 52U); - - /* unpaused, reset still true, value should reflect current */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 60U); -} - -static void test_measured_while_paused_extra_alts() { - uint8_t ram[] = { 0x00, 0x00, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* (measured(byte(2) == 99) && unless(byte(1) == 1)) || (byte(3) == 1) */ - assert_parse_trigger(&trigger, buffer, "SM:0xH0002=99_P:0xH0001=1S0xH0003=1"); - - /* alt1 will capture measured value */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 52U); - ASSERT_NUM_EQUALS(trigger->measured_target, 99U); - - /* first alt paused - expect last measured value to be kept */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 52U); - ASSERT_NUM_EQUALS(trigger->measured_target, 99U); - - /* measured value changed but alt still paused - expect last measured value to be kept */ - ram[2] = 61; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 52U); - ASSERT_NUM_EQUALS(trigger->measured_target, 99U); - - /* first alt unpaused - it will update, measured will use the active value */ - ram[1] = 0; - ram[2] = 63; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 63U); - ASSERT_NUM_EQUALS(trigger->measured_target, 99U); -} - -static void test_measured_reset_hitcount() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52)) && unless(byte(1) == 1) && never(byte(3) == 1) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_P:0xH0001=1_R:0xH0003=1"); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* paused - hit count should not be incremented */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* reset primed, but ignored by pause */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* unpaused, reset should clear value */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* no longer reset, hit count should increment */ - ram[3] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - - /* reset again, hit count should go back to 0 */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); -} - -static void test_measured_reset_comparison() { - uint8_t ram[] = {0x00, 0x12, 0x02, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(byte(2) >= 10) && unless(byte(1) == 1) && never(byte(3) == 1) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002>=10_P:0xH0001=1_R:0xH0003=1"); - - /* condition is true - measured will come from value */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 10U); - - /* condition is true - value updated */ - ram[2] = 3; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - - /* paused - updated value should be ignored */ - ram[1] = 1; - ram[2] = 4; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - - /* reset primed, but ignored by pause */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - - /* unpaused, reset should not affect non-hitcount measurement */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 4U); - - /* no longer reset, value updated */ - ram[3] = 0; - ram[2] = 5; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 5U); - - /* reset again, should not affect non-hitcount measurement */ - ram[3] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->measured_value, 5U); -} - -static void test_measured_if() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52), when=byte(0) == 1) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(3)_Q:0xH0000=1"); - - /* condition is true - hit count should be incremented, but not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented and measured */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - hit count should be incremented to reach target, but it's not measured */ - ram[0] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* condition is true - target previously met, but now it's measured */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_measured_if_comparison() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(byte(2) >= 80, when=byte(0)==1) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002>=80_Q:0xH0000=1"); - - /* condition is not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition not true, but measured */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0x34U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition is still not true, but measured */ - ram[2] = 79; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 79U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition is true, but not measured */ - ram[0] = 0; - ram[2] = 80; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); - - /* condition is true and measured */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 80U); - ASSERT_NUM_EQUALS(trigger->measured_target, 80U); -} - -static void test_measured_if_multiple_measured() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* multiple measured conditions are only okay if they all have the same target, in which - * case, the maximum of all the measured values is returned */ - - /* measured(repeated(5, byte(2) == 52), when=byte(0)=1) || measured(repeated(5, byte(3) == 17), when=byte(0)=2) */ - assert_parse_trigger(&trigger, buffer, "SM:0xH0002=52(5)_Q:0xH0000=1SM:0xH0003=17(5)_Q:0xH0000=2"); - - /* first condition is true - hit count should be incremented, but not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* second condition is true - second hit count should be incremented - both will be the same; still not measured */ - ram[2] = 9; - ram[3] = 17; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* second condition still true - second hit count should be incremented and become prominent, but first is measured */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 1U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* switch back to first condition */ - ram[2] = 52; - ram[3] = 8; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 2U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* first hit count will be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 3U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* first hit count will be incremented, but neither measured */ - ram[0] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 4U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* first will increment to trigger state, but it's not measured - second is */ - ram[0] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 1, 0, 5U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* first is measured and will trigger */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 1, 0, 5U); - assert_hit_count(trigger, 2, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 5U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); -} - -static void test_measured_if_multiple_measured_if() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(5, byte(2) == 52), when=byte(0)=1 && byte(1)==1) */ - assert_parse_trigger(&trigger, buffer, "M:0xH0002=52(5)_Q:0xH0000=1_Q:0xH0001=1"); - - /* first condition is true - hit count should be incremented, but not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* second condition is true - hit count still incremented, but not measured because of third condition */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* third condition is true, measured should be measured */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* second condition no longer true, measured ignored */ - ram[0] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 4U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* hit target met, but not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 5U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* hit target met, but not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 0, 5U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); - - /* second condition true, measured should be measured, trigger will fire */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 0, 5U); - ASSERT_NUM_EQUALS(trigger->measured_value, 5U); - ASSERT_NUM_EQUALS(trigger->measured_target, 5U); -} - -static void test_measured_if_while_paused() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* measured(repeated(3, byte(2) == 52), when=byte(0)==1) && unless(byte(1) == 1) */ - /* NOTE: this test also verifies the behavior when the MeasuredIf is first */ - assert_parse_trigger(&trigger, buffer, "Q:0xH0000=1_M:0xH0002=52(3)_P:0xH0001=1"); - - /* condition is true - hit count should be incremented, but not measured */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* paused - hit count should not be incremented */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* paused - but measured - measured_value is not updated when paused */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - - /* unpaused - hit count should be incremented and measured value captured */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* paused - hit count should not be incremented, and last hit count should be measured */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - - /* paused but not measured - pause will prevent evaluation of MeasuredIf, so measured retained */ - ram[0] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); -} - -static void test_measured_trigger() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* never(byte(0) != 0) && trigger_when(measured(repeated(3, byte(2) == 52))) */ - assert_parse_trigger(&trigger, buffer, "R:0xH0000!=0SM:0xH0002=52(3)ST:0=1"); - ASSERT_NUM_EQUALS(trigger->measured_as_percent, 0); - - /* condition is true - hit count should be incremented, and trigger shown */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - assert_hit_count(trigger, 1, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* core condition is false - trigger should not be shown and hit count reset */ - ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); - assert_hit_count(trigger, 1, 0, 0U); - ASSERT_NUM_EQUALS(trigger->measured_value, 0U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* core condition is true again - hit count should be incremented, and trigger shown */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - assert_hit_count(trigger, 1, 0, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 1U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* increment hit count */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - assert_hit_count(trigger, 1, 0, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); - - /* trigger */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); - assert_hit_count(trigger, 1, 0, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 3U); - ASSERT_NUM_EQUALS(trigger->measured_target, 3U); -} - -static void test_resetnextif_trigger() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* ResetNextIf byte(0x0002)=1 - * byte(0x0001)=1 (1) - * Trigger byte(0x0003)=0 - */ - assert_parse_trigger(&trigger, buffer, "Z:0xH0002=1_0xH0001=1.1._T:0xH0003=0"); - - /* both conditions false */ - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - - /* second condition true */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); - - /* second condition not true */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); - - /* second condition true */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); - - /* first condition true */ - ram[2] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - - /* first condition not true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); - - /* second condition not true */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); - - /* first condition true */ - ram[2] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - - /* first condition not true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); -} - -static void test_evaluate_trigger_inactive() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); - trigger->state = RC_TRIGGER_STATE_INACTIVE; - - /* Inactive is a permanent state - trigger is initially true */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - ram[2] = 24; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - - /* Trigger no longer true, still inactive */ - ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - - /* hits should not be tallied when inactive */ - assert_hit_count(trigger, 0, 0, 0U); - assert_hit_count(trigger, 0, 1, 0U); - - /* memrefs should be updated while inactive */ - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.value, 24U); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.changed, 0); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.value.memref->value.prior, 52U); - - /* reset should be ignored while inactive */ - ram[4] = 4; - trigger_get_cond(trigger, 0, 0)->current_hits = 1U; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - assert_hit_count(trigger, 0, 0, 1U); -} - -static void test_evaluate_trigger_waiting() { - uint8_t ram[] = {0x00, 0x12, 0x18, 0xAB, 0x09}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); - trigger->state = RC_TRIGGER_STATE_WAITING; - - /* trigger is ready to fire, but won't as long as its waiting */ - /* prevents triggers from uninitialized memory */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); - ram[2] = 16; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); - - /* waiting trigger should not tally hits */ - ASSERT_FALSE(trigger->has_hits); - - /* ResetIf makes the trigger state false, so the trigger should become active */ - ram[4] = 4; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* reset to previous state */ - trigger->state = RC_TRIGGER_STATE_WAITING; - ram[4] = 9; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); - ASSERT_FALSE(trigger->has_hits); - - /* trigger is no longer true, proceed to active state */ - ram[1] = 5; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - assert_hit_count(trigger, 0, 0, 0U); - assert_hit_count(trigger, 0, 1, 1U); -} - -static void test_evaluate_trigger_reset() { - uint8_t ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* generate a hit count */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - - /* ResetIf that resets hits returns RESET, but doesn't change the state */ - ram[4] = 4; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_FALSE(trigger->has_hits); - - /* ResetIf that doesn't resets hits doesn't return RESET */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_FALSE(trigger->has_hits); -} - -static void test_evaluate_trigger_reset_next() { - uint8_t ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "Z:0xL0004=4_0xH0001=5.2._0xH0003=3"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* generate a hit count */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - - /* ResetNext that resets hits returns RESET, but doesn't change the state */ - ram[4] = 4; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); /* ResetNext will have a hit */ - - /* ResetNext that doesn't resets hits doesn't return RESET */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); /* ResetNext will have a hit */ - - /* Secondary hit should still be tallied, ResetNext that doesn't reset hits doesn't return RESET */ - ram[3] = 3; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - - /* ResetNext no longer true, tally hit */ - ram[4] = 5; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - - /* ResetNext that resets hits returns RESET, but doesn't reset the secondary hits */ - ram[4] = 4; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - - /* ResetNext no longer true, tally hit */ - ram[4] = 5; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - - /* tally second hit to trigger */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); -} - -static void test_evaluate_trigger_triggered() { - uint8_t ram[] = {0x00, 0x05, 0x10, 0xAB, 0x09}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0002<=52_R:0xL0004=4"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* transition to TRIGGERED */ - ram[1] = 18; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 0, 1, 1U); - - /* triggered trigger remains triggered, but returns INACTIVE and does not increment hit counts */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 0, 1, 1U); - - /* triggered trigger remains triggered when no longer true */ - ram[1] = 5; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); - assert_hit_count(trigger, 0, 0, 1U); - assert_hit_count(trigger, 0, 1, 1U); - - /* triggered trigger does not update deltas */ - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.value.memref->value.value, 18U); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.value.memref->value.changed, 1U); -} - -static void test_evaluate_trigger_paused() { - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0001=18_0xH0003=171_P:0xH0002=1SR:0xH0004=4"); - - /* INACTIVE is a permanent state - trigger is initially true */ - trigger->state = RC_TRIGGER_STATE_INACTIVE; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - - /* PauseIf is ignored when INACTIVE */ - ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - - /* unpause, switch to WAITING, ready to trigger, so will stay WAITING */ - ram[2] = 2; - trigger->state = RC_TRIGGER_STATE_WAITING; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_WAITING); - - /* PauseIf makes the evaluation false, so will transition to ACTIVE, but PAUSED */ - ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); - ASSERT_TRUE(trigger->has_hits); /* the PauseIf has a hit */ - assert_hit_count(trigger, 0, 0, 0U); - - /* hitcounts will update when unpaused; adjust memory so trigger is no longer true */ - ram[2] = 2; - ram[3] = 99; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - ASSERT_TRUE(trigger->has_hits); - assert_hit_count(trigger, 0, 0, 1U); - - /* hitcounts should remain while paused */ - ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); - ASSERT_TRUE(trigger->has_hits); - assert_hit_count(trigger, 0, 0, 1U); - - /* ResetIf while paused should notify, but not change state */ - ram[4] = 4; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PAUSED); - ASSERT_FALSE(trigger->has_hits); - assert_hit_count(trigger, 0, 0, 0U); - - /* ResetIf without hitcounts should return current state */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); - - /* trigger while paused is ignored */ - ram[4] = 0; - ram[3] = 171; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PAUSED); - - /* trigger should file when unpaused */ - ram[2] = 2; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); - - /* triggered trigger ignore pause */ - ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); -} - -static void test_evaluate_trigger_primed() { - uint8_t ram[] = {0x00, 0x01, 0x00, 0x01, 0x00}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[640]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0000=1_T:0xH0001=1_0xH0002=1_T:0xH0003=1_0xH0004=1"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* T (trigger) conditions are true, but nothing else */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* one non-trigger condition is still false */ - ram[0] = ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* all non-trigger conditions are true, one trigger condition is not true */ - ram[1] = 0; ram[4] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - - /* non-trigger condition is false again */ - ram[0] = 0; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* all conditions are true */ - ram[0] = ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); - - /* one non-trigger condition is false */ - ram[3] = 0; - trigger->state = RC_TRIGGER_STATE_ACTIVE; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - - /* all conditions are true */ - ram[3] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); -} - -static void test_evaluate_trigger_primed_in_alts() { - uint8_t ram[] = {0x01, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1_0xH0002=1ST:0xH0003=1_0xH0004=1"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* core is true, but neither alt is primed */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* both alts primed */ - ram[2] = ram[4] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - - /* only second alt is primed */ - ram[4] = 0; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - - /* neither alt is primed */ - ram[2] = 0; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* both alts primed */ - ram[2] = ram[4] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - - /* alt 2 is true */ - ram[3] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); -} - -static void test_evaluate_trigger_primed_one_alt() { - uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1S0xH0002=1"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* core must be true for trigger to be primed */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* second alt is true, but core is not */ - ram[2] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* first alt is true, but core is not */ - ram[2] = 0; ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - - /* only core is true, first alt is marked as Trigger, eligible to fire */ - ram[1] = 0; ram[0] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - - /* alt is true */ - ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_TRIGGERED); -} - -static void test_evaluate_trigger_disabled() { - uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "0xH0000=1ST:0xH0001=1S0xH0002=1"); - trigger->state = RC_TRIGGER_STATE_DISABLED; - - /* state stays DISABLED, but evaluate returns INACTIVE */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_INACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_DISABLED); -} - -static void test_evaluate_trigger_chained_resetnextif() { - uint8_t ram[] = {0x00, 0x00, 0x00, 0x00, 0x00}; - memory_t memory; - rc_trigger_t* trigger; - char buffer[640]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* once(byte(4)==1 && never(repeated(2, byte(3)==1 && never(byte(1)==1 || byte(2)==1))) && trigger_when(byte(0)==1) */ - assert_parse_trigger(&trigger, buffer, "O:0xH0001=1_Z:0xH0002=1_Z:0xH0003=1.2._0xH0004=1.1._T:0xH0000=1"); - trigger->state = RC_TRIGGER_STATE_ACTIVE; - - /* nothing is true */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_ACTIVE); - assert_hit_count(trigger, 0, 0, 0); /* OrNext 0x0001 == 1 */ - assert_hit_count(trigger, 0, 1, 0); /* ResetNextIf 0x0002 == 1 */ - assert_hit_count(trigger, 0, 2, 0); /* ResetNextIf 0x0003 == 1 (2) */ - assert_hit_count(trigger, 0, 3, 0); /* 0x0004 == 1 (1) */ - assert_hit_count(trigger, 0, 4, 0); /* Trigger 0x0000 == 1 */ - - /* non-trigger condition is true */ - ram[4] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - assert_hit_count(trigger, 0, 3, 1); - - /* second ResetNextIf is true */ - ram[3] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - assert_hit_count(trigger, 0, 2, 1); - assert_hit_count(trigger, 0, 3, 1); - - /* OrNext resets second ResetNextIf */ - ram[1] = 1; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); /* result is RESET */ - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); /* state is PRIMED */ - assert_hit_count(trigger, 0, 0, 1); /* OrNext tallies a hit of its own */ - assert_hit_count(trigger, 0, 1, 1); /* ResetNextIf gets a hit from the OrNext */ - assert_hit_count(trigger, 0, 2, 0); /* hit is reset by the ResetNextIf */ - assert_hit_count(trigger, 0, 3, 1); /* hit is not affected by the reset ResetNextIf */ - - /* OrNext no longer true */ - ram[1] = 0; - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_PRIMED); - assert_hit_count(trigger, 0, 0, 1); - assert_hit_count(trigger, 0, 1, 1); - assert_hit_count(trigger, 0, 2, 1); - assert_hit_count(trigger, 0, 3, 1); - - /* second ResetNextIf fires */ - ASSERT_NUM_EQUALS(evaluate_trigger(trigger, &memory), RC_TRIGGER_STATE_RESET); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_ACTIVE); - assert_hit_count(trigger, 0, 0, 1); - assert_hit_count(trigger, 0, 1, 1); - assert_hit_count(trigger, 0, 2, 2); - assert_hit_count(trigger, 0, 3, 0); -} - -static void test_prev_prior_share_memref() { - rc_trigger_t* trigger; - rc_memrefs_t* memrefs; - char buffer[512]; - - assert_parse_trigger(&trigger, buffer, "0xH0001=d0xH0001_0xH0001!=p0xH0001"); - - memrefs = rc_trigger_get_memrefs(trigger); - ASSERT_PTR_NOT_NULL(memrefs); - ASSERT_NUM_EQUALS(memrefs->memrefs.count, 1); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 1U); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); - - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand2.type, RC_OPERAND_DELTA); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand2.type, RC_OPERAND_PRIOR); -} - -static void test_bit_lookups_share_memref() { - rc_trigger_t* trigger; - rc_memrefs_t* memrefs; - char buffer[512]; - - assert_parse_trigger(&trigger, buffer, "0xM0001=1_0xN0x0001=0_0xO0x0001=1"); - - memrefs = rc_trigger_get_memrefs(trigger); - ASSERT_PTR_NOT_NULL(memrefs); - ASSERT_NUM_EQUALS(memrefs->memrefs.count, 1); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 1U); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); - - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.size, RC_MEMSIZE_BIT_0); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.size, RC_MEMSIZE_BIT_1); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 2)->operand1.size, RC_MEMSIZE_BIT_2); -} - -static void test_bitcount_shares_memref() { - rc_trigger_t* trigger; - rc_memrefs_t* memrefs; - char buffer[512]; - - assert_parse_trigger(&trigger, buffer, "0xH0001>5_0xK0001!=3"); - - memrefs = rc_trigger_get_memrefs(trigger); - ASSERT_PTR_NOT_NULL(memrefs); - ASSERT_NUM_EQUALS(memrefs->memrefs.count, 1); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 1U); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); - - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 0)->operand1.size, RC_MEMSIZE_8_BITS); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.type, RC_OPERAND_ADDRESS); - ASSERT_NUM_EQUALS(trigger_get_cond(trigger, 0, 1)->operand1.size, RC_MEMSIZE_BITCOUNT); -} - -static void test_large_memref_not_shared() { - rc_trigger_t* trigger; - rc_memrefs_t* memrefs; - char buffer[512]; - - assert_parse_trigger(&trigger, buffer, "0xH1234=1_0xX1234>d0xX1234"); - - memrefs = rc_trigger_get_memrefs(trigger); - ASSERT_PTR_NOT_NULL(memrefs); - - /* this could be shared, but isn't currently */ - ASSERT_NUM_EQUALS(memrefs->memrefs.count, 2); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].address, 0x1234U); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[0].value.size, RC_MEMSIZE_8_BITS); - - ASSERT_NUM_EQUALS(memrefs->memrefs.items[1].address, 0x1234U); - ASSERT_NUM_EQUALS(memrefs->memrefs.items[1].value.size, RC_MEMSIZE_32_BITS); -} - -static void test_remember_recall() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[256]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "K:1_{recall}=1(3)"); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - - /* condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - - /* condition is true - hit count should be incremented to reach target */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 1, 3U); - - /* condition is true - target previously met */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 1, 3U); -} - -static void test_remember_recall_separate_accumulator_per_group() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[640]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "K:1_{recall}=1.3.S{recall}=1.3.SK:1_K:{recall}*2_{recall}=2.5."); - - /* core group condition is true - hit count should be incremented */ - /* alt1 group condition is false since it's a different recall accumulator */ - /* alt2 group condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 2, 1U); - - /* core group condition is true - hit count should be incremented */ - /* alt group condition is false since it's a different recall accumulator */ - /* alt2 group condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 2, 2U); - - /* core group condition is true - hit count should be incremented to reach target */ - /* alt group condition is false since it's a different recall accumulator */ - /* alt2 group condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 3U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 2, 3U); - - /* core group condition is true - target previously met */ - /* alt group condition is false since it's a different recall accumulator */ - /* alt2 group condition is true - hit count should be incremented */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 3U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 2, 4U); - - /* core group condition is true - target previously met */ - /* alt group condition is false since it's a different recall accumulator */ - /* alt2 group condition is true - hit count incremented to reach target */ - /* core + alt2 now satisfied, trigger is true*/ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 1, 3U); - assert_hit_count(trigger, 1, 0, 0U); - assert_hit_count(trigger, 2, 2, 5U); -} - -static void test_remember_recall_separate_accumulator_per_group_complex() -{ - uint8_t ram[128]; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1280]; - - memory.ram = ram; - memory.size = sizeof(ram); - memset(ram, 0, sizeof(ram)); - - assert_parse_trigger(&trigger, buffer, "0=0SK:0x 0002&1023_K:{recall}*2_K:{recall}+4_{recall}=100SK:0x 0004&1023_K:{recall}*2_K:{recall}+4_{recall}=100"); - - /* $2=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ - /* $4=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ - assert_evaluate_trigger(trigger, &memory, 0); - - /* $2=0030 & 03FF = 0030 * 2 = 0060 + 4 = 0064 ?= 100 = true */ - /* $4=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ - ram[2] = 0x30; - assert_evaluate_trigger(trigger, &memory, 1); - - /* $2=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ - /* $4=0030 & 03FF = 0030 * 2 = 0060 + 4 = 0064 ?= 100 = true */ - ram[2] = 0x00; - ram[4] = 0x30; - assert_evaluate_trigger(trigger, &memory, 1); - - /* $2=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ - /* $4=0000 & 03FF = 0000 * 2 = 0000 + 4 = 0004 ?= 100 = false */ - ram[4] = 0x00; - assert_evaluate_trigger(trigger, &memory, 0); -} - -static void test_remember_recall_use_same_value_multiple() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[640]; - - memory.ram = ram; - memory.size = sizeof(ram); - - ram[0] = 1; - assert_parse_trigger(&trigger, buffer, "K:5_A:0xH00_C:{recall}=6_B:0xH00_C:{recall}=4_M:0=1.4."); - - /* because the recall accumulator can be re-used, both add hits are true and increment hits */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 2, 1U); - assert_hit_count(trigger, 0, 4, 1U); - ASSERT_NUM_EQUALS(trigger->measured_value, 2U); - ASSERT_NUM_EQUALS(trigger->measured_target, 4U); - - /* because the recall accumulator can be re-used, both add hits are true and increment hits to reach target*/ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 2, 2U); - assert_hit_count(trigger, 0, 4, 2U); - ASSERT_NUM_EQUALS(trigger->measured_value, 4U); - ASSERT_NUM_EQUALS(trigger->measured_target, 4U); - - /* condition is true - previously met */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 2, 3U); - assert_hit_count(trigger, 0, 4, 3U); - ASSERT_NUM_EQUALS(trigger->measured_value, 6U); - ASSERT_NUM_EQUALS(trigger->measured_target, 4U); -} - -static void test_remember_recall_in_pause_and_main() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[640]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "K:0xH00_{recall}<3.4._K:0xH00*2_{recall}>0xH01_K:0xH00*2_P:{recall}=2"); - - /* pause checks 0*2=2, not paused. Condition 2 gets hit since recalled value < 3 */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - - ram[0] = 1; - /* pause checks 1*2 = 2, pause active. condition 2 does not get new hit */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 1U); - - ram[0] = 2; - /* pause checks 2*2 = 2, pause inactive. condition 2 dgets hit because < 3 */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - - ram[0] = 3; - /* pause checks 2*2 = 2, pause inactive. condition 2 gets no because = 3 */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 2U); - - ram[0] = 0; - /* pause checks 0*2=2, not paused. Condition 2 gets hit since recalled value < 3 */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 3U); - - ram[0] = 1; - /* pause checks 1*2 = 2, pause active. condition 2 does not get new hit */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 3U); - - ram[0] = 2; - /* pause checks 2*2 = 2, pause inactive. condition 2 dgets hit because < 3, not true because condition 4 untrue */ - assert_evaluate_trigger(trigger, &memory, 0); - assert_hit_count(trigger, 0, 1, 4U); - - ram[0] = 10; - /* condition is true - hits on condition 2 previously met, no active pause. */ - assert_evaluate_trigger(trigger, &memory, 1); - assert_hit_count(trigger, 0, 1, 4U); -} - -static void test_remember_recall_in_pause_with_chain() -{ - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - rc_condition_t* condition; - char buffer[640]; - - memory.ram = ram; - memory.size = sizeof(ram); - - assert_parse_trigger(&trigger, buffer, "I:{recall}_I:0xH02_0xH03=10_K:0xH00_P:0=1"); - - /* ensure recall memrefs point at the remember for the pause chain */ - condition = trigger->requirement->conditions; - ASSERT_NUM_EQUALS(condition->operand1.type, RC_OPERAND_RECALL); - ASSERT_PTR_NOT_NULL(condition->operand1.value.memref); - - condition = condition->next; - ASSERT_NUM_EQUALS(condition->operand1.value.memref->value.memref_type, RC_MEMREF_TYPE_MODIFIED_MEMREF); - ASSERT_NUM_EQUALS(((rc_modified_memref_t*)condition->operand1.value.memref)->parent.type, RC_OPERAND_RECALL); - ASSERT_PTR_NOT_NULL(((rc_modified_memref_t*)condition->operand1.value.memref)->parent.value.memref); - - /* byte(0)=0, remember. byte(0+2)=0x34, byte(0x34+3)=n/a */ - assert_evaluate_trigger(trigger, &memory, 0); - - ram[2] = 1; - /* byte(0)=0, remember. byte(0+2)=1, byte(1+3)=0x56 */ - assert_evaluate_trigger(trigger, &memory, 0); - - ram[4] = 10; - /* byte(0)=0, remember. byte(0+2)=1, byte(1+3)=10 */ - assert_evaluate_trigger(trigger, &memory, 1); -} - -static void test_remember_recall_in_addaddress() -{ - uint8_t ram[] = { 0x02, 0x03, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(byte(0x0001) + (byte(0x0000) - 1) * 2) == 60 */ - assert_parse_trigger(&trigger, buffer, "K:0xH0000-1_K:{recall}*2_I:{recall}+0xH0001_0xH0000=60"); - - /* byte(3 + (2 - 1) * 2) => byte(3+2) => byte(5) == 60 */ - assert_evaluate_trigger(trigger, &memory, 0); - - /* condition is true */ - ram[5] = 60; - assert_evaluate_trigger(trigger, &memory, 1); - - /* byte(3 + (3 - 1) * 2) => byte(3+4) => byte(7) == 60 */ - ram[0] = 3; - assert_evaluate_trigger(trigger, &memory, 0); - - /* condition is true */ - ram[7] = 60; - assert_evaluate_trigger(trigger, &memory, 1); - - /* byte(0 + (3 - 1) * 2) => byte(0+4) => byte(4) == 60 */ - ram[1] = 0; - assert_evaluate_trigger(trigger, &memory, 0); - - /* condition is true */ - ram[4] = 60; - assert_evaluate_trigger(trigger, &memory, 1); -} - -static void test_remember_recall_self_in_addaddress() -{ - uint8_t ram[] = { 0x02, 0x03, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[1024]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(byte(0x0000) + 1) == 60 */ - assert_parse_trigger(&trigger, buffer, "K:0xH0000_K:{recall}_I:{recall}_0xH0001=60"); - - /* byte(2 + 1) => byte(3) == 60 */ - assert_evaluate_trigger(trigger, &memory, 0); - - /* condition is true */ - ram[3] = 60; - assert_evaluate_trigger(trigger, &memory, 1); - - /* byte(3 + 1) => byte(4) == 60 */ - ram[0] = 3; - assert_evaluate_trigger(trigger, &memory, 0); - - /* condition is true */ - ram[4] = 60; - assert_evaluate_trigger(trigger, &memory, 1); - - /* byte(2 + 1) => byte(3) == 60 */ - ram[0] = 2; - assert_evaluate_trigger(trigger, &memory, 1); - - /* condition is false */ - ram[3] = 9; - assert_evaluate_trigger(trigger, &memory, 0); -} - -/* ======================================================== */ - -static void test_trailing_andnext() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0002)=1 - * AndNext byte(0x0001)=1 -- ignored as compound condition is incomplete - */ - assert_parse_trigger(&trigger, buffer, "0xH0002=1_N:0xH0001=1"); - ASSERT_PTR_NOT_NULL(trigger->requirement->conditions); - ASSERT_PTR_NOT_NULL(trigger->requirement->conditions->next); - ASSERT_PTR_NULL(trigger->requirement->conditions->next->next); - - /* both conditions false */ - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition true */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition not true */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition true */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - - /* first condition true */ - ram[2] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - - /* first condition not true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition not true */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - - /* first condition true */ - ram[2] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - - /* first condition not true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 0); -} - -static void test_trailing_andnext_with_alt() { - uint8_t ram[] = { 0x00, 0x12, 0x34, 0xAB, 0x56 }; - memory_t memory; - rc_trigger_t* trigger; - char buffer[512]; - - memory.ram = ram; - memory.size = sizeof(ram); - - /* byte(0x0002)=1 - * AndNext byte(0x0001)=1 -- ignored as compound condition is incomplete - */ - assert_parse_trigger(&trigger, buffer, "0xH0002=1_N:0xH0001=1SR:0xH0003=1"); - ASSERT_PTR_NOT_NULL(trigger->requirement->conditions); - ASSERT_PTR_NOT_NULL(trigger->requirement->conditions->next); - ASSERT_PTR_NULL(trigger->requirement->conditions->next->next); - ASSERT_PTR_NOT_NULL(trigger->alternative->conditions); - ASSERT_PTR_NULL(trigger->alternative->conditions->next); - - /* both conditions false */ - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition true */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition not true */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition true */ - ram[1] = 1; - assert_evaluate_trigger(trigger, &memory, 0); - - /* first condition true */ - ram[2] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - - /* first condition not true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - - /* second condition not true */ - ram[1] = 2; - assert_evaluate_trigger(trigger, &memory, 0); - - /* first condition true */ - ram[2] = 1; - assert_evaluate_trigger(trigger, &memory, 1); - - /* first condition not true */ - ram[2] = 2; - assert_evaluate_trigger(trigger, &memory, 0); -} - -/* ======================================================== */ - -void test_trigger(void) { - TEST_SUITE_BEGIN(); - - /* alt groups */ - TEST(test_alt_groups); - TEST(test_empty_core); - TEST(test_empty_alt); - TEST(test_empty_last_alt); - TEST(test_empty_all_alts); - - /* resetif */ - TEST(test_resetif_in_alt_group); - - /* pauseif */ - TEST(test_pauseif_in_alt_group); - TEST(test_pauseif_resetif_in_alt_group); - TEST(test_pauseif_hitcount_with_reset); - - /* measured */ - TEST(test_measured); - TEST(test_measured_as_percent); - TEST(test_measured_comparison); - TEST(test_measured_addhits); - TEST(test_measured_indirect); - TEST(test_measured_multiple); - TEST(test_measured_multiple_with_hitcount_in_core); - TEST(test_measured_while_paused); - TEST(test_measured_while_paused_multiple); - TEST(test_measured_while_paused_reset_alt); - TEST(test_measured_while_paused_reset_core); - TEST(test_measured_while_paused_reset_non_hitcount); - TEST(test_measured_while_paused_extra_alts); - TEST(test_measured_reset_hitcount); - TEST(test_measured_reset_comparison); - TEST(test_measured_if); - TEST(test_measured_if_comparison); - TEST(test_measured_if_multiple_measured); - TEST(test_measured_if_multiple_measured_if); - TEST(test_measured_if_while_paused); - TEST(test_measured_trigger); - - /* trigger */ - TEST(test_resetnextif_trigger); - - /* rc_evaluate_trigger */ - TEST(test_evaluate_trigger_inactive); - TEST(test_evaluate_trigger_waiting); - TEST(test_evaluate_trigger_reset); - TEST(test_evaluate_trigger_reset_next); - TEST(test_evaluate_trigger_triggered); - TEST(test_evaluate_trigger_paused); - TEST(test_evaluate_trigger_primed); - TEST(test_evaluate_trigger_primed_in_alts); - TEST(test_evaluate_trigger_primed_one_alt); - TEST(test_evaluate_trigger_disabled); - TEST(test_evaluate_trigger_chained_resetnextif); - - /* memref sharing */ - TEST(test_prev_prior_share_memref); - TEST(test_bit_lookups_share_memref); - TEST(test_bitcount_shares_memref); - TEST(test_large_memref_not_shared); - - /* accumulator - remember and recall*/ - TEST(test_remember_recall); - TEST(test_remember_recall_separate_accumulator_per_group); - TEST(test_remember_recall_separate_accumulator_per_group_complex); - TEST(test_remember_recall_use_same_value_multiple); - TEST(test_remember_recall_in_pause_and_main); - TEST(test_remember_recall_in_pause_with_chain); - TEST(test_remember_recall_in_addaddress); - TEST(test_remember_recall_self_in_addaddress); - - /* incomplete logic */ - TEST(test_trailing_andnext); - TEST(test_trailing_andnext_with_alt); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rcheevos/test_value.c b/src/rcheevos/test/rcheevos/test_value.c deleted file mode 100644 index 0cffe9bf46..0000000000 --- a/src/rcheevos/test/rcheevos/test_value.c +++ /dev/null @@ -1,868 +0,0 @@ -#include "rc_internal.h" - -#include "../test_framework.h" -#include "mock_memory.h" - -static void test_evaluate_value(const char* memaddr, int expected_value) { - rc_value_t* self; - /* bytes 5-8 are the float value for pi */ - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56, 0xDB, 0x0F, 0x49, 0x40}; - memory_t memory; - char buffer[2048]; - unsigned* overflow; - int ret; - - memory.ram = ram; - memory.size = sizeof(ram); - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - overflow = (unsigned*)(((char*)buffer) + ret); - *overflow = 0xCDCDCDCD; - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } - - ret = rc_evaluate_value(self, peek, &memory, NULL); - ASSERT_NUM_EQUALS(ret, expected_value); -} - -static void test_invalid_value(const char* memaddr, int expected_error) { - int ret = rc_value_size(memaddr); - ASSERT_NUM_EQUALS(ret, expected_error); -} - -static void test_measured_value_target(const char* memaddr, int expected_target) { - rc_value_t* self; - char buffer[2048]; - unsigned* overflow; - int ret; - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - overflow = (unsigned*)(((char*)buffer) + ret); - *overflow = 0xCDCDCDCD; - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - if (*overflow != 0xCDCDCDCD) { - ASSERT_FAIL("write past end of buffer"); - } - - ASSERT_NUM_EQUALS(self->conditions->conditions->required_hits, expected_target); -} - -static void test_evaluate_measured_value_with_pause() { - rc_value_t* self; - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - char buffer[2048]; - const char* memaddr = "P:0xH0003=hAB_M:0xH0002!=d0xH0002"; - int ret; - - memory.ram = ram; - memory.size = sizeof(ram); - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - - /* should initially be paused, no hits captured */ - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* pause should prevent hitcount */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* unpause should not report the change that occurred while paused */ - ram[3] = 0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* hitcount should be captured */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* pause should return current hitcount */ - ram[3] = 0xAB; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* pause should prevent hitcount */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* unpause should not report the change that occurred while paused */ - ram[3] = 0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* additional hitcount should be captured */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 2); -} - -static void test_evaluated_and_next_measured_if_value() { - rc_value_t* self; - const rc_condition_t* cond2; - const rc_condition_t* cond4; - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - char buffer[2048]; - const char* memaddr = "R:0xH0004=1_N:0xH0000=1.1._N:d0xH0000=9_Q:0xH0000=0.1._M:100"; - int ret; - - memory.ram = ram; - memory.size = sizeof(ram); - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - - cond2 = self->conditions->conditions->next; - cond4 = cond2->next->next; - - /* measured if cannot be true */ - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - ASSERT_NUM_EQUALS(cond2->current_hits, 0); - ASSERT_NUM_EQUALS(cond4->current_hits, 0); - - /* capture first hit, measured_if still not true */ - ram[0] = 1; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - ASSERT_NUM_EQUALS(cond2->current_hits, 1); - ASSERT_NUM_EQUALS(cond4->current_hits, 0); - - /* reset */ - ram[4] = 1; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - ASSERT_NUM_EQUALS(cond2->current_hits, 0); - ASSERT_NUM_EQUALS(cond4->current_hits, 0); - - /* clear reset */ - ram[4] = 0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - ASSERT_NUM_EQUALS(cond2->current_hits, 1); - ASSERT_NUM_EQUALS(cond4->current_hits, 0); - - /* prime measured_if */ - ram[0] = 9; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - ASSERT_NUM_EQUALS(cond2->current_hits, 1); - ASSERT_NUM_EQUALS(cond4->current_hits, 0); - - /* trigger measured if */ - ram[0] = 0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 100); - ASSERT_NUM_EQUALS(cond2->current_hits, 1); - ASSERT_NUM_EQUALS(cond4->current_hits, 1); - - /* measured if should remain triggered */ - ram[0] = 1; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 100); - ASSERT_NUM_EQUALS(cond2->current_hits, 1); - ASSERT_NUM_EQUALS(cond4->current_hits, 1); - - /* reset */ - ram[4] = 1; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - ASSERT_NUM_EQUALS(cond2->current_hits, 0); - ASSERT_NUM_EQUALS(cond4->current_hits, 0); -} - -static void test_evaluate_measured_value_with_reset() { - rc_value_t* self; - uint8_t ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; - memory_t memory; - char buffer[2048]; - const char* memaddr = "R:0xH0003=hAB_M:0xH0002!=d0xH0002"; - int ret; - - memory.ram = ram; - memory.size = sizeof(ram); - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - - /* reset should initially be true, no hits captured */ - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* reset should prevent hitcount */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* reset no longer true, change while reset shouldn't be captured */ - ram[3] = 0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* additional hitcount should be captured */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* reset should clear hit count */ - ram[3] = 0xAB; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* reset should prevent hitcount */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* reset no longer true, change while reset shouldn't be captured */ - ram[3] = 0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* additional hitcount should be captured */ - ram[2]++; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); -} - -static void init_typed_value(rc_typed_value_t* value, uint8_t type, uint32_t u32, double f32) { - value->type = type; - - switch (type) { - case RC_VALUE_TYPE_UNSIGNED: - value->value.u32 = u32; - break; - - case RC_VALUE_TYPE_SIGNED: - value->value.i32 = (int)u32; - break; - - case RC_VALUE_TYPE_FLOAT: - value->value.f32 = (float)f32; - break; - - case RC_VALUE_TYPE_NONE: - value->value.u32 = 0xCDCDCDCD; /* force uninitialized value */ - break; - - default: - break; - } -} - -static void _assert_typed_value(const rc_typed_value_t* value, uint8_t type, uint32_t u32, double f32) { - ASSERT_NUM_EQUALS(value->type, type); - - switch (type) { - case RC_VALUE_TYPE_UNSIGNED: - ASSERT_NUM_EQUALS(value->value.u32, u32); - break; - - case RC_VALUE_TYPE_SIGNED: - ASSERT_NUM_EQUALS(value->value.i32, (int)u32); - break; - - case RC_VALUE_TYPE_FLOAT: - ASSERT_FLOAT_EQUALS(value->value.f32, (float)f32); - break; - - default: - break; - } -} -#define assert_typed_value(value, type, u32, f32) ASSERT_HELPER(_assert_typed_value(value, type, u32, f32), "assert_typed_value") - -static void test_typed_value_convert(uint8_t type, uint32_t u32, double f32, uint8_t new_type, uint32_t new_u32, double new_f32) { - rc_typed_value_t value; - init_typed_value(&value, type, u32, f32); - - rc_typed_value_convert(&value, new_type); - - assert_typed_value(&value, new_type, new_u32, new_f32); -} - -static void test_typed_value_conversion() { - /* unsigned source */ - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4294967295.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 12, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* signed source */ - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_FLOAT, 0, -1.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, 12, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* float source (whole numbers) */ - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_UNSIGNED, 12, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_SIGNED, (unsigned)-1, 0.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_FLOAT, 0, -1.0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 12.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* float source (non-whole numbers) */ - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 3.14159); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 3, 0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, 3, 0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0); - - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_FLOAT, 0, -3.14159); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFD, 0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-3, 0); - TEST_PARAMS6(test_typed_value_convert, RC_VALUE_TYPE_FLOAT, 0, -3.14159, RC_VALUE_TYPE_NONE, 0, 0); -} - -static void test_typed_value_add(uint8_t type, uint32_t u32, double f32, - uint8_t amount_type, uint32_t amount_u32, double amount_f32, uint32_t result_u32, double result_f32) { - rc_typed_value_t value, amount; - - init_typed_value(&value, type, u32, f32); - init_typed_value(&amount, amount_type, amount_u32, amount_f32); - - rc_typed_value_add(&value, &amount); - - if (result_f32 != 0.0) { - assert_typed_value(&value, RC_VALUE_TYPE_FLOAT, result_u32, result_f32); - } - else if (type == RC_VALUE_TYPE_NONE) { - assert_typed_value(&value, amount_type, result_u32, result_f32); - } - else { - assert_typed_value(&value, type, result_u32, result_f32); - } -} - -static void test_typed_value_addition() { - /* no source */ - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 8, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, 0, 8.0); - - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 0xFFFFFFFE, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, (unsigned)-2, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, 0, -2.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 0.0); - - /* unsigned source */ - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 14, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, 0, 14.0); - - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 4, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 4, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, 0, 4.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, 6, 0.0); - - /* signed source */ - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, (unsigned)-4, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, -4.0); - - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, 2, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, (unsigned)-8, 0.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, -4.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, (unsigned)-6, 0.0); - - /* float source (whole numbers) */ - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, 0, 8.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, 8.0); - - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, 0, 4294967300.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 0, 4.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, 0, -2.0); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 6.0); - - /* float source (non-whole numbers) */ - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, 0, 5.14159); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, 0, 1.14159); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, 0, 5.14159); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, 0, 9.16459); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, 0, -4.85841); - TEST_PARAMS8(test_typed_value_add, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0.0, 0, 3.14159); -} - -static void test_typed_value_multiply(char type, uint32_t u32, double f32, - uint8_t amount_type, uint32_t amount_u32, double amount_f32, - uint8_t result_type, uint32_t result_u32, double result_f32) { - rc_typed_value_t value, amount; - - init_typed_value(&value, type, u32, f32); - init_typed_value(&amount, amount_type, amount_u32, amount_f32); - - rc_typed_value_multiply(&value, &amount); - - assert_typed_value(&value, result_type, result_u32, result_f32); -} - -static void test_typed_value_multiplication() { - /* unsigned source */ - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 8, 0.0, RC_VALUE_TYPE_UNSIGNED, 48, 0.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 8.0, RC_VALUE_TYPE_FLOAT, 0, 48.0); - - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFF4, 0.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFF4, 0.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* signed source */ - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-12, 0.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); - - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_SIGNED, 12, 0.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* float source (whole numbers) */ - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 12.0); - - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 25769803764.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -12.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -48.0); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* float source (non-whole numbers) */ - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 6.28318); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -6.28318); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 6.28318); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 18.92179657); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -25.13272); - TEST_PARAMS9(test_typed_value_multiply, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); -} - -static void test_typed_value_divide(uint8_t type, uint32_t u32, double f32, - uint8_t amount_type, uint32_t amount_u32, double amount_f32, - uint8_t result_type, uint32_t result_u32, double result_f32) { - rc_typed_value_t value, amount; - - init_typed_value(&value, type, u32, f32); - init_typed_value(&amount, amount_type, amount_u32, amount_f32); - - rc_typed_value_divide(&value, &amount); - - assert_typed_value(&value, result_type, result_u32, result_f32); -} - -static void test_typed_value_division() { - /* division by zero */ - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* unsigned source */ - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_UNSIGNED, 3, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.5, RC_VALUE_TYPE_FLOAT, 0, 2.4); - - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); - - /* signed source */ - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-3, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); - - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_SIGNED, 3, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_SIGNED, 3, 0.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); - - /* float source (whole numbers) */ - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 3.0); - - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.00000000139698386); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -3.0); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -0.75); - - /* float source (non-whole numbers) */ - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 1.570795); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, -1.570795); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 1.570795); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 0.52159887099); - TEST_PARAMS9(test_typed_value_divide, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -8.0, RC_VALUE_TYPE_FLOAT, 0, -0.39269875); -} - -static void test_typed_value_mod(uint8_t type, uint32_t u32, double f32, - uint8_t amount_type, uint32_t amount_u32, double amount_f32, - uint8_t result_type, uint32_t result_u32, double result_f32) { - rc_typed_value_t value, amount; - - init_typed_value(&value, type, u32, f32); - init_typed_value(&amount, amount_type, amount_u32, amount_f32); - - rc_typed_value_modulus(&value, &amount); - - assert_typed_value(&value, result_type, result_u32, result_f32); -} - -static void test_typed_value_modulus() { - /* modulus with zero divisor */ - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_NONE, 0, 0.0); - - /* unsigned source */ - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 4, 0.0, RC_VALUE_TYPE_UNSIGNED, 2, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.25, RC_VALUE_TYPE_FLOAT, 0, 1.5); - - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFC, 0.0, RC_VALUE_TYPE_UNSIGNED, 6, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-4, 0.0, RC_VALUE_TYPE_UNSIGNED, 6, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_UNSIGNED, 6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -4.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); - - /* signed source */ - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 4, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, 4.0, RC_VALUE_TYPE_FLOAT, 0, -2.0); - - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFC, 0.0, RC_VALUE_TYPE_SIGNED, -2, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_SIGNED, (unsigned)-4, 0.0, RC_VALUE_TYPE_SIGNED, -2, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_SIGNED, (unsigned)-6, 0.0, RC_VALUE_TYPE_FLOAT, 0, -4.0, RC_VALUE_TYPE_FLOAT, 0, -2.0); - - /* float source (whole numbers) */ - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 4, 0.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, 4.0, RC_VALUE_TYPE_FLOAT, 0, 2.0); - - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFE, 0.0, RC_VALUE_TYPE_FLOAT, 0, 6.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_SIGNED, (unsigned)-2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 6.0, RC_VALUE_TYPE_FLOAT, 0, -5.0, RC_VALUE_TYPE_FLOAT, 0, 1.0); - - /* float source (non-whole numbers) */ - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_UNSIGNED, 2, 0.0, RC_VALUE_TYPE_FLOAT, 0, 1.141590); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_SIGNED, (unsigned)-4, 0.0, RC_VALUE_TYPE_FLOAT, 0, 3.141590); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 2.0, RC_VALUE_TYPE_FLOAT, 0, 1.141590); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 1.570795, RC_VALUE_TYPE_FLOAT, 0, 0.0); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, 6.023, RC_VALUE_TYPE_FLOAT, 0, 3.141590); - TEST_PARAMS9(test_typed_value_mod, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -2.0, RC_VALUE_TYPE_FLOAT, 0, 1.141590); -} - -static void test_typed_value_negate(char type, int i32, double f32, char expected_type, signed result_i32, double result_f32) { - rc_typed_value_t value; - - init_typed_value(&value, type, (unsigned)i32, f32); - - rc_typed_value_negate(&value); - - assert_typed_value(&value, expected_type, (unsigned)result_i32, result_f32); -} - -static void test_typed_value_negation() { - /* unsigned source */ - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 99, 0.0, RC_VALUE_TYPE_SIGNED, -99, 0.0); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_UNSIGNED, 0xFFFFFFFF, 0.0, RC_VALUE_TYPE_SIGNED, 1, 0.0); - - /* signed source */ - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, 0, 0.0, RC_VALUE_TYPE_SIGNED, 0, 0.0); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, 99, 0.0, RC_VALUE_TYPE_SIGNED, -99, 0.0); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_SIGNED, -1, 0.0, RC_VALUE_TYPE_SIGNED, 1, 0.0); - - /* float source (whole numbers) */ - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 0.0, RC_VALUE_TYPE_FLOAT, 0, 0.0); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 99.0, RC_VALUE_TYPE_FLOAT, 0, -99.0); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, -1.0, RC_VALUE_TYPE_FLOAT, 0, 1.0); - - /* float source (non-whole numbers) */ - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 0.1, RC_VALUE_TYPE_FLOAT, 0, -0.1); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, 3.14159, RC_VALUE_TYPE_FLOAT, 0, -3.14159); - TEST_PARAMS6(test_typed_value_negate, RC_VALUE_TYPE_FLOAT, 0, -2.7, RC_VALUE_TYPE_FLOAT, 0, 2.7); -} - -static void test_addhits_float_coercion() { - rc_value_t* self; - uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F }; /* fF0004 = 1.5 */ - memory_t memory; - char buffer[2048]; - /* measured(tally(0, (0 + float(4) * 10 - prev(float(4)) * 10) == 1)) */ - const char* memaddr = "A:0_A:fF0004*10_B:dfF0004*10_C:0=1_M:0=1"; - int ret; - - memory.ram = ram; - memory.size = sizeof(ram); - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - - /* The 0+ at the start of the expression changes the accumulator to an integer, - * so when float(4)*10 is added and prev(float(4))*10 is subtracted, they'll also - * be converted to integers before they're combined. - */ - - /* float(4) = 1.5, prev(float(4)) = 0.0. 0+15-0=1 is false => 0 */ - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* float(4) = 1.75, prev(float(4)) = 1.5. 0+17-15 => 2 => 2=1 is false => 0 */ - ram[7] = 0x3f; ram[6] = 0xe0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* float(4) = 1.82, prev(float(4)) = 1.75. 0+18-17 => 1 => 1=1 is true => 1 */ - ram[6] = 0xe8; ram[5] = 0xf5; ram[4] = 0xc3; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* float(4) = 2.06, prev(float(4)) = 1.82. 0+20-18 => 2 => 2=1 is false => 1 */ - ram[7] = 0x40; ram[6] = 0x03; ram[5] = 0xd7; ram[4] = 0x0a; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); -} - -static void test_addhits_float_coercion_remembered() { - rc_value_t* self; - uint8_t ram[] = { 0x00, 0x06, 0x34, 0xAB, 0x00, 0x00, 0xC0, 0x3F }; /* fF0004 = 1.5 */ - memory_t memory; - char buffer[2048]; - /* measured(tally(0, remembered(0 - prev(float(4)) * 10) + (0 + float(4) * 10) == 1)) */ - const char* memaddr = "A:0_B:dfF0004*10_K:0_A:0_A:fF0004*10_C:{recall}=1_M:0=1"; - int ret; - - memory.ram = ram; - memory.size = sizeof(ram); - - ret = rc_value_size(memaddr); - ASSERT_NUM_GREATER_EQUALS(ret, 0); - - self = rc_parse_value(buffer, memaddr, NULL, 0); - ASSERT_PTR_NOT_NULL(self); - - /* using remember allows for both sides to explicitly be cast to integer before - * performing the subtraction. */ - - /* float(4) = 1.5, prev(float(4)) = 0.0. 15-0 => 15=1 is false => 0 */ - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* float(4) = 1.75, prev(float(4)) = 1.5. 17-15 => 2=1 is false => 0 */ - ram[7] = 0x3f; ram[6] = 0xe0; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 0); - - /* float(4) = 1.82, prev(float(4)) = 1.75. 18-17 => 1=1 is true => 1 */ - ram[6] = 0xe8; ram[5] = 0xf5; ram[4] = 0xc3; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); - - /* float(4) = 2.06, prev(float(4)) = 1.82. 20-18 => 2=1 is false => 1 */ - ram[7] = 0x40; ram[6] = 0x03; ram[5] = 0xd7; ram[4] = 0x0a; - ASSERT_NUM_EQUALS(rc_evaluate_value(self, peek, &memory, NULL), 1); -} - -void test_value(void) { - TEST_SUITE_BEGIN(); - - /* ram[] = {0x00, 0x12, 0x34, 0xAB, 0x56}; */ - - /* classic format - supports multipliers, max, inversion */ - TEST_PARAMS2(test_evaluate_value, "V6", 6); - TEST_PARAMS2(test_evaluate_value, "V6*2", 12); - TEST_PARAMS2(test_evaluate_value, "V6*0.5", 3); - TEST_PARAMS2(test_evaluate_value, "V-6", -6); - TEST_PARAMS2(test_evaluate_value, "V-6*2", -12); - - TEST_PARAMS2(test_evaluate_value, "0xH0001_0xH0002", 0x12 + 0x34); - TEST_PARAMS2(test_evaluate_value, "0xH0001*100_0xH0002*0.5_0xL0003", 0x12 * 100 + 0x34 / 2 + 0x0B); - TEST_PARAMS2(test_evaluate_value, "0xH0001$0xH0002", 0x34); - TEST_PARAMS2(test_evaluate_value, "0xH0001_0xH0004*3$0xH0002*0xL0003", 0x34 * 0x0B); - TEST_PARAMS2(test_evaluate_value, "0xH0001_V-20", 0x12 - 20); - TEST_PARAMS2(test_evaluate_value, "0xH0001_H10", 0x12 + 0x10); - TEST_PARAMS2(test_evaluate_value, "100-0xH0002", 100 - 0x34); - TEST_PARAMS2(test_evaluate_value, "0xh0000*-1_99_0xh0001*-100_5900", 4199); - TEST_PARAMS2(test_evaluate_value, "v5900_0xh0000*-1.0_0xh0001*-100.0", 4100); - TEST_PARAMS2(test_evaluate_value, "v5900_0xh0000*v-1_0xh0001*v-100", 4100); - - TEST_PARAMS2(test_evaluate_value, "0xH01*4", 0x12 * 4); /* multiply by constant */ - TEST_PARAMS2(test_evaluate_value, "0xH01*0.5", 0x12 / 2); /* multiply by fraction */ - TEST_PARAMS2(test_evaluate_value, "0xH01/2", 0x12 / 2); /* divide by constant */ - TEST_PARAMS2(test_evaluate_value, "0xH01*0xH02", 0x12 * 0x34); /* multiply by second address */ - TEST_PARAMS2(test_evaluate_value, "0xH01*0xT02", 0); /* multiply by bit */ - TEST_PARAMS2(test_evaluate_value, "0xH01*~0xT02", 0x12); /* multiply by inverse bit */ - TEST_PARAMS2(test_evaluate_value, "0xH01*~0xH02", 0x12 * (0x34 ^ 0xff)); /* multiply by inverse byte */ - - TEST_PARAMS2(test_evaluate_value, "B0xH01", 12); - TEST_PARAMS2(test_evaluate_value, "B0x00001", 3412); - TEST_PARAMS2(test_evaluate_value, "B0xH03", 111); /* 0xAB not really BCD */ - TEST_PARAMS2(test_evaluate_value, "B0xW00", 341200); - - /* non-comparison measured values just return the value at the address and have no target */ - TEST_PARAMS2(test_measured_value_target, "M:0xH0002", 0); - - /* hitcount based measured values always have unbounded targets, even if one is specified */ - TEST_PARAMS2(test_measured_value_target, "M:0xH0002!=d0xH0002", (unsigned)-1); - TEST_PARAMS2(test_measured_value_target, "M:0xH0002!=d0xH0002.99.", (unsigned)-1); - /* measured values always assumed to be hitcount based - they do not stop/trigger when the condition is met */ - TEST_PARAMS2(test_measured_value_target, "M:0xH0002<100", (unsigned)-1); - - /* measured format - supports hit counts and combining flags - * (AddSource, SubSource, AddHits, SubHits, AndNext, OrNext, and AddAddress) */ - TEST_PARAMS2(test_evaluate_value, "M:0xH0002", 0x34); - TEST_PARAMS2(test_evaluate_value, "A:0xH0001_M:0xH0002", 0x12 + 0x34); - TEST_PARAMS2(test_evaluate_value, "B:0xH0001_M:0xH0002", 0x34 - 0x12); - TEST_PARAMS2(test_evaluate_value, "C:0xH0000=0_M:0xH0002=52", 2); - TEST_PARAMS2(test_evaluate_value, "C:0xH0000=0_D:0xH0001=18_M:0xH0002=52", 1); - TEST_PARAMS2(test_evaluate_value, "N:0xH0000=0_M:0xH0002=52", 1); - TEST_PARAMS2(test_evaluate_value, "O:0xH0000=0_M:0xH0002=0", 1); - TEST_PARAMS2(test_evaluate_value, "I:0xH0000_M:0xH0002", 0x34); - - TEST_PARAMS2(test_evaluate_value, "M:0xH0002*2", 0x34 * 2); - TEST_PARAMS2(test_evaluate_value, "M:0xH0002/2", 0x34 / 2); - TEST_PARAMS2(test_evaluate_value, "M:0xH0002%2", 0x34 % 2); - TEST_PARAMS2(test_evaluate_value, "M:0xH0001%3", 0x12 % 3); - TEST_PARAMS2(test_evaluate_value, "A:0xH0001*2_A:0xH0002*2_M:0", 0x12 * 2 + 0x34 * 2); - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%3_A:0xH0002%4_M:0", 0x12 % 3 + 0x34 % 4); - TEST_PARAMS2(test_evaluate_value, "A:0xH0001*2_M:0xH0002*2", 0x12 * 2 + 0x34 * 2); /* multiplier in final condition */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001/2_M:0xH0002/2", 0x12 / 2 + 0x34 / 2); - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%3_M:0xH0002%4", 0x12 % 3 + 0x34 % 4); - TEST_PARAMS2(test_evaluate_value, "A:0xH0001&15_M:0xH0002&15", (0x12 & 15) + (0x34 & 15)); - - /* measured format does not support alt groups */ - TEST_PARAMS2(test_invalid_value, "M:0xH0002=6SM:0xH0003=6", RC_INVALID_VALUE_FLAG); - /* does not start with X:, so legacy parser says it's an invalid memory accessor */ - TEST_PARAMS2(test_invalid_value, "SM:0xH0002=6SM:0xH0003=6", RC_INVALID_MEMORY_OPERAND); - - /* measured format does not support trigger flag */ - TEST_PARAMS2(test_invalid_value, "T:0xH0002=6", RC_INVALID_VALUE_FLAG); - - /* measured format requires a measured condition */ - TEST_PARAMS2(test_invalid_value, "A:0xH0002_0xH0003>10.99.", RC_INVALID_VALUE_FLAG); /* no flag on condition 2 */ - TEST_PARAMS2(test_invalid_value, "A:0xH0002_A:0xH0003", RC_MISSING_VALUE_MEASURED); - - /* measured value with float data */ - TEST_PARAMS2(test_evaluate_value, "M:fF0005", 3); /* 3.141592 -> 3 */ - TEST_PARAMS2(test_evaluate_value, "A:fF0005*10_M:0", 31); /* 3.141592 x 10 -> 31.415 -> 31 */ - TEST_PARAMS2(test_evaluate_value, "A:fF0005*f11.2_M:f6.9", 42); /* 3.141592 x 11.2 -> 35.185 + 6.9 -> -> 42.085 -> 42 */ - TEST_PARAMS2(test_evaluate_value, "A:fF0005*f5.555555555555555555555555555555555555555555555556_M:f6.9", 24); /* 3.141592 x 5.555556 -> 17.4532902 + 6.9 -> -> 24.353290 -> 24 */ - - /* delta should initially be 0, so a hit will be tallied */ - TEST_PARAMS2(test_evaluate_value, "M:0xH0002!=d0xH0002", 1); - - /* division */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001/2_M:0", 9); /* 18/2 = 9 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001/5_M:0", 3); /* 18/5 = 3 */ - TEST_PARAMS2(test_evaluate_value, "M:0xH0001/5", 3); /* 18/5 = 3 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0002/0xH0001_M:0", 2); /* 52/18 = 2 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0002_M:0", 0); /* 18/52 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0001_M:0", 1); /* 18/18 = 1 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001/0xH0000_M:0", 0); /* 18/0 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0000/0xH0000_M:0", 0); /* 0/0 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0000/0xH0001_M:0", 0); /* 0/18 = 0 */ - - /* modulus */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%3_M:0", 0); /* 18%3 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%5_M:0", 3); /* 18%5 = 3 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%7_M:0", 4); /* 18%7 = 4 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0002%0xH0001_M:0", 16); /* 52%18 = 16 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%0xH0002_M:0", 18); /* 18%52 = 18 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%0xH0001_M:0", 0); /* 18%18 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001%0xH0000_M:0", 0); /* 18%0 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0000%0xH0000_M:0", 0); /* 0%0 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0000%0xH0001_M:0", 0); /* 0%18 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0000%0xH0001_M:0", 0); /* 0%18 = 0 */ - TEST_PARAMS2(test_evaluate_value, "A:f5.5%f2.0_M:0", 1) /* 5.5 % 2.0 = 1.5 -> 1 */ - TEST_PARAMS2(test_evaluate_value, "A:f123.7%f5.65_M:0", 5) /* 123.7 % 5.65 = 5.05 -> 5 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH01%f7.5_A:0xH02%f5.5_M:0", 5); /* 18%7.3 = 3.4, 52%5.5=2.5, becomes 3+2=5*/ - - /* addition */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001+3_M:0", 21); /* 18+3 = 21 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0002+0xH0001_M:0", 70); /* 52+18 = 70 */ - TEST_PARAMS2(test_evaluate_value, "A:fF5+f2_M:0", 5) /* PI + 2.0 = 5.141592 -> 5 */ - TEST_PARAMS2(test_evaluate_value, "A:f5.5+f2.0_M:0", 7) /* 5.5 + 2.0 = 7.5 -> 7 */ - TEST_PARAMS2(test_evaluate_value, "A:f5.5+f2.7_M:0", 8) /* 5.5 + 2.7 = 8.2 -> 8 */ - TEST_PARAMS2(test_evaluate_value, "B:0xH0001+3_M:100", 79) /* 100 - (18+3) = 79 */ - TEST_PARAMS2(test_evaluate_value, "I:0xH0000+3_M:0x0", 0x56AB) /* Add Address (0+3) -> Offset 0. 16-Bit Read @ Byte 3 = 0x56AB */ - - /* subtraction */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0001-3_M:0", 15); /* 18-3 = 15 */ - TEST_PARAMS2(test_evaluate_value, "A:0xH0002-0xH0001_M:0", 34); /* 52-18 = 34 */ - TEST_PARAMS2(test_evaluate_value, "A:fF5-f2_M:0", 1) /* PI - 2.0 = 1.141592 -> 1 */ - TEST_PARAMS2(test_evaluate_value, "A:f5.5-f2.0_M:0", 3) /* 5.5 - 2.0 = 2.5 -> 3 */ - TEST_PARAMS2(test_evaluate_value, "A:f5.5-f2.7_M:0", 2) /* 5.5 - 2.7 = 2.8 -> 2 */ - TEST_PARAMS2(test_evaluate_value, "B:0xH0001-3_M:100",85) /* 100 - (18-3) = 85 */ - - /* rounding */ - TEST_PARAMS2(test_evaluate_value, "0xH03/2_0xH03/2", 0xAA); /* integer division results in rounding */ - TEST_PARAMS2(test_evaluate_value, "0xH03/f2.0_0xH03/f2.0", 0xAB); /* float division does not result in rounding */ - TEST_PARAMS2(test_evaluate_value, "0xH03*0.5_0xH03*0.5", 0xAB); /* float multiplication does not result in rounding */ - TEST_PARAMS2(test_evaluate_value, "A:0xH03/2_A:0xH03/2_M:0", 0xAA); /* integer division results in rounding */ - TEST_PARAMS2(test_evaluate_value, "A:0xH03/f2.0_A:0xH03/f2.0_M:0", 0xAB); /* float division does not result in rounding */ - TEST_PARAMS2(test_evaluate_value, "I:0xH0001-17_M:0x0", 0x3412) /* Add Address (18-17) -> Offset 0. 16-Bit Read @ Byte 1 = 0x3412 */ - - /* using measured_if */ - TEST_PARAMS2(test_evaluate_value, "Q:0xH0001!=0_M:0xH0002", 0x34); - TEST_PARAMS2(test_evaluate_value, "Q:0xH0001=0_M:0xH0002", 0); - TEST_PARAMS2(test_evaluate_value, "Q:0xH0001!=0_M:1", 1); - TEST(test_evaluated_and_next_measured_if_value); - - /* using accumulator */ - TEST_PARAMS2(test_evaluate_value, "K:0xH01_M:{recall}", 0x12); /* 18-> recall accumulator, Measurement = 18 */ - TEST_PARAMS2(test_evaluate_value, "K:0xH01_K:{recall}*2_M:{recall}", 0x24); /* 18-> recall accumulator, recall accumulator*2 -> recall accumulator, Measurement 18*2 = 36 */ - TEST_PARAMS2(test_evaluate_value, "K:0xH01*0xH02_M:{recall}", 0x3A8); /* 18*52-> recall accumulator, Measurement = 936 */ - TEST_PARAMS2(test_evaluate_value, "A:4_K:0xH01_K:{recall}*2_M:{recall}", 44); /* Chain Addsource into Remember (4 + 18) * 2 = 44 */ - TEST_PARAMS2(test_evaluate_value, "A:4_K:2*8_M:{recall}", 20); /* Chain Addsource into Remember 4 + (2 * 8) = 20 */ - TEST_PARAMS2(test_evaluate_value, "A:4_K:2*8_A:{recall}*2_M:4*{recall}", 120); /* Use remembered value multiple times */ - TEST_PARAMS2(test_evaluate_value, "K:0xH01*2_Q:{recall}<40_P:{recall}=36_M:{recall}", 36); /* Pause happens before recall accumulator is set because remember not part of pause chain. */ - TEST_PARAMS2(test_evaluate_value, "K:0xH01*2_P:{recall}=18_M:{recall}", 36); /* Measures the accumulated value, which was set in the pause pass. */ - TEST_PARAMS2(test_evaluate_value, "K:1_I:{recall}_M:0x02", 0x56AB); /* using recall accumulator as pointer */ - TEST_PARAMS2(test_evaluate_value, "K:1_I:{recall}_K:0x02_M:{recall}", 0x56AB); /* Use recall accumulator as pointer, then store pointed-to data in recall accumulator and measure that */ - TEST_PARAMS2(test_evaluate_value, "K:5_C:{recall}>3_C:{recall}<7_C:{recall}>5_M:0=1", 2); /* with addhits, reusing the recall accumulator in each. */ - TEST_PARAMS2(test_evaluate_value, "K:5_A:1_M:{recall}", 6); /* Add Source onto a read of the recall accumulator as the value */ - TEST_PARAMS2(test_evaluate_value, "K:5_A:1_C:{recall}=6_B:1_C:{recall}=4_M:0=1", 2); - TEST_PARAMS2(test_evaluate_value, "A:{recall}_M:1", 1); /* Using recall without remember. 0 + 1 = 1*/ - TEST_PARAMS2(test_evaluate_value, "K:{recall}^255_M:{recall}", 255); /* Using recall in first remember. 0x00^0xFF = 0xFF */ - TEST_PARAMS2(test_evaluate_value, "A:1_K:2_M:{recall}", 3); /* Remembered value includes AddSource chain */ - - /* pause and reset affect hit count */ - TEST(test_evaluate_measured_value_with_pause); - TEST(test_evaluate_measured_value_with_reset); - - /* overflow - 145406052 * 86 = 125049208332 -> 0x1D1D837E0C, leading 0x1D is truncated off */ - TEST_PARAMS2(test_evaluate_value, "0xX0001*0xH0004", 0x1D837E0C); - - test_typed_value_conversion(); - test_typed_value_addition(); - test_typed_value_multiplication(); - test_typed_value_division(); - test_typed_value_modulus(); - test_typed_value_negation(); - - test_addhits_float_coercion(); - test_addhits_float_coercion_remembered(); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rhash/data.c b/src/rcheevos/test/rhash/data.c deleted file mode 100644 index 0161ec5b4e..0000000000 --- a/src/rcheevos/test/rhash/data.c +++ /dev/null @@ -1,657 +0,0 @@ -#include "data.h" - -#include "../src/rc_compat.h" - -#include -#include - -void fill_image(uint8_t* image, size_t size) -{ - int seed = (int)(size ^ (size >> 8) ^ ((size - 1) * 25387)); - int count; - uint8_t value; - - while (size > 0) - { - switch (seed & 0xFF) - { - case 0: - count = (((seed >> 8) & 0x3F) & ~(size & 0x0F)); - if (count == 0) - count = 1; - value = 0; - break; - - case 1: - count = ((seed >> 8) & 0x07) + 1; - value = ((seed >> 16) & 0xFF); - break; - - case 2: - count = ((seed >> 8) & 0x03) + 1; - value = ((seed >> 16) & 0xFF) ^ 0xFF; - break; - - case 3: - count = ((seed >> 8) & 0x03) + 1; - value = ((seed >> 16) & 0xFF) ^ 0xA5; - break; - - case 4: - count = ((seed >> 8) & 0x03) + 1; - value = ((seed >> 16) & 0xFF) ^ 0xC3; - break; - - case 5: - count = ((seed >> 8) & 0x03) + 1; - value = ((seed >> 16) & 0xFF) ^ 0x96; - break; - - case 6: - count = ((seed >> 8) & 0x03) + 1; - value = ((seed >> 16) & 0xFF) ^ 0x78; - break; - - case 7: - count = ((seed >> 8) & 0x03) + 1; - value = ((seed >> 16) & 0xFF) ^ 0x78; - break; - - default: - count = 1; - value = ((seed >> 8) ^ (seed >> 16)) & 0xFF; - break; - } - - do - { - *image++ = value; - --size; - } while (size && --count); - - /* state mutation from psuedo-random number generator */ - seed = (seed * 0x41C64E6D + 12345) & 0x7FFFFFFF; - } -} - -uint8_t* generate_gamecube_iso(size_t mb, size_t* image_size) -{ - uint8_t* image; - const size_t size_needed = mb * 1024 * 1024; - int ix; - - image = (uint8_t*)calloc(size_needed, 1); - if (image != NULL) - { - uint32_t apploader_sizes_addr = 0x2440 + 0x14; - uint32_t dol_offset_addr = 0x420; - uint32_t dol_sizes_addr = 0x3000; - - fill_image(image, size_needed); - - image[0x1c] = 0xC2; - image[0x1d] = 0x33; - image[0x1e] = 0x9F; - image[0x1f] = 0x3D; - - for (ix = 0; ix < 8; ix++) - { - /* 0x000000ff for both */ - image[apploader_sizes_addr + ix] = (ix % 4 == 3) ? 0xff : 0; - } - for (ix = 0; ix < 4; ix++) - { - /* 0x00003000 */ - image[dol_offset_addr + ix] = (ix % 4 == 2) ? 0x30 : 0; - } - for (ix = 0; ix < 18 * 4; ix++) - { - /* offsets start at 0x00003100 and increment */ - image[dol_sizes_addr + ix] = (ix % 4 == 2) ? (0x30 + 1 + ix / 4) : 0; - /* 0x000000ff for every other size */ - image[dol_sizes_addr + 0x90 + ix] = (ix % 8 == 3) ? 0xff : 0; - } - } - - if (image_size) - *image_size = size_needed; - return image; -} - -uint8_t* generate_3do_bin(uint32_t root_directory_sectors, uint32_t binary_size, size_t* image_size) -{ - const uint8_t volume_header[] = { - 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01, 0x00, /* header */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* comment */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 'C', 'D', '-', 'R', 'O', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* label */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0x2D, 0x79, 0x6E, 0x96, /* identifier */ - 0x00, 0x00, 0x08, 0x00, /* block size */ - 0x00, 0x00, 0x05, 0x00, /* block count */ - 0x31, 0x5a, 0xf2, 0xe6, /* root directory identifier */ - 0x00, 0x00, 0x00, 0x01, /* root directory size in blocks */ - 0x00, 0x00, 0x08, 0x00, /* block size in root directory */ - 0x00, 0x00, 0x00, 0x06, /* number of copies of root directory */ - 0x00, 0x00, 0x00, 0x01, /* block location of root directory */ - 0x00, 0x00, 0x00, 0x01, /* block location of first copy of root directory */ - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, /* block location of last copy of root directory */ - }; - - const uint8_t directory_data[] = { - 0xFF, 0xFF, 0xFF, 0xFF, /* next block */ - 0xFF, 0xFF, 0xFF, 0xFF, /* previous block */ - 0x00, 0x00, 0x00, 0x00, /* flags */ - 0x00, 0x00, 0x00, 0xA4, /* end of block */ - 0x00, 0x00, 0x00, 0x14, /* start of block */ - - 0x00, 0x00, 0x00, 0x07, /* flags - directory */ - 0x00, 0x00, 0x00, 0x00, /* identifier */ - 0x00, 0x00, 0x00, 0x00, /* type */ - 0x00, 0x00, 0x08, 0x00, /* block size */ - 0x00, 0x00, 0x00, 0x00, /* length in bytes */ - 0x00, 0x00, 0x00, 0x00, /* length in blocks */ - 0x00, 0x00, 0x00, 0x00, /* burst */ - 0x00, 0x00, 0x00, 0x00, /* gap */ - 'f', 'o', 'l', 'd', 'e', 'r', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* filename */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0x00, 0x00, 0x00, 0x00, /* extra copies */ - 0x00, 0x00, 0x00, 0x00, /* directory block address */ - - 0x00, 0x00, 0x00, 0x02, /* flags - file */ - 0x00, 0x00, 0x00, 0x00, /* identifier */ - 0x00, 0x00, 0x00, 0x00, /* type */ - 0x00, 0x00, 0x08, 0x00, /* block size */ - 0x00, 0x00, 0x00, 0x00, /* length in bytes */ - 0x00, 0x00, 0x00, 0x00, /* length in blocks */ - 0x00, 0x00, 0x00, 0x00, /* burst */ - 0x00, 0x00, 0x00, 0x00, /* gap */ - 'L', 'a', 'u', 'n', 'c', 'h', 'M', 'e', 0, 0, 0, 0, 0, 0, 0, 0, /* filename */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0x00, 0x00, 0x00, 0x00, /* extra copies */ - 0x00, 0x00, 0x00, 0x02, /* directory block address */ - }; - - size_t size_needed = (root_directory_sectors + 1 + ((binary_size + 2047) / 2048)) * 2048; - uint8_t* image = (uint8_t*)calloc(size_needed, 1); - size_t offset = 2048; - uint32_t i; - - if (!image) - return NULL; - - /* first sector - volume header */ - memcpy(image, volume_header, sizeof(volume_header)); - image[0x5B] = (uint8_t)root_directory_sectors; - - /* root directory sectors */ - for (i = 0; i < root_directory_sectors; ++i) - { - memcpy(&image[offset], directory_data, sizeof(directory_data)); - if (i < root_directory_sectors - 1) - { - image[offset + 0] = 0; - image[offset + 1] = 0; - image[offset + 2] = 0; - image[offset + 3] = (uint8_t)(i + 1); - - memcpy(&image[offset + 0x14 + 0x48 + 0x20], "filename", 8); - } - else - { - image[offset + 0x14 + 0x48 + 0x11] = (binary_size >> 16) & 0xFF; - image[offset + 0x14 + 0x48 + 0x12] = (binary_size >> 8) & 0xFF; - image[offset + 0x14 + 0x48 + 0x13] = (binary_size & 0xFF); - - image[offset + 0x14 + 0x48 + 0x16] = (((binary_size + 2047) / 2048) >> 8) & 0xFF; - image[offset + 0x14 + 0x48 + 0x17] = ((binary_size + 2047) / 2048) & 0xFF; - - image[offset + 0x14 + 0x48 + 0x47] = (uint8_t)(i + 2); - } - - if (i > 0) - { - image[offset + 4] = 0; - image[offset + 5] = 0; - image[offset + 6] = 0; - image[offset + 7] = (uint8_t)(i - 1); - } - - offset += 2048; - } - - /* binary data */ - fill_image(&image[offset], binary_size); - - *image_size = size_needed; - return image; -} - -uint8_t* generate_dreamcast_bin(uint32_t track_first_sector, uint32_t binary_size, size_t* image_size) -{ - /* https://mc.pp.se/dc/ip0000.bin.html */ - const uint8_t volume_header[] = - "SEGA SEGAKATANA " - "SEGA ENTERPRISES" - "5966 GD-ROM1/1 " /* device info */ - " U 918FA01 " /* region and peripherals */ - "X-1234N V1.001" /* product number and version */ - "20200910 " /* release date */ - "1ST_READ.BIN " /* boot file */ - "RETROACHIEVEMENT" /* company name */ - "UNIT TEST " /* product name */ - " " - " " - " " - " " - " " - " " - " "; - - const uint8_t directory_data[] = { - 0x30, /* length of directory record */ - 0x00, /* extended attribute record length */ - 0xD9, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* first logical block of file */ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* length in bytes */ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* date/time */ - 0x00, 0x00, 0x00, /* flags, unit size, gap size */ - 0x00, 0x00, 0x00, 0x00, /* sequence number*/ - 0x0E, /* length of file identifier */ - '1', 'S', 'T', '_', 'R', 'E', 'A', 'D', '.', 'B', 'I', 'N', ';', '1', /* file identifier */ - }; - - const size_t binary_sectors = (binary_size + 2047) / 2048; - const size_t size_needed = (binary_sectors + 18) * 2048; - uint8_t* image = (uint8_t*)calloc(size_needed, 1); - if (!image) - return NULL; - - /* volume header goes in sector 0 */ - memcpy(&image[0], volume_header, sizeof(volume_header)); - - /* directory information goes in sectors 16 and 17 */ - memcpy(&image[2048 * 16], "1CD001", 6); - image[2048 * 16 + 156 + 2] = 45017 & 0xFF; - image[2048 * 16 + 156 + 3] = (45017 >> 8) & 0xFF; - image[2048 * 16 + 156 + 4] = (45017 >> 16) & 0xFF; - memcpy(&image[2048 * 17], directory_data, sizeof(directory_data)); - - track_first_sector += 18; - image[2048 * 17 + 2] = (track_first_sector & 0xFF); - image[2048 * 17 + 3] = ((track_first_sector >> 8) & 0xFF); - image[2048 * 17 + 4] = ((track_first_sector >> 16) & 0xFF); - image[2048 * 17 + 10] = (binary_size & 0xFF); - image[2048 * 17 + 11] = ((binary_size >> 8) & 0xFF); - image[2048 * 17 + 12] = ((binary_size >> 16) & 0xFF); - image[2048 * 17 + 13] = ((binary_size >> 24) & 0xFF); - - /* binary data */ - fill_image(&image[2048 * 18], binary_sectors * 2048); - - *image_size = size_needed; - return image; -} - -uint8_t* generate_pce_cd_bin(uint32_t binary_sectors, size_t* image_size) -{ - const uint8_t volume_header[] = { - 0x00, 0x00, 0x02, /* first sector of boot code */ - 0x14, /* number of sectors for boot code */ - 0x00, 0x40, /* program load address */ - 0x00, 0x40, /* program execute address */ - 0, 1, 2, 3, 4, /* IPLMPR */ - 0, /* open mode */ - 0, 0, 0, 0, 0, 0, /* GRPBLK and addr */ - 0, 0, 0, 0, 0, /* ADPBLK and rate */ - 0, 0, 0, 0, 0, 0, 0, /* reserved */ - 'P', 'C', ' ', 'E', 'n', 'g', 'i', 'n', 'e', ' ', 'C', 'D', '-', 'R', 'O', 'M', - ' ', 'S', 'Y', 'S', 'T', 'E', 'M', '\0', 'C', 'o', 'p', 'y', 'r', 'i', 'g', 'h', - 't', ' ', 'H', 'U', 'D', 'S', 'O', 'N', ' ', 'S', 'O', 'F', 'T', ' ', '/', ' ', - 'N', 'E', 'C', ' ', 'H', 'o', 'm', 'e', ' ', 'E', 'l', 'e', 'c', 't', 'r', 'o', - 'n', 'i', 'c', 's', ',', 'L', 't', 'd', '.', '\0', 'G', 'A', 'M', 'E', 'N', 'A', - 'M', 'E', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' - }; - - size_t size_needed = (binary_sectors + 2) * 2048; - uint8_t* image = (uint8_t*)calloc(size_needed, 1); - if (!image) - return NULL; - - /* volume header goes in second sector */ - memcpy(&image[2048], volume_header, sizeof(volume_header)); - image[2048 + 0x03] = (uint8_t)binary_sectors; - - /* binary data */ - fill_image(&image[4096], binary_sectors * 2048); - - *image_size = size_needed; - return image; -} - -uint8_t* generate_pcfx_bin(uint32_t binary_sectors, size_t* image_size) -{ - const uint8_t volume_header[] = { - 'G', 'A', 'M', 'E', 'N', 'A', 'M', 'E', 0, 0, 0, 0, 0, 0, 0, 0, /* title (32-bytes) */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0x02, 0x00, 0x00, 0x00, /* first sector of boot code */ - 0x14, 0x00, 0x00, 0x00, /* number of sectors for boot code */ - 0x00, 0x80, 0x00, 0x00, /* program load address */ - 0x00, 0x80, 0x00, 0x00, /* program execute address */ - 'N', '/', 'A', '\0', /* maker id */ - 'r', 'c', 'h', 'e', 'e', 'v', 'o', 's', 't', 'e', 's', 't', 0, 0, 0, 0, /* maker name (60-bytes) */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0x00, 0x00, 0x00, 0x00, /* volume number */ - 0x00, 0x01, /* version */ - 0x01, 0x00, /* country */ - '2', '0', '2', '0', 'X', 'X', 'X', 'X', /* date */ - }; - - size_t size_needed = (binary_sectors + 2) * 2048; - uint8_t* image = (uint8_t*)calloc(size_needed, 1); - if (!image) - return NULL; - - /* volume header goes in second sector */ - strcpy_s((char*)&image[0], size_needed, "PC-FX:Hu_CD-ROM"); - memcpy(&image[2048], volume_header, sizeof(volume_header)); - image[2048 + 0x24] = (uint8_t)binary_sectors; - - /* binary data */ - fill_image(&image[4096], binary_sectors * 2048); - - *image_size = size_needed; - return image; -} - -uint8_t* generate_iso9660_bin(uint32_t num_sectors, const char* volume_label, size_t* image_size) -{ - const uint8_t identifier[] = { 0x01, 'C', 'D', '0', '0', '1', 0x01, 0x00 }; - uint8_t* volume_descriptor;; - uint8_t* image; - - *image_size = num_sectors * 2048; - image = calloc(*image_size, 1); - if (!image) - return NULL; - - volume_descriptor = &image[16 * 2048]; - - /* CD001 identifier */ - memcpy(volume_descriptor, identifier, 8); - - /* volume label */ - memcpy(&volume_descriptor[40], volume_label, strlen(volume_label)); - - /* number of sectors (little endian, then big endian) */ - volume_descriptor[80] = image[87] = num_sectors & 0xFF; - volume_descriptor[81] = image[86] = (num_sectors >> 8) & 0xFF; - volume_descriptor[82] = image[85] = (num_sectors >> 16) & 0xFF; - volume_descriptor[83] = image[84] = (num_sectors >> 24) & 0xFF; - - /* size of each sector */ - volume_descriptor[128] = (2048) & 0xFF; - volume_descriptor[129] = (2048 >> 8) & 0xFF; - - /* root directory record location */ - volume_descriptor[158] = 17; - - /* helper for tracking next free sector - not actually part of iso9660 spec */ - image[17 * 2048 - 4] = 18; - - return image; -} - -uint8_t* generate_iso9660_file(uint8_t* image, const char* filename, const uint8_t* contents, size_t contents_size) -{ - const uint32_t root_directory_record_offset = 17 * 2048; - uint8_t* file_entry_start = &image[root_directory_record_offset]; - uint8_t* file_contents_start; - size_t filename_len; - uint32_t next_free_sector = image[root_directory_record_offset - 4] | - (image[root_directory_record_offset - 3] << 8) | (image[root_directory_record_offset - 2] << 16); - const char* separator; - - /* we start at the root. ignore explicit root path */ - if (*filename == '\\') - ++filename; - - /* handle subdirectories */ - do - { - separator = filename; - while (*separator && *separator != '\\') - ++separator; - - if (!*separator) - break; - - filename_len = separator - filename; - int found = 0; - while (*file_entry_start) - { - if (file_entry_start[25] && /* is directory */ - file_entry_start[33 + filename_len] == '\0' && memcmp(&file_entry_start[33], filename, filename_len) == 0) - { - const uint32_t directory_sector = file_entry_start[2]; - file_entry_start = &image[directory_sector * 2048]; - found = 1; - break; - } - - file_entry_start += *file_entry_start; - } - - if (!found) - { - /* entry size*/ - file_entry_start[0] = (filename_len & 0xFF) + 48; - - /* directory sector */ - file_entry_start[2] = next_free_sector & 0xFF; - file_entry_start[3] = (next_free_sector >> 8) & 0xFF; - - /* is directory */ - file_entry_start[25] = 1; - - /* directory name */ - file_entry_start[32] = filename_len & 0xFF; - memcpy(&file_entry_start[33], filename, filename_len); - file_entry_start[33 + filename_len] = '\0'; - - /* advance to next sector */ - file_entry_start = &image[next_free_sector * 2048]; - next_free_sector++; - } - - filename = separator + 1; - } while (1); - - /* skip over any items already in the directory */ - while (*file_entry_start) - file_entry_start += *file_entry_start; - - /* create the new entry */ - - /* entry size*/ - filename_len = separator - filename; - file_entry_start[0] = (filename_len & 0xFF) + 48; - - /* file sector */ - file_entry_start[2] = next_free_sector & 0xFF; - file_entry_start[3] = (next_free_sector >> 8) & 0xFF; - - /* file size */ - file_entry_start[10] = contents_size & 0xFF; - file_entry_start[11] = (contents_size >> 8) & 0xFF; - file_entry_start[12] = (contents_size >> 16) & 0xFF; - - /* file name */ - file_entry_start[32] = (filename_len + 2) & 0xFF; - memcpy(&file_entry_start[33], filename, filename_len); - file_entry_start[33 + filename_len] = ';'; - file_entry_start[34 + filename_len] = '1'; - - /* contents */ - file_contents_start = &image[next_free_sector * 2048]; - - if (contents) - memcpy(file_contents_start, contents, contents_size); - else - fill_image(file_contents_start, contents_size); - - /* update next free sector */ - next_free_sector += (unsigned)(contents_size + 2047) / 2048; - image[root_directory_record_offset - 4] = (next_free_sector & 0xFF); - image[root_directory_record_offset - 3] = (next_free_sector >> 8) & 0xFF; - image[root_directory_record_offset - 2] = (next_free_sector >> 16) & 0xFF; - - /* return pointer to contents so caller can modify if desired */ - return file_contents_start; -} - -uint8_t* generate_jaguarcd_bin(uint32_t header_offset, uint32_t binary_size, int byteswapped, size_t* image_size) -{ - size_t size_needed = (((binary_size + 64 + 32 + 8) + 2351) / 2352) * 2352; - uint8_t* image = (uint8_t*)calloc(size_needed, 1); - size_t i; - - if (!image) - return NULL; - - /* header is 64 bytes of ATRI repeating followed by approved data message, load address, and binary size */ - for (i = 0; i < 64; i += 4) - memcpy(&image[header_offset + i], "ATRI", 4); - memcpy(&image[header_offset + 64], "ATARI APPROVED DATA HEADER ATRI ", 32); - image[header_offset + 64 + 32 + 2] = 0xA0; - image[header_offset + 64 + 32 + 4 + 1] = (binary_size >> 16); - image[header_offset + 64 + 32 + 4 + 2] = (binary_size >> 8) & 0xFF; - image[header_offset + 64 + 32 + 4 + 3] = (binary_size & 0xFF); - - /* binary data */ - fill_image(&image[header_offset + 64 + 32 + 8], size_needed - (header_offset + 64 + 32 + 8)); - - if (byteswapped) - { - for (i = 0; i < size_needed; i += 2) - { - uint8_t tmp = image[i]; - image[i] = image[i + 1]; - image[i + 1] = tmp; - } - } - - *image_size = size_needed; - return image; -} - -uint8_t* generate_psx_bin(const char* binary_name, uint32_t binary_size, size_t* image_size) -{ - const uint32_t sectors_needed = (((binary_size + 2047) / 2048) + 20); - char system_cnf[256]; - uint8_t* image; - uint8_t* exe; - - snprintf(system_cnf, sizeof(system_cnf), "BOOT=cdrom:\\%s;1\nTCB=4\nEVENT=10\nSTACK=801FFFF0\n", binary_name); - - image = generate_iso9660_bin(sectors_needed, "TEST", image_size); - generate_iso9660_file(image, "SYSTEM.CNF", (const uint8_t*)system_cnf, strlen(system_cnf)); - - /* binary data */ - exe = generate_iso9660_file(image, binary_name, NULL, binary_size); - memcpy(exe, "PS-X EXE", 8); - - binary_size -= 2048; - exe[28] = binary_size & 0xFF; - exe[29] = (binary_size >> 8) & 0xFF; - exe[30] = (binary_size >> 16) & 0xFF; - exe[31] = (binary_size >> 24) & 0xFF; - - return image; -} - -uint8_t* generate_ps2_bin(const char* binary_name, uint32_t binary_size, size_t* image_size) -{ - const uint32_t sectors_needed = (((binary_size + 2047) / 2048) + 20); - char system_cnf[256]; - uint8_t* image; - uint8_t* exe; - - snprintf(system_cnf, sizeof(system_cnf), "BOOT2 = cdrom0:\\%s;1\nVER = 1.0\nVMODE = NTSC\n", binary_name); - - image = generate_iso9660_bin(sectors_needed, "TEST", image_size); - generate_iso9660_file(image, "SYSTEM.CNF", (const uint8_t*)system_cnf, strlen(system_cnf)); - - /* binary data */ - exe = generate_iso9660_file(image, binary_name, NULL, binary_size); - memcpy(exe, "\x7f\x45\x4c\x46", 4); - - return image; -} - -uint8_t* generate_generic_file(size_t size) -{ - uint8_t* image; - image = (uint8_t*)calloc(size, 1); - if (image != NULL) - fill_image(image, size); - - return image; -} - -uint8_t* convert_to_2352(uint8_t* input, size_t* size, uint32_t first_sector) -{ - const uint32_t num_sectors = (uint32_t)((*size + 2047) / 2048); - const uint32_t output_size = num_sectors * 2352; - const uint8_t sync_pattern[] = { - 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 - }; - uint8_t* output = (uint8_t*)malloc(output_size); - uint8_t* input_ptr = input; - uint8_t* ptr = output; - uint8_t minutes, seconds, frames; - uint32_t i; - - first_sector += 150; - frames = (first_sector % 75); - first_sector /= 75; - seconds = (first_sector % 60); - minutes = first_sector / 60; - - for (i = 0; i < num_sectors; i++) - { - /* 16 - byte sync header */ - memcpy(ptr, sync_pattern, 12); - ptr += 12; - *ptr++ = ((minutes / 10) << 4) | (minutes % 10); - *ptr++ = ((seconds / 10) << 4) | (seconds % 10); - *ptr++ = ((frames / 10) << 4) | (frames % 10); - if (++frames == 75) - { - frames = 0; - if (++seconds == 60) - { - seconds = 0; - ++minutes; - } - } - *ptr++ = 2; - - /* 2048 bytes data */ - memcpy(ptr, input_ptr, 2048); - input_ptr += 2048; - - /* 288 bytes parity / checksums */ - ptr += 2352 - 16; - } - - free(input); - *size = output_size; - return output; -} - diff --git a/src/rcheevos/test/rhash/data.h b/src/rcheevos/test/rhash/data.h deleted file mode 100644 index 509d6cea68..0000000000 --- a/src/rcheevos/test/rhash/data.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef RHASH_TEST_DATA_H -#define RHASH_TEST_DATA_H - -#include "rc_export.h" - -#include -#include - -RC_BEGIN_C_DECLS - -uint8_t* generate_generic_file(size_t size); - -uint8_t* convert_to_2352(uint8_t* input, size_t* input_size, uint32_t first_sector); - -uint8_t* generate_3do_bin(uint32_t root_directory_sectors, uint32_t binary_size, size_t* image_size); -uint8_t* generate_dreamcast_bin(uint32_t track_first_sector, uint32_t binary_size, size_t* image_size); -uint8_t* generate_jaguarcd_bin(uint32_t header_offset, uint32_t binary_size, int byteswapped, size_t* image_size); -uint8_t* generate_pce_cd_bin(uint32_t binary_sectors, size_t* image_size); -uint8_t* generate_pcfx_bin(uint32_t binary_sectors, size_t* image_size); -uint8_t* generate_psx_bin(const char* binary_name, uint32_t binary_size, size_t* image_size); -uint8_t* generate_ps2_bin(const char* binary_name, uint32_t binary_size, size_t* image_size); - -uint8_t* generate_gamecube_iso(size_t mb, size_t* image_size); - -uint8_t* generate_iso9660_bin(uint32_t binary_sectors, const char* volume_label, size_t* image_size); -uint8_t* generate_iso9660_file(uint8_t* image, const char* filename, const uint8_t* contents, size_t contents_size); - -void fill_image(uint8_t* image, size_t size); - -RC_END_C_DECLS - -#endif /* RHASH_TEST_DATA_H */ diff --git a/src/rcheevos/test/rhash/mock_filereader.c b/src/rcheevos/test/rhash/mock_filereader.c deleted file mode 100644 index 2c48654368..0000000000 --- a/src/rcheevos/test/rhash/mock_filereader.c +++ /dev/null @@ -1,236 +0,0 @@ -#include "rc_hash.h" - -#include "../rc_compat.h" - -#include -#include - -typedef struct mock_file_data -{ - const char* path; - const uint8_t* data; - int64_t size; - int64_t pos; - int first_sector; -} mock_file_data; - -static mock_file_data mock_file_instance[16]; -static int mock_cd_tracks; - -static void* _mock_file_open(const char* path) -{ - size_t i; - for (i = 0; i < sizeof(mock_file_instance) / sizeof(mock_file_instance[0]); ++i) - { - if (strcmp(path, mock_file_instance[i].path) == 0) - { - mock_file_instance[i].pos = 0; - return &mock_file_instance[i]; - } - } - - return NULL; -} - -static void _mock_file_seek(void* file_handle, int64_t offset, int origin) -{ - mock_file_data* file = (mock_file_data*)file_handle; - switch (origin) - { - case SEEK_SET: - file->pos = offset; - break; - case SEEK_CUR: - file->pos += offset; - break; - case SEEK_END: - file->pos = file->size - offset; - break; - } - - if (file->pos > file->size) - file->pos = file->size; -} - -static int64_t _mock_file_tell(void* file_handle) -{ - mock_file_data* file = (mock_file_data*)file_handle; - return file->pos; -} - -static size_t _mock_file_read(void* file_handle, void* buffer, size_t count) -{ - mock_file_data* file = (mock_file_data*)file_handle; - const size_t remaining = (size_t)(file->size - file->pos); - if (count > remaining) - count = remaining; - - if (count > 0) - { - if (file->data) - memcpy(buffer, &file->data[file->pos], count); - else - memset(buffer, 0, count); - - file->pos += count; - } - - return count; -} - -static void _mock_file_close(void* file_handle) -{ - (void)file_handle; -} - -static void reset_mock_files() -{ - size_t i; - - memset(&mock_file_instance, 0, sizeof(mock_file_instance)); - for (i = 0; i < sizeof(mock_file_instance) / sizeof(mock_file_instance[0]); ++i) - mock_file_instance[i].path = ""; - - mock_cd_tracks = 0; -} - -void get_mock_filereader(struct rc_hash_filereader* reader) -{ - reader->open = _mock_file_open; - reader->seek = _mock_file_seek; - reader->tell = _mock_file_tell; - reader->read = _mock_file_read; - reader->close = _mock_file_close; - - reset_mock_files(); -} - -void init_mock_filereader() -{ - struct rc_hash_filereader reader; - get_mock_filereader(&reader); - - rc_hash_init_custom_filereader(&reader); -} - -void mock_file(int index, const char* filename, const uint8_t* buffer, size_t buffer_size) -{ - if (index == 0) - reset_mock_files(); - - mock_file_instance[index].path = filename; - mock_file_instance[index].data = buffer; - mock_file_instance[index].size = buffer_size; - mock_file_instance[index].pos = 0; - mock_file_instance[index].first_sector = 0; -} - -void mock_file_text(int index, const char* filename, const char* contents) -{ - mock_file(index, filename, (const uint8_t*)contents, strlen(contents)); -} - -void mock_file_first_sector(int index, int first_sector) -{ - mock_file_instance[index].first_sector = first_sector; -} - -void mock_file_size(int index, size_t mock_size) -{ - mock_file_instance[index].size = mock_size; -} - -void mock_empty_file(int index, const char* filename, size_t mock_size) -{ - mock_file(index, filename, NULL, mock_size); -} - -#ifndef RC_HASH_NO_DISC - -static void* _mock_cd_open_track(const char* path, uint32_t track) -{ - if (track == RC_HASH_CDTRACK_LAST) - track = mock_cd_tracks; - - if (track == 1 || track == RC_HASH_CDTRACK_FIRST_DATA || track == RC_HASH_CDTRACK_LARGEST) - { - if (strstr(path, ".cue")) - { - mock_file_data* file = (mock_file_data*)_mock_file_open(path); - if (!file) - return file; - - return _mock_file_open((const char*)file->data); - } - - return _mock_file_open(path); - } - else if (strstr(path, ".cue")) - { - mock_file_data* file = (mock_file_data*)_mock_file_open(path); - if (file) - { - char buffer[256]; - - const size_t file_len = strlen((const char*)file->data); - memcpy(buffer, file->data, file_len - 4); - snprintf(&buffer[file_len - 4], sizeof(buffer) - (file_len - 4), "%d%s", track, &file->data[file_len - 4]); - - return _mock_file_open(buffer); - } - } - else if (strstr(path, ".gdi")) - { - mock_file_data* file = (mock_file_data*)_mock_file_open(path); - if (file) - { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "track%02d.bin", track); - return _mock_file_open(buffer); - } - } - - return NULL; -} - -static size_t _mock_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) -{ - mock_file_data* file = (mock_file_data*)track_handle; - sector -= file->first_sector; - - _mock_file_seek(track_handle, sector * 2048, SEEK_SET); - return _mock_file_read(track_handle, buffer, requested_bytes); -} - -static uint32_t _mock_cd_first_track_sector(void* track_handle) -{ - mock_file_data* file = (mock_file_data*)track_handle; - return file->first_sector; -} - -void mock_cd_num_tracks(int num_tracks) -{ - mock_cd_tracks = num_tracks; -} - -void init_mock_cdreader() -{ - struct rc_hash_cdreader cdreader; - memset(&cdreader, 0, sizeof(cdreader)); - cdreader.open_track = _mock_cd_open_track; - cdreader.close_track = _mock_file_close; - cdreader.read_sector = _mock_cd_read_sector; - cdreader.first_track_sector = _mock_cd_first_track_sector; - - rc_hash_init_custom_cdreader(&cdreader); - - mock_cd_tracks = 0; -} - -#endif - -const char* get_mock_filename(void* file_handle) -{ - mock_file_data* file = (mock_file_data*)file_handle; - return file->path; -} diff --git a/src/rcheevos/test/rhash/mock_filereader.h b/src/rcheevos/test/rhash/mock_filereader.h deleted file mode 100644 index 552c71c645..0000000000 --- a/src/rcheevos/test/rhash/mock_filereader.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef RHASH_MOCK_FILEREADER_H -#define RHASH_MOCK_FILEREADER_H - -#include "rc_export.h" - -#include -#include - -RC_BEGIN_C_DECLS - -void init_mock_filereader(); -void get_mock_filereader(struct rc_hash_filereader* reader); - -#ifndef RC_HASH_NO_DISC -void init_mock_cdreader(); -void mock_cd_num_tracks(int num_tracks); -#endif - -void rc_hash_reset_filereader(); - -void mock_file(int index, const char* filename, const uint8_t* buffer, size_t buffer_size); -void mock_file_text(int index, const char* filename, const char* contents); -void mock_empty_file(int index, const char* filename, size_t mock_size); -void mock_file_size(int index, size_t mock_size); -void mock_file_first_sector(int index, int first_sector); - -const char* get_mock_filename(void* file_handle); - -RC_END_C_DECLS - -#endif /* RHASH_MOCK_FILEREADER_H */ diff --git a/src/rcheevos/test/rhash/test_cdreader.c b/src/rcheevos/test/rhash/test_cdreader.c deleted file mode 100644 index e3c475ae09..0000000000 --- a/src/rcheevos/test/rhash/test_cdreader.c +++ /dev/null @@ -1,920 +0,0 @@ -#include "rc_hash.h" - -#include "../rc_compat.h" -#include "../src/rhash/rc_hash_internal.h" -#include "../test_framework.h" -#include "data.h" -#include "mock_filereader.h" - -#include - -static const uint8_t sync_pattern[] = { - 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 -}; - -static char cue_single_track[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 MODE2/2352\n" - " INDEX 01 00:00:00\n"; - -static char cue_single_bin_multiple_data[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - " TRACK 02 MODE1/2352\n" - " PREGAP 00:03:00\n" - " INDEX 01 00:55:45\n" - " TRACK 03 MODE1/2352\n" - " INDEX 01 11:30:74\n" - " TRACK 04 MODE1/2352\n" - " INDEX 01 13:31:51\n" - " TRACK 05 MODE1/2352\n" - " INDEX 01 13:48:56\n" - " TRACK 06 MODE1/2352\n" - " INDEX 01 34:48:19\n" - " TRACK 07 MODE1/2352\n" - " INDEX 01 50:42:74\n" - " TRACK 08 MODE1/2352\n" - " INDEX 01 55:20:74\n" - " TRACK 09 MODE1/2352\n" - " INDEX 01 56:25:67\n" - " TRACK 10 MODE1/2352\n" - " INDEX 01 59:04:08\n" - " TRACK 11 MODE1/2352\n" - " INDEX 01 61:17:18\n" - " TRACK 12 MODE1/2352\n" - " INDEX 01 62:44:33\n" - " TRACK 13 AUDIO\n" - " PREGAP 00:02:00\n" - " INDEX 01 66:24:37\n"; - -static char cue_multiple_bin_multiple_data[] = - "FILE \"track1.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track2.bin\" BINARY\n" - " TRACK 02 MODE1/2352\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:03:00\n" - "FILE \"track3.bin\" BINARY\n" - " TRACK 03 MODE1/2352\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n" - "FILE \"track4.bin\" BINARY\n" - " TRACK 04 AUDIO\n" - " INDEX 00 00:00:00\n"; - -static char gdi_three_tracks[] = - "3\n" - "1 0 4 2352 track01.bin 0\n" - "2 600 0 2352 track02.raw 0\n" - "3 45000 4 2352 track03.bin 0"; - -static char gdi_many_tracks[] = - "26\n" - "1 0 4 2352 track01.bin 0\n" - "2 450 0 2352 track02.raw 0\n" - "3 45000 4 2352 track03.bin 0\n" - "4 370673 0 2352 track04.raw 0\n" - "5 371347 0 2352 track05.raw 0\n" - "6 372014 0 2352 track06.raw 0\n" - "7 372915 0 2352 track07.raw 0\n" - "8 373626 0 2352 track08.raw 0\n" - "9 379011 0 2352 track09.raw 0\n" - "10 384738 0 2352 track10.raw 0\n" - "11 390481 0 2352 track11.raw 0\n" - "12 395473 0 2352 track12.raw 0\n" - "13 398926 0 2352 track13.raw 0\n" - "14 404448 0 2352 track14.raw 0\n" - "15 425246 0 2352 track15.raw 0\n" - "16 445520 0 2352 track16.raw 0\n" - "17 466032 0 2352 track17.raw 0\n" - "18 474231 0 2352 track18.raw 0\n" - "19 485598 0 2352 track19.raw 0\n" - "20 486386 0 2352 track20.raw 0\n" - "21 487098 0 2352 track21.raw 0\n" - "22 487822 0 2352 track22.raw 0\n" - "23 498356 0 2352 track23.raw 0\n" - "24 508297 0 2352 track24.raw 0\n" - "25 527383 0 2352 track25.raw 0\n" - "26 548106 4 2352 track26.bin 0\n"; - -static void initialize_iterator(rc_hash_iterator_t* iterator) -{ - memset(iterator, 0, sizeof(*iterator)); - rc_hash_get_default_cdreader(&iterator->callbacks.cdreader); - get_mock_filereader(&iterator->callbacks.filereader); -} - -static rc_hash_cdrom_track_t* open_track(rc_hash_iterator_t* iterator, const char* path, uint32_t index) -{ - return (rc_hash_cdrom_track_t*)iterator->callbacks.cdreader.open_track_iterator(path, index, iterator); -} - -static void close_track(rc_hash_iterator_t* iterator, rc_hash_cdrom_track_t* track_handle) -{ - iterator->callbacks.cdreader.close_track(track_handle); -} - -static void test_open_cue_track_2() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", 2); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 9807840); /* track 2: 0x95A7E0 */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_12() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", 12); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 664047216); /* track 12: 0x27948E70 */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_14() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - mock_empty_file(1, "game.bin", 718310208); - - /* only 13 tracks */ - track_handle = open_track(&iterator, "game.cue", 14); - ASSERT_PTR_NULL(track_handle); -} - -static void test_open_cue_track_missing_bin() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - - track_handle = open_track(&iterator, "game.cue", 2); - ASSERT_PTR_NULL(track_handle); -} - -static void test_open_gdi_track_3() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.gdi", gdi_three_tracks); - mock_empty_file(1, "track03.bin", 1185760800); - - track_handle = open_track(&iterator, "game.gdi", 3); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track03.bin"); - ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); - ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_gdi_track_3_quoted() -{ - const char gdi_contents[] = - "3\n" - "1 0 4 2352 \"track 01.bin\" 0\n" - "2 600 0 2352 \"track 02.raw\" 0\n" - "3 45000 4 2352 \"track 03.bin\" 0"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.gdi", gdi_contents); - mock_empty_file(1, "track 03.bin", 1185760800); - - track_handle = open_track(&iterator, "game.gdi", 3); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track 03.bin"); - ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); - ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_gdi_track_3_extra_whitespace() -{ - const char gdi_contents[] = - "3\n\n" - " 1 0 4 2352 \"track 01.bin\" 0\n\n" - " 2 600 0 2352 \"track 02.raw\" 0\n\n" - " 3 45000 4 2352 \"track 03.bin\" 0\n\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.gdi", gdi_contents); - mock_empty_file(1, "track 03.bin", 1185760800); - - track_handle = open_track(&iterator, "game.gdi", 3); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track 03.bin"); - ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); - ASSERT_NUM_EQUALS(track_handle->track_first_sector, 45000); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_open_gdi_track_last() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.gdi", gdi_many_tracks); - mock_empty_file(1, "track26.bin", 2457600); - - track_handle = open_track(&iterator, "game.gdi", RC_HASH_CDTRACK_LAST); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track26.bin"); - ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); - ASSERT_NUM_EQUALS(track_handle->track_first_sector, 548106); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_multiple_bin() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_multiple_bin_multiple_data); - mock_empty_file(1, "track2.bin", 406423248); - mock_empty_file(2, "track3.bin", 11553024); - - track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track2.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 225); /* INDEX 01 00:03:00 */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_backwards_compatibility() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - mock_empty_file(1, "game.bin", 718310208); - - /* before defining the enum, 0 meant largest */ - track_handle = open_track(&iterator, "game.cue", 0); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_last_track() -{ - const char cue[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - " TRACK 02 MODE1/2352\n" - " PREGAP 00:03:00\n" - " INDEX 01 00:55:45\n" - " TRACK 03 MODE1/2352\n" - " INDEX 01 11:30:74\n" - " TRACK 04 MODE1/2352\n" - " INDEX 01 13:31:51\n" - " TRACK 05 MODE1/2352\n" - " INDEX 01 13:48:56\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 146190912); /* track 5: 0x8B6B240 (13:48:56) */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_index0s() -{ - const char cue[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - " TRACK 02 MODE1/2352\n" - " INDEX 00 00:44:65\n" - " INDEX 01 00:47:65\n" - " TRACK 03 AUDIO\n" - " INDEX 00 01:19:52\n" - " INDEX 01 01:21:52\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 7914480); /* track 2: 0x78C3F0 (00:44:65) */ - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 225); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_index2() -{ - const char cue[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - " TRACK 02 MODE1/2352\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n" - " INDEX 02 00:08:64\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_LARGEST); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); /* 00:02:00 = 150 frames in */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_multiple_bins() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_multiple_bin_multiple_data); - mock_empty_file(1, "track1.bin", 4132464); - mock_empty_file(2, "track2.bin", 30080102); - mock_empty_file(3, "track3.bin", 40343152); - mock_empty_file(4, "track4.bin", 47277552); - - track_handle = open_track(&iterator, "game.cue", 0); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track3.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); /* 00:02:00 = 150 frames in */ - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_open_cue_track_largest_data_only_audio() -{ - const char cue[] = - "FILE \"track1.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track2.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:03:00\n" - "FILE \"track3.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n" - "FILE \"track4.bin\" BINARY\n" - " TRACK 04 AUDIO\n" - " INDEX 00 00:00:00\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_empty_file(1, "track1.bin", 4132464); - mock_empty_file(2, "track2.bin", 30080102); - mock_empty_file(3, "track3.bin", 40343152); - mock_empty_file(4, "track4.bin", 47277552); - - track_handle = open_track(&iterator, "game.cue", 0); - ASSERT_PTR_NULL(track_handle); -} - -static void test_open_cue_track_first_data() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_bin_multiple_data); - mock_empty_file(1, "game.bin", 718310208); - - track_handle = open_track(&iterator, "game.cue", RC_HASH_CDTRACK_FIRST_DATA); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 9807840); /* track 2: 0x0095a7e0 (00:55:45) */ - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 0); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - close_track(&iterator, track_handle); -} - -static void test_determine_sector_size_sync(int sector_size) -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - - const size_t image_size = (size_t)sector_size * 32; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_track); - mock_file(1, "game.bin", image, image_size); - - memset(image, 0, image_size); - memcpy(&image[sector_size * 16], sync_pattern, sizeof(sync_pattern)); - - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_determine_sector_size_sync_primary_volume_descriptor(int sector_size) -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - - const size_t image_size = (size_t)sector_size * 32; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_track); - mock_file(1, "game.bin", image, image_size); - - memset(image, 0, image_size); - memcpy(&image[sector_size * 16], sync_pattern, sizeof(sync_pattern)); - memcpy(&image[sector_size * 16 + 25], "CD001", 5); - - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_determine_sector_size_sync_primary_volume_descriptor_index0(int sector_size) -{ - char cue[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 MODE2/2352\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - - const size_t image_size = (size_t)sector_size * 200; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_file(1, "game.bin", image, image_size); - - char sector_size_str[16]; - snprintf(sector_size_str, sizeof(sector_size_str), "%d", sector_size); - memcpy(&cue[40], sector_size_str, 4); - - memset(image, 0, image_size); - memcpy(&image[sector_size * (150 + 16)], sync_pattern, sizeof(sync_pattern)); - memcpy(&image[sector_size * (150 + 16) + 25], "CD001", 5); - - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); - ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_determine_sector_size_sync_2048() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - const int sector_size = 2048; - const size_t image_size = (size_t)sector_size * 32; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_track); - mock_file(1, "game.bin", image, image_size); - - memset(image, 0, image_size); - - /* 2048 byte sectors don't have a sync pattern - will use mode specified in header */ - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 24); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_determine_sector_size_sync_primary_volume_descriptor_2048() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - const int sector_size = 2048; - const size_t image_size = (size_t)sector_size * 32; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_track); - mock_file(1, "game.bin", image, image_size); - - memset(image, 0, image_size); - memcpy(&image[sector_size * 16 + 1], "CD001", 5); - - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 0); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_determine_sector_size_sync_primary_volume_descriptor_index0_2048() -{ - char cue[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 MODE1/2048\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - - const int sector_size = 2048; - const size_t image_size = (size_t)sector_size * 200; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_file(1, "game.bin", image, image_size); - - char sector_size_str[16]; - snprintf(sector_size_str, sizeof(sector_size_str), "%d", sector_size); - memcpy(&cue[40], sector_size_str, 4); - - memset(image, 0, image_size); - memcpy(&image[sector_size * (150 + 16) + 1], "CD001", 5); - - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->file_track_offset, 0); - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); - ASSERT_NUM_EQUALS(track_handle->sector_size, sector_size); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 0); - ASSERT_NUM_EQUALS(track_handle->raw_data_size, 2048); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_absolute_sector_to_track_sector_cue_pregap() -{ - const char cue[] = - "FILE \"game1.bin\" BINARY\n"/* file contains 500 sectors of data [1176000 bytes] */ - " TRACK 01 MODE2/2352\n" - " INDEX 00 00:00:00\n" /* 150 pre-gap sectors */ - " INDEX 01 00:02:00\n" /* 350 sectors of data */ - "FILE \"game2.bin\" BINARY\n" - " TRACK 02 MODE2/2352\n" - " INDEX 00 00:00:00\n" /* 150 pre-gap sectors */ - " INDEX 01 00:02:00\n"; - - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - - const size_t image_size = (size_t)60 * 200; - uint8_t* image = (uint8_t*)malloc(image_size); - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue); - mock_file(1, "game1.bin", NULL, (size_t)500 * 2352); - mock_file(2, "game2.bin", image, image_size); - - track_handle = open_track(&iterator, "game.cue", 2); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game2.bin"); - - /* pregap of second track starts at sector 500 */ - ASSERT_NUM_EQUALS(track_handle->track_first_sector, 500); - ASSERT_NUM_EQUALS(track_handle->track_pregap_sectors, 150); - - /* data for second track starts at sector 650 */ - ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.first_track_sector(track_handle), 650); - - close_track(&iterator, track_handle); - free(image); -} - -static void test_absolute_sector_to_track_sector_gdi() -{ - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - initialize_iterator(&iterator); - - mock_file_text(0, "game.gdi", gdi_many_tracks); - mock_file(1, "track26.bin", NULL, 1234567); - - track_handle = open_track(&iterator, "game.gdi", 26); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "track26.bin"); - ASSERT_NUM_EQUALS(track_handle->track_first_sector, 548106); - - ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.first_track_sector(track_handle), 548106); - - close_track(&iterator, track_handle); -} - -static void test_read_sector() -{ - char buffer[4096]; - rc_hash_cdrom_track_t* track_handle; - rc_hash_iterator_t iterator; - const size_t image_size = (size_t)2352 * 32; - uint8_t* image = (uint8_t*)malloc(image_size); - int offset, i; - ASSERT_PTR_NOT_NULL(image); - - initialize_iterator(&iterator); - - mock_file_text(0, "game.cue", cue_single_track); - mock_file(1, "game.bin", image, image_size); - - memset(image, 0, image_size); - memcpy(&image[2352 * 16], sync_pattern, sizeof(sync_pattern)); - image[2352 * 16 + 12] = 0; - image[2352 * 16 + 13] = 2; - image[2352 * 16 + 14] = 0x16; - image[2352 * 16 + 15] = 2; - - offset = 2352 * 1 + 16; - for (i = 0; i < 26; i++) - { - memset(&image[offset], i + 'A', 256); - offset += 256; - - if ((i % 8) == 7) - offset += (2352 - 2048); - } - - track_handle = open_track(&iterator, "game.cue", 1); - ASSERT_PTR_NOT_NULL(track_handle); - - ASSERT_PTR_NOT_NULL(track_handle->file_handle); - ASSERT_STR_EQUALS(get_mock_filename(track_handle->file_handle), "game.bin"); - ASSERT_NUM64_EQUALS(track_handle->track_pregap_sectors, 0); - ASSERT_NUM_EQUALS(track_handle->sector_size, 2352); - ASSERT_NUM_EQUALS(track_handle->sector_header_size, 16); - - /* read across multiple sectors */ - ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.read_sector(track_handle, 1, buffer, sizeof(buffer)), 4096); - - ASSERT_NUM_EQUALS(buffer[0], 'A'); - ASSERT_NUM_EQUALS(buffer[255], 'A'); - ASSERT_NUM_EQUALS(buffer[256], 'B'); - ASSERT_NUM_EQUALS(buffer[2047], 'H'); - ASSERT_NUM_EQUALS(buffer[2048], 'I'); - ASSERT_NUM_EQUALS(buffer[4095], 'P'); - - /* read of partial sector */ - ASSERT_NUM_EQUALS(iterator.callbacks.cdreader.read_sector(track_handle, 2, buffer, 10), 10); - ASSERT_NUM_EQUALS(buffer[0], 'I'); - ASSERT_NUM_EQUALS(buffer[9], 'I'); - ASSERT_NUM_EQUALS(buffer[10], 'A'); - - close_track(&iterator, track_handle); - free(image); -} - -/* ========================================================================= */ - -void test_cdreader(void) { - TEST_SUITE_BEGIN(); - - init_mock_filereader(); - rc_hash_init_default_cdreader(); - - TEST(test_open_cue_track_2); - TEST(test_open_cue_track_12); - TEST(test_open_cue_track_14); - TEST(test_open_cue_track_missing_bin); - - TEST(test_open_gdi_track_3); - TEST(test_open_gdi_track_3_quoted); - TEST(test_open_gdi_track_3_extra_whitespace); - TEST(test_open_gdi_track_last); - - TEST(test_open_cue_track_largest_data); - TEST(test_open_cue_track_largest_data_multiple_bin); - TEST(test_open_cue_track_largest_data_backwards_compatibility); - TEST(test_open_cue_track_largest_data_last_track); - TEST(test_open_cue_track_largest_data_index0s); - TEST(test_open_cue_track_largest_data_index2); - TEST(test_open_cue_track_largest_data_multiple_bins); - TEST(test_open_cue_track_largest_data_only_audio); - - TEST(test_open_cue_track_first_data); - - TEST_PARAMS1(test_determine_sector_size_sync, 2352); - TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor, 2352); - TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor_index0, 2352); - - TEST_PARAMS1(test_determine_sector_size_sync, 2336); - TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor, 2336); - TEST_PARAMS1(test_determine_sector_size_sync_primary_volume_descriptor_index0, 2336); - - TEST(test_determine_sector_size_sync_2048); - TEST(test_determine_sector_size_sync_primary_volume_descriptor_2048); - TEST(test_determine_sector_size_sync_primary_volume_descriptor_index0_2048); - - TEST(test_absolute_sector_to_track_sector_cue_pregap); - TEST(test_absolute_sector_to_track_sector_gdi); - - TEST(test_read_sector); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rhash/test_hash.c b/src/rcheevos/test/rhash/test_hash.c deleted file mode 100644 index a16402630b..0000000000 --- a/src/rcheevos/test/rhash/test_hash.c +++ /dev/null @@ -1,310 +0,0 @@ -#include "rc_hash.h" - -#include "../rhash/rc_hash_internal.h" - -#include "../rc_compat.h" -#include "../test_framework.h" -#include "data.h" -#include "mock_filereader.h" - -#include - -/* ========================================================================= */ - -void test_hash_full_file(uint32_t console_id, const char* filename, size_t size, const char* expected_md5) -{ - uint8_t* image = generate_generic_file(size); - char hash_buffer[33], hash_file[33], hash_iterator[33]; - mock_file(0, filename, image, size); - - /* test full buffer hash */ - int result_buffer = rc_hash_generate_from_buffer(hash_buffer, console_id, image, size); - - /* test full file hash */ - int result_file = rc_hash_generate_from_file(hash_file, console_id, filename); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, filename, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_buffer, 1); - ASSERT_STR_EQUALS(hash_buffer, expected_md5); - - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -void test_hash_m3u(uint32_t console_id, const char* filename, size_t size, const char* expected_md5) -{ - uint8_t* image = generate_generic_file(size); - char hash_file[33], hash_iterator[33]; - const char* m3u_filename = "test.m3u"; - - mock_file(0, filename, image, size); - mock_file(1, m3u_filename, (uint8_t*)filename, strlen(filename)); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, console_id, m3u_filename); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static void assert_valid_m3u(const char* disc_filename, const char* m3u_filename, const char* m3u_contents) -{ - const size_t size = 131072; - uint8_t* image = generate_generic_file(size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "a0f425b23200568132ba76b2405e3933"; - - mock_file(0, disc_filename, image, size); - mock_file(1, m3u_filename, (uint8_t*)m3u_contents, strlen(m3u_contents)); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC8800, m3u_filename); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_m3u_buffered() -{ - const size_t size = 131072; - uint8_t* image = generate_generic_file(size); - char hash_iterator[33]; - const char* m3u_filename = "test.m3u"; - const char* filename = "test.d88"; - const char* expected_md5 = "a0f425b23200568132ba76b2405e3933"; - uint8_t* m3u_contents = (uint8_t*)filename; - const size_t m3u_size = strlen(filename); - - mock_file(0, filename, image, size); - mock_file(1, m3u_filename, m3u_contents, m3u_size); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, m3u_filename, m3u_contents, m3u_size); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_m3u_with_comments() -{ - assert_valid_m3u("test.d88", "test.m3u", - "#EXTM3U\r\n\r\n#EXTBYT:131072\r\ntest.d88\r\n"); -} - -static void test_hash_m3u_empty() -{ - char hash_file[33], hash_iterator[33]; - const char* m3u_filename = "test.m3u"; - const char* m3u_contents = "#EXTM3U\r\n\r\n#EXTBYT:131072\r\n"; - - mock_file(0, m3u_filename, (uint8_t*)m3u_contents, strlen(m3u_contents)); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC8800, m3u_filename); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, m3u_filename, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); - ASSERT_NUM_EQUALS(result_iterator, 0); -} - -static void test_hash_m3u_trailing_whitespace() -{ - assert_valid_m3u("test.d88", "test.m3u", - "#EXTM3U \r\n \r\n#EXTBYT:131072 \r\ntest.d88 \t \r\n"); -} - -static void test_hash_m3u_line_ending() -{ - assert_valid_m3u("test.d88", "test.m3u", - "#EXTM3U\n\n#EXTBYT:131072\ntest.d88\n"); -} - -static void test_hash_m3u_extension_case() -{ - assert_valid_m3u("test.D88", "test.M3U", - "#EXTM3U\r\n\r\n#EXTBYT:131072\r\ntest.D88\r\n"); -} - -static void test_hash_m3u_relative_path() -{ - assert_valid_m3u("folder1/folder2/test.d88", "folder1/test.m3u", - "#EXTM3U\r\n\r\n#EXTBYT:131072\r\nfolder2/test.d88"); -} - -static void test_hash_m3u_absolute_path(const char* absolute_path) -{ - char m3u_contents[128]; - snprintf(m3u_contents, sizeof(m3u_contents), "#EXTM3U\r\n\r\n#EXTBYT:131072\r\n%s", absolute_path); - - assert_valid_m3u(absolute_path, "relative/test.m3u", m3u_contents); -} - -#ifndef RC_HASH_NO_ROM -uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size); - -static void test_hash_file_without_ext() -{ - size_t image_size; - uint8_t* image = generate_nes_file(32, 1, &image_size); - char hash_file[33], hash_iterator[33]; - const char* filename = "test"; - - mock_file(0, filename, image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO, filename); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, filename, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - - /* specifying a console will use the appropriate hasher */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, "6a2305a2b6675a97ff792709be1ca857"); - - /* no extension will use the default full file iterator, so hash should include header */ - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, "64b131c5c7fec32985d9c99700babb7e"); -} -#endif - -static void test_hash_handler_table_order() -{ - size_t num_handlers; - const rc_hash_iterator_ext_handler_entry_t* handlers = rc_hash_get_iterator_ext_handlers(&num_handlers); - int index; - - for (index = 1; index < num_handlers; ++index) { - if (strcmp(handlers[index].ext, handlers[index - 1].ext) <= 0) { - ASSERT_FAIL("handler[%s] after handler[%s]", handlers[index].ext, handlers[index - 1].ext); - } - } -} - -/* ========================================================================= */ - -void test_hash(void) { - TEST_SUITE_BEGIN(); - - init_mock_filereader(); - - /* Amstrad CPC */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); - - /* Apple II */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.nib", 232960, "96e8d33bdc385fd494327d6e6791cbe4"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); - - /* Commodore 64 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.nib", 327936, "e7767d32b23e3fa62c5a250a08caeba3"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); - - /* MSX */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); - - /* PC-8800 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); - - /* ZX Spectrum */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tap", 1596, "714a9f455e616813dd5421c5b347e5e5"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tzx", 14971, "93723e6d1100f9d1d448a27cf6618c47"); - - /* m3u support */ - TEST(test_hash_m3u_buffered); - TEST(test_hash_m3u_with_comments); - TEST(test_hash_m3u_empty); - TEST(test_hash_m3u_trailing_whitespace); - TEST(test_hash_m3u_line_ending); - TEST(test_hash_m3u_extension_case); - TEST(test_hash_m3u_relative_path); - TEST_PARAMS1(test_hash_m3u_absolute_path, "/absolute/test.d88"); - TEST_PARAMS1(test_hash_m3u_absolute_path, "\\absolute\\test.d88"); - TEST_PARAMS1(test_hash_m3u_absolute_path, "C:\\absolute\\test.d88"); - TEST_PARAMS1(test_hash_m3u_absolute_path, "\\\\server\\absolute\\test.d88"); - TEST_PARAMS1(test_hash_m3u_absolute_path, "samba:/absolute/test.d88"); - - /* other */ -#ifndef RC_HASH_NO_ROM - TEST(test_hash_file_without_ext); -#endif - TEST(test_hash_handler_table_order); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rhash/test_hash_disc.c b/src/rcheevos/test/rhash/test_hash_disc.c deleted file mode 100644 index b45b57fbc4..0000000000 --- a/src/rcheevos/test/rhash/test_hash_disc.c +++ /dev/null @@ -1,1450 +0,0 @@ -#include "rc_hash.h" - -#include "../rhash/rc_hash_internal.h" - -#include "../rc_compat.h" -#include "../test_framework.h" -#include "data.h" -#include "mock_filereader.h" - -#include - -/* in test_hash.c */ -void test_hash_full_file(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); -void test_hash_m3u(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); - -/* ========================================================================= */ - -static void test_hash_unknown_format(uint32_t console_id, const char* path) -{ - char hash_file[33] = "", hash_iterator[33] = ""; - - /* test file hash (won't match) */ - int result_file = rc_hash_generate_from_file(hash_file, console_id, path); - - /* test file identification from iterator (won't match) */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, path, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); - ASSERT_STR_EQUALS(hash_file, ""); - - ASSERT_NUM_EQUALS(result_iterator, 0); - ASSERT_STR_EQUALS(hash_iterator, ""); -} - -/* ========================================================================= */ - -static void test_hash_3do_bin() -{ - size_t image_size; - uint8_t* image = generate_3do_bin(1, 123456, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "9b2266b8f5abed9c12cce780750e88d6"; - - mock_file(0, "game.bin", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - mock_file_size(0, 45678901); /* must be > 32MB for iterator to consider CD formats for bin */ - rc_hash_initialize_iterator(&iterator, "game.bin", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_3do_cue() -{ - size_t image_size; - uint8_t* image = generate_3do_bin(1, 9347, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "257d1d19365a864266b236214dbea29c"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_3do_iso() -{ - size_t image_size; - uint8_t* image = generate_3do_bin(1, 9347, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "257d1d19365a864266b236214dbea29c"; - - mock_file(0, "game.iso", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.iso"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_3do_invalid_header() -{ - /* this is meant to simulate attempting to open a non-3DO CD. TODO: generate PSX CD */ - size_t image_size; - uint8_t* image = generate_3do_bin(1, 12, &image_size); - char hash_file[33]; - - /* make the header not match */ - image[3] = 0x34; - - mock_file(0, "game.bin", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); -} - -static void test_hash_3do_launchme_case_insensitive() -{ - /* main executable for "Captain Quazar" is "launchme" */ - /* main executable for "Rise of the Robots" is "launchMe" */ - /* main executable for "Road Rash" is "LaunchMe" */ - /* main executable for "Sewer Shark" is "Launchme" */ - size_t image_size; - uint8_t* image = generate_3do_bin(1, 6543, &image_size); - char hash_file[33]; - const char* expected_md5 = "59622882e3261237e8a1e396825ae4f5"; - - memcpy(&image[2048 + 0x14 + 0x48 + 0x20], "launchme", 8); - mock_file(0, "game.bin", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); -} - -static void test_hash_3do_no_launchme() -{ - /* this case should not happen */ - size_t image_size; - uint8_t* image = generate_3do_bin(1, 6543, &image_size); - char hash_file[33]; - - memcpy(&image[2048 + 0x14 + 0x48 + 0x20], "filename", 8); - mock_file(0, "game.bin", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); -} - -static void test_hash_3do_long_directory() -{ - /* root directory for "Dragon's Lair" uses more than one sector */ - size_t image_size; - uint8_t* image = generate_3do_bin(3, 6543, &image_size); - char hash_file[33]; - const char* expected_md5 = "8979e876ae502e0f79218f7ff7bd8c2a"; - - mock_file(0, "game.bin", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_3DO, "game.bin"); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); -} - -/* ========================================================================= */ - -static void test_hash_atari_jaguar_cd() -{ - const char* cue_file = - "REM SESSION 01\n" - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "REM SESSION 02\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 01 00:00:00\n"; - size_t image_size; - uint8_t* image = generate_jaguarcd_bin(2, 60024, 0, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "c324d95dc5831c2d5c470eefb18c346b"; - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(1, "track02.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_atari_jaguar_cd_byteswapped() -{ - const char* cue_file = - "REM SESSION 01\n" - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "REM SESSION 02\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 01 00:00:00\n"; - size_t image_size; - uint8_t* image = generate_jaguarcd_bin(2, 60024, 1, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "c324d95dc5831c2d5c470eefb18c346b"; - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(1, "track02.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_atari_jaguar_cd_track3() -{ - const char* cue_file = - "REM SESSION 01\n" - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 01 00:00:00\n" - "REM SESSION 02\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 01 00:00:00\n"; - size_t image_size; - uint8_t* image = generate_jaguarcd_bin(1470, 99200, 1, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "060e9d223c584b581cf7d7ce17c0e5dc"; - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(1, "track03.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_atari_jaguar_cd_no_header() -{ - const char* cue_file = - "REM SESSION 01\n" - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "REM SESSION 02\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 01 00:00:00\n"; - size_t image_size; - uint8_t* image = generate_jaguarcd_bin(2, 32768, 1, &image_size); - char hash_file[33], hash_iterator[33]; - - image[2 + 64 + 12] = 'B'; /* corrupt the header */ - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(1, "track02.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); - ASSERT_NUM_EQUALS(result_iterator, 0); -} - -static void test_hash_atari_jaguar_cd_no_sessions() -{ - const char* cue_file = - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 01 00:00:00\n"; - size_t image_size; - uint8_t* image = generate_jaguarcd_bin(2, 99200, 1, &image_size); - char hash_file[33], hash_iterator[33]; - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(1, "track03.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); - ASSERT_NUM_EQUALS(result_iterator, 0); -} - -extern const char* _rc_hash_jaguar_cd_homebrew_hash; - -static void test_hash_atari_jaguar_cd_homebrew() -{ - /* Jaguar CD homebrew games all appear to have a common bootloader in the primary boot executable space. They only - * differ in a secondary executable in the second track (part of the first session). This doesn't appear to be - * intentional behavior based on the CD BIOS documentation, which states that all developer code should be in the - * first track of the second session. I speculate this is done to work around the authentication logic. */ - const char* cue_file = - "REM SESSION 01\n" - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 AUDIO\n" - " INDEX 01 00:00:00\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 01 00:00:00\n" - "REM SESSION 02\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 AUDIO\n" - " INDEX 01 00:00:00\n"; - size_t image_size, image_size2; - uint8_t* image = generate_jaguarcd_bin(2, 45760, 1, &image_size); - uint8_t* image2 = generate_jaguarcd_bin(2, 986742, 1, &image_size2); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "3fdf70e362c845524c9e447aacaed0a9"; - - image2[0x60] = 0x21; /* ATARI APPROVED DATA HEADER ATRI! */ - memcpy(&image2[0xA2], &image2[0x62], 8); /* addr / size */ - memcpy(&image2[0x62], "RTKARTKARTKARTKA", 16); /* KARTKARTKARTKART */ - memcpy(&image2[0x72], "RTKARTKARTKARTKA", 16); - memcpy(&image2[0x82], "RTKARTKARTKARTKA", 16); - memcpy(&image2[0x92], "RTKARTKARTKARTKA", 16); - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(2, "track02.bin", image2, image_size2); - mock_file(1, "track03.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual FIRST_OF_SECOND_SESSION calculation */ - _rc_hash_jaguar_cd_homebrew_hash = "4e4114b2675eff21bb77dd41e141ddd6"; /* mock the hash of the homebrew bootloader */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ATARI_JAGUAR_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - _rc_hash_jaguar_cd_homebrew_hash = NULL; - free(image); - free(image2); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static void test_hash_dreamcast_single_bin() -{ - size_t image_size; - uint8_t* image = generate_dreamcast_bin(45000, 1458208, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "2a550500caee9f06e5d061fe10a46f6e"; - - mock_file(0, "track03.bin", image, image_size); - mock_file_first_sector(0, 45000); - mock_file(1, "game.gdi", (uint8_t*)"game.bin", 8); - mock_cd_num_tracks(3); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.gdi"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.gdi", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_dreamcast_split_bin() -{ - size_t image_size; - uint8_t* image = generate_dreamcast_bin(548106, 1830912, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "771e56aff169230ede4505013a4bcf9f"; - - mock_file(0, "game.gdi", (uint8_t*)"game.bin", 8); - mock_file(1, "track03.bin", image, image_size); - mock_file_first_sector(1, 45000); - mock_file(2, "track26.bin", image, image_size); - mock_file_first_sector(2, 548106); - mock_cd_num_tracks(26); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.gdi"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.gdi", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_dreamcast_cue() -{ - const char* cue_file = - "FILE \"track01.bin\" BINARY\n" - " TRACK 01 MODE1/2352\n" - " INDEX 01 00:00:00\n" - "FILE \"track02.bin\" BINARY\n" - " TRACK 02 AUDIO\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n" - "FILE \"track03.bin\" BINARY\n" - " TRACK 03 MODE1/2352\n" - " INDEX 01 00:00:00\n" - "FILE \"track04.bin\" BINARY\n" - " TRACK 04 AUDIO\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:02:00\n" - "FILE \"track05.bin\" BINARY\n" - " TRACK 05 MODE1/2352\n" - " INDEX 00 00:00:00\n" - " INDEX 01 00:03:00\n"; - size_t image_size; - uint8_t* image = convert_to_2352(generate_dreamcast_bin(45000, 1697028, &image_size), &image_size, 45000); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "c952864c3364591d2a8793ce2cfbf3a0"; - - mock_file(0, "game.cue", (uint8_t*)cue_file, strlen(cue_file)); - mock_file(1, "track01.bin", image, 1425312); /* 606 sectors */ - mock_file(2, "track02.bin", image, 1589952); /* 676 sectors */ - mock_file(3, "track03.bin", image, image_size); /* 737 sectors */ - mock_file(4, "track04.bin", image, 1237152); /* 526 sectors */ - mock_file(5, "track05.bin", image, image_size); - - rc_hash_init_default_cdreader(); /* want to test actual first_track_sector calculation */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_DREAMCAST, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - init_mock_cdreader(); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static void test_hash_gamecube() -{ - size_t image_size; - uint8_t* image = generate_gamecube_iso(32, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "c7803b704fa43d22d8f6e55f4789cb45"; - - mock_file(0, "test.iso", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_GAMECUBE, "test.iso"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "test.iso", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); - - ASSERT_NUM_EQUALS(image_size, 32 * 1024 * 1024); -} - -/* ========================================================================= */ - -static void test_hash_neogeocd() -{ - const char* ipl_txt = "FIXA.FIX,0,0\r\nPROG.PRG,0,0\r\nSOUND.PCM,0,0\r\n\x1a"; - const size_t prog_prg_size = 273470; - uint8_t* prog_prg = generate_generic_file(prog_prg_size); - size_t image_size; - uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "96f35b20c6cf902286da45e81a50b2a3"; - - generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); - generate_iso9660_file(image, "PROG.PRG", prog_prg, prog_prg_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - free(prog_prg); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_neogeocd_multiple_prg() -{ - const char* ipl_txt = "FIXA.FIX,0,0\r\nPROG1.PRG,0,0\r\nSOUND.PCM,0,0\r\nPROG2.PRG,0,44000\r\n\x1a"; - const size_t prog1_prg_size = 273470; - uint8_t* prog1_prg = generate_generic_file(prog1_prg_size); - const size_t prog2_prg_size = 13768; - uint8_t* prog2_prg = generate_generic_file(prog2_prg_size); - size_t image_size; - uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "d62df483c4786d3c63f27b6c5f17eeca"; - - generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); - generate_iso9660_file(image, "PROG1.PRG", prog1_prg, prog1_prg_size); - generate_iso9660_file(image, "PROG2.PRG", prog2_prg, prog2_prg_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - free(prog1_prg); - free(prog2_prg); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_neogeocd_lowercase_ipl_contents() -{ - const char* ipl_txt = "fixa.fix,0,0\r\nprog.prg,0,0\r\nsound.pcm,0,0\r\n\x1a"; - const size_t prog_prg_size = 273470; - uint8_t* prog_prg = generate_generic_file(prog_prg_size); - size_t image_size; - uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "96f35b20c6cf902286da45e81a50b2a3"; - - generate_iso9660_file(image, "IPL.TXT", (uint8_t*)ipl_txt, strlen(ipl_txt)); - generate_iso9660_file(image, "PROG.PRG", prog_prg, prog_prg_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NEO_GEO_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - free(prog_prg); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static void test_hash_pce_cd() -{ - size_t image_size; - uint8_t* image = generate_pce_cd_bin(72, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "6565819195a49323e080e7539b54f251"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PC_ENGINE_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_pce_cd_invalid_header() -{ - size_t image_size; - uint8_t* image = generate_pce_cd_bin(72, &image_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* make the header not match */ - image[2048 + 0x24] = 0x34; - - test_hash_unknown_format(RC_CONSOLE_PC_ENGINE_CD, "game.cue"); - - free(image); -} - -/* ========================================================================= */ - -static void test_hash_pcfx() -{ - size_t image_size; - uint8_t* image = generate_pcfx_bin(72, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "0a03af66559b8529c50c4e7788379598"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PCFX, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_pcfx_invalid_header() -{ - size_t image_size; - uint8_t* image = generate_pcfx_bin(72, &image_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* make the header not match */ - image[12] = 0x34; - - test_hash_unknown_format(RC_CONSOLE_PCFX, "game.cue"); - - free(image); -} - -static void test_hash_pcfx_pce_cd() -{ - /* Battle Heat is formatted as a PC-Engine CD */ - size_t image_size; - uint8_t* image = generate_pce_cd_bin(72, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "6565819195a49323e080e7539b54f251"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - mock_file(2, "game2.bin", image, image_size); /* PC-Engine CD check only applies to track 2 */ - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PCFX, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_psx_cd() -{ - /* BOOT=cdrom:\SLUS_007.45 */ - size_t image_size; - uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_psx_cd_no_system_cnf() -{ - size_t image_size; - uint8_t* image; - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "e494c79a7315be0dc3e8571c45df162c"; - uint32_t binary_size = 0x12000; - const uint32_t sectors_needed = (((binary_size + 2047) / 2048) + 20); - uint8_t* exe; - - image = generate_iso9660_bin(sectors_needed, "HOMEBREW", &image_size); - exe = generate_iso9660_file(image, "PSX.EXE", NULL, binary_size); - memcpy(exe, "PS-X EXE", 8); - binary_size -= 2048; - exe[28] = binary_size & 0xFF; - exe[29] = (binary_size >> 8) & 0xFF; - exe[30] = (binary_size >> 16) & 0xFF; - exe[31] = (binary_size >> 24) & 0xFF; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_psx_cd_exe_in_subfolder() -{ - /* BOOT=cdrom:\bin\SLUS_012.37 */ - size_t image_size; - uint8_t* image = generate_psx_bin("bin\\SCES_012.37", 0x07D800, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "674018e23a4052113665dfb264e9c2fc"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_psx_cd_extra_slash() -{ - /* BOOT=cdrom:\\SLUS_007.45 */ - size_t image_size; - uint8_t* image = generate_psx_bin("\\SLUS_007.45", 0x07D800, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_ps2_iso() -{ - size_t image_size; - uint8_t* image = generate_ps2_bin("SLUS_200.64", 0x07D800, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "01a517e4ad72c6c2654d1b839be7579d"; - - mock_file(0, "game.iso", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION_2, "game.iso"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_ps2_psx() -{ - size_t image_size; - uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "db433fb038cde4fb15c144e8c7dea6e3"; - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PLAYSTATION_2, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); /* PSX hash */ - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation (should not generate PS2 hash for PSX file) */ - ASSERT_NUM_EQUALS(result_file, 0); - ASSERT_NUM_EQUALS(result_iterator, 0); -} - -static void test_hash_psp() -{ - const size_t param_sfo_size = 690; - uint8_t* param_sfo = generate_generic_file(param_sfo_size); - const size_t eboot_bin_size = 273470; - uint8_t* eboot_bin = generate_generic_file(eboot_bin_size); - size_t image_size; - uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "27ec2f9b7238b2ef29af31ddd254f201"; - - generate_iso9660_file(image, "PSP_GAME\\PARAM.SFO", param_sfo, param_sfo_size); - generate_iso9660_file(image, "PSP_GAME\\SYSDIR\\EBOOT.BIN", eboot_bin, eboot_bin_size); - - mock_file(0, "game.iso", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "game.iso"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - free(eboot_bin); - free(param_sfo); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_psp_video() -{ - const size_t param_sfo_size = 690; - uint8_t* param_sfo = generate_generic_file(param_sfo_size); - const size_t eboot_bin_size = 273470; - uint8_t* eboot_bin = generate_generic_file(eboot_bin_size); - size_t image_size; - uint8_t* image = generate_iso9660_bin(160, "TEST", &image_size); - char hash_file[33], hash_iterator[33]; - - /* UMD video disc may have an UPDATE folder, but nothing in the PSP_GAME or SYSDIR folders. */ - generate_iso9660_file(image, "PSP_GAME\\SYSDIR\\UPDATE\\EBOOT.BIN", eboot_bin, eboot_bin_size); - /* the PARAM.SFO file is in the UMD_VIDEO folder. */ - generate_iso9660_file(image, "UMD_VIDEO\\PARAM.SFO", param_sfo, param_sfo_size); - - mock_file(0, "game.iso", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "game.iso"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.iso", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - free(eboot_bin); - free(param_sfo); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 0); - ASSERT_NUM_EQUALS(result_iterator, 0); -} - -static void test_hash_psp_homebrew() -{ - const size_t image_size = 3532124; - uint8_t* image = generate_generic_file(image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "fcde8760893b09e508e5f4fe642eb132"; - - mock_file(0, "eboot.pbp", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_PSP, "eboot.pbp"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "eboot.pbp", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_sega_cd() -{ - /* the first 512 bytes of sector 0 are a volume header and ROM header. - * generate a generic block and add the Sega CD marker */ - size_t image_size = 512; - uint8_t* image = generate_generic_file(image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "574498e1453cb8934df60c4ab906e783"; - memcpy(image, "SEGADISCSYSTEM ", 16); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SEGA_CD, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_sega_cd_invalid_header() -{ - size_t image_size = 512; - uint8_t* image = generate_generic_file(image_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - test_hash_unknown_format(RC_CONSOLE_SEGA_CD, "game.cue"); - - free(image); -} - -static void test_hash_saturn() -{ - /* the first 512 bytes of sector 0 are a volume header and ROM header. - * generate a generic block and add the Sega CD marker */ - size_t image_size = 512; - uint8_t* image = generate_generic_file(image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "4cd9c8e41cd8d137be15bbe6a93ae1d8"; - memcpy(image, "SEGA SEGASATURN ", 16); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SATURN, "game.cue"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cue", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_saturn_invalid_header() -{ - size_t image_size = 512; - uint8_t* image = generate_generic_file(image_size); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)"game.bin", 8); - - test_hash_unknown_format(RC_CONSOLE_SATURN, "game.cue"); - - free(image); -} - -/* ========================================================================= */ - -void test_hash_disc(void) { - TEST_SUITE_BEGIN(); - - init_mock_filereader(); - init_mock_cdreader(); - - /* 3DO */ - TEST(test_hash_3do_bin); - TEST(test_hash_3do_cue); - TEST(test_hash_3do_iso); - TEST(test_hash_3do_invalid_header); - TEST(test_hash_3do_launchme_case_insensitive); - TEST(test_hash_3do_no_launchme); - TEST(test_hash_3do_long_directory); - - /* Amstrad CPC */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_AMSTRAD_PC, "test.dsk", 194816, "9d616e4ad3f16966f61422c57e22aadd"); - - /* Apple II */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.nib", 232960, "96e8d33bdc385fd494327d6e6791cbe4"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_APPLE_II, "test.dsk", 143360, "88be638f4d78b4072109e55f13e8a0ac"); - - /* Atari Jaguar CD */ - TEST(test_hash_atari_jaguar_cd); - TEST(test_hash_atari_jaguar_cd_byteswapped); - TEST(test_hash_atari_jaguar_cd_track3); - TEST(test_hash_atari_jaguar_cd_no_header); - TEST(test_hash_atari_jaguar_cd_no_sessions); - TEST(test_hash_atari_jaguar_cd_homebrew); - - /* Commodore 64 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.nib", 327936, "e7767d32b23e3fa62c5a250a08caeba3"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_COMMODORE_64, "test.d64", 174848, "ecd5a8ef4e77f2e9469d9b6e891394f0"); - - /* Dreamcast */ - TEST(test_hash_dreamcast_single_bin); - TEST(test_hash_dreamcast_split_bin); - TEST(test_hash_dreamcast_cue); - - /* Gamecube */ - TEST(test_hash_gamecube); - - /* MSX */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MSX, "test.dsk", 737280, "0e73fe94e5f2e2d8216926eae512b7a6"); - - /* Neo Geo CD */ - TEST(test_hash_neogeocd); - TEST(test_hash_neogeocd_multiple_prg); - TEST(test_hash_neogeocd_lowercase_ipl_contents); - - /* PC-8800 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_PC8800, "test.d88", 348288, "8cca4121bf87200f45e91b905a9f5afd"); - - /* PC Engine CD */ - TEST(test_hash_pce_cd); - TEST(test_hash_pce_cd_invalid_header); - - /* PC-FX */ - TEST(test_hash_pcfx); - TEST(test_hash_pcfx_invalid_header); - TEST(test_hash_pcfx_pce_cd); - - /* Playstation */ - TEST(test_hash_psx_cd); - TEST(test_hash_psx_cd_no_system_cnf); - TEST(test_hash_psx_cd_exe_in_subfolder); - TEST(test_hash_psx_cd_extra_slash); - - /* Playstation 2 */ - TEST(test_hash_ps2_iso); - TEST(test_hash_ps2_psx); - - /* Playstation Portable */ - TEST(test_hash_psp); - TEST(test_hash_psp_video); - TEST(test_hash_psp_homebrew); - - /* Sega CD */ - TEST(test_hash_sega_cd); - TEST(test_hash_sega_cd_invalid_header); - - /* Sega Saturn */ - TEST(test_hash_saturn); - TEST(test_hash_saturn_invalid_header); - - /* ZX Spectrum */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tap", 1596, "714a9f455e616813dd5421c5b347e5e5"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ZX_SPECTRUM, "test.tzx", 14971, "93723e6d1100f9d1d448a27cf6618c47"); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rhash/test_hash_rom.c b/src/rcheevos/test/rhash/test_hash_rom.c deleted file mode 100644 index 323dda2b12..0000000000 --- a/src/rcheevos/test/rhash/test_hash_rom.c +++ /dev/null @@ -1,899 +0,0 @@ -#include "rc_hash.h" - -#include "../rhash/rc_hash_internal.h" - -#include "../rc_compat.h" -#include "../test_framework.h" -#include "data.h" -#include "mock_filereader.h" - -#include - -/* in test_hash.c */ -void test_hash_full_file(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); -void test_hash_m3u(uint32_t console_id, const char* filename, size_t size, const char* expected_md5); - -/* ========================================================================= */ - -static void test_hash_arcade(const char* path, const char* expected_md5) -{ - char hash_file[33], hash_iterator[33]; - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARCADE, path); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, path, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static void test_hash_arduboy() -{ - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; - - const char* hex_input = - ":100000000C94690D0C94910D0C94910D0C94910D20\n" - ":100010000C94910D0C94910D0C94910D0C94910DE8\n" - ":100020000C94910D0C94910D0C94C32A0C94352BC7\n" - ":00000001FF\n"; - mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_arduboy_crlf() -{ - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; - - const char* hex_input = - ":100000000C94690D0C94910D0C94910D0C94910D20\r\n" - ":100010000C94910D0C94910D0C94910D0C94910DE8\r\n" - ":100020000C94910D0C94910D0C94C32A0C94352BC7\r\n" - ":00000001FF\r\n"; - mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_arduboy_no_final_lf() -{ - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "67b64633285a7f965064ba29dab45148"; - - const char* hex_input = - ":100000000C94690D0C94910D0C94910D0C94910D20\n" - ":100010000C94910D0C94910D0C94910D0C94910DE8\n" - ":100020000C94910D0C94910D0C94C32A0C94352BC7\n" - ":00000001FF"; - mock_file(0, "game.hex", (const uint8_t*)hex_input, strlen(hex_input)); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.hex"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.hex", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static uint8_t* generate_atari_7800_file(size_t kb, int with_header, size_t* image_size) -{ - uint8_t* image; - size_t size_needed = kb * 1024; - if (with_header) - size_needed += 128; - - image = (uint8_t*)calloc(size_needed, 1); - if (image != NULL) { - if (with_header) { - const uint8_t header[128] = { - 3, 'A', 'T', 'A', 'R', 'I', '7', '8', '0', '0', 0, 0, 0, 0, 0, 0, /* version + magic text */ - 0, 'G', 'a', 'm', 'e', 'N', 'a', 'm', 'e', 0, 0, 0, 0, 0, 0, 0, /* game name */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* game name (cont'd) */ - 0, 0, 2, 0, 0, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, /* attributes */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* unused */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* unused */ - 0, 0, 0, 0, 'A', 'C', 'T', 'U', 'A', 'L', ' ', 'C', 'A', 'R', 'T',/* magic text*/ - 'D', 'A', 'T', 'A', ' ', 'S', 'T', 'A', 'R', 'T', 'S', ' ', 'H', 'E', 'R', 'E' /* magic text */ - }; - memcpy(image, header, sizeof(header)); - image[50] = (uint8_t)(kb / 4); /* 4-byte value starting at address 49 is the ROM size without header */ - - fill_image(image + 128, size_needed - 128); - } - else { - fill_image(image, size_needed); - } - } - - if (image_size) - *image_size = size_needed; - return image; -} - -static void test_hash_atari_7800() -{ - size_t image_size; - uint8_t* image = generate_atari_7800_file(16, 0, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_ATARI_7800, image, image_size); - free(image); - - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "455f07d8500f3fabc54906737866167f"); - ASSERT_NUM_EQUALS(image_size, 16384); -} - -static void test_hash_atari_7800_with_header() -{ - size_t image_size; - uint8_t* image = generate_atari_7800_file(16, 1, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_ATARI_7800, image, image_size); - free(image); - - /* NOTE: expectation is that this hash matches the hash in test_hash_atari_7800 */ - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "455f07d8500f3fabc54906737866167f"); - ASSERT_NUM_EQUALS(image_size, 16384 + 128); -} - -/* ========================================================================= */ - -uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size) -{ - uint8_t* image; - size_t size_needed = kb * 1024; - if (with_header) - size_needed += 16; - - image = (uint8_t*)calloc(size_needed, 1); - if (image != NULL) { - if (with_header) { - image[0] = 'N'; - image[1] = 'E'; - image[2] = 'S'; - image[3] = '\x1A'; - image[4] = (uint8_t)(kb / 16); - - fill_image(image + 16, size_needed - 16); - } - else { - fill_image(image, size_needed); - } - } - - if (image_size) - *image_size = size_needed; - return image; -} - -static uint8_t* generate_fds_file(size_t sides, int with_header, size_t* image_size) -{ - uint8_t* image; - size_t size_needed = sides * 65500; - if (with_header) - size_needed += 16; - - image = (uint8_t*)calloc(size_needed, 1); - if (image != NULL) { - if (with_header) { - image[0] = 'F'; - image[1] = 'D'; - image[2] = 'S'; - image[3] = '\x1A'; - image[4] = (uint8_t)sides; - - fill_image(image + 16, size_needed - 16); - } - else { - fill_image(image, size_needed); - } - } - - if (image_size) - *image_size = size_needed; - return image; -} - -static void test_hash_nes_32k() -{ - size_t image_size; - uint8_t* image = generate_nes_file(32, 0, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); - free(image); - - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_NUM_EQUALS(image_size, 32768); -} - -static void test_hash_nes_32k_with_header() -{ - size_t image_size; - uint8_t* image = generate_nes_file(32, 1, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); - free(image); - - /* NOTE: expectation is that this hash matches the hash in test_hash_nes_32k */ - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_NUM_EQUALS(image_size, 32768 + 16); -} - -static void test_hash_nes_256k() -{ - size_t image_size; - uint8_t* image = generate_nes_file(256, 0, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); - free(image); - - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "545d527301b8ae148153988d6c4fcb84"); - ASSERT_NUM_EQUALS(image_size, 262144); -} - -static void test_hash_fds_two_sides() -{ - size_t image_size; - uint8_t* image = generate_fds_file(2, 0, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); - free(image); - - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "fd770d4d34c00760fabda6ad294a8f0b"); - ASSERT_NUM_EQUALS(image_size, 65500 * 2); -} - -static void test_hash_fds_two_sides_with_header() -{ - size_t image_size; - uint8_t* image = generate_fds_file(2, 1, &image_size); - char hash[33]; - int result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO, image, image_size); - free(image); - - /* NOTE: expectation is that this hash matches the hash in test_hash_fds_two_sides */ - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "fd770d4d34c00760fabda6ad294a8f0b"); - ASSERT_NUM_EQUALS(image_size, 65500 * 2 + 16); -} - -static void test_hash_nes_file_32k() -{ - size_t image_size; - uint8_t* image = generate_nes_file(32, 0, &image_size); - char hash[33]; - int result; - - mock_file(0, "test.nes", image, image_size); - result = rc_hash_generate_from_file(hash, RC_CONSOLE_NINTENDO, "test.nes"); - free(image); - - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_NUM_EQUALS(image_size, 32768); -} - -static void test_hash_nes_iterator_32k() -{ - size_t image_size; - uint8_t* image = generate_nes_file(32, 0, &image_size); - char hash1[33], hash2[33]; - int result1, result2; - struct rc_hash_iterator iterator; - - mock_file(0, "test.nes", image, image_size); - rc_hash_initialize_iterator(&iterator, "test.nes", NULL, 0); - result1 = rc_hash_iterate(hash1, &iterator); - result2 = rc_hash_iterate(hash2, &iterator); - rc_hash_destroy_iterator(&iterator); - free(image); - - ASSERT_NUM_EQUALS(result1, 1); - ASSERT_STR_EQUALS(hash1, "6a2305a2b6675a97ff792709be1ca857"); - - ASSERT_NUM_EQUALS(result2, 0); - ASSERT_STR_EQUALS(hash2, ""); -} - -static void test_hash_nes_file_iterator_32k() -{ - size_t image_size; - uint8_t* image = generate_nes_file(32, 0, &image_size); - char hash1[33], hash2[33]; - int result1, result2; - struct rc_hash_iterator iterator; - rc_hash_initialize_iterator(&iterator, "test.nes", image, image_size); - result1 = rc_hash_iterate(hash1, &iterator); - result2 = rc_hash_iterate(hash2, &iterator); - rc_hash_destroy_iterator(&iterator); - free(image); - - ASSERT_NUM_EQUALS(result1, 1); - ASSERT_STR_EQUALS(hash1, "6a2305a2b6675a97ff792709be1ca857"); - - ASSERT_NUM_EQUALS(result2, 0); - ASSERT_STR_EQUALS(hash2, ""); -} - -/* ========================================================================= */ - -/* first 64 bytes of SUPER MARIO 64 ROM in each N64 format */ -static uint8_t test_rom_z64[64] = { - 0x80, 0x37, 0x12, 0x40, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x24, 0x60, 0x00, 0x00, 0x00, 0x14, 0x44, - 0x63, 0x5A, 0x2B, 0xFF, 0x8B, 0x02, 0x23, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x53, 0x55, 0x50, 0x45, 0x52, 0x20, 0x4D, 0x41, 0x52, 0x49, 0x4F, 0x20, 0x36, 0x34, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x53, 0x4D, 0x45, 0x00 -}; - -static uint8_t test_rom_v64[64] = { - 0x37, 0x80, 0x40, 0x12, 0x00, 0x00, 0x0F, 0x00, 0x24, 0x80, 0x00, 0x60, 0x00, 0x00, 0x44, 0x14, - 0x5A, 0x63, 0xFF, 0x2B, 0x02, 0x8B, 0x26, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x55, 0x53, 0x45, 0x50, 0x20, 0x52, 0x41, 0x4D, 0x49, 0x52, 0x20, 0x4F, 0x34, 0x36, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x4D, 0x53, 0x00, 0x45 -}; - -static uint8_t test_rom_n64[64] = { - 0x40, 0x12, 0x37, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x60, 0x24, 0x80, 0x44, 0x14, 0x00, 0x00, - 0xFF, 0x2B, 0x5A, 0x63, 0x26, 0x23, 0x02, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x45, 0x50, 0x55, 0x53, 0x41, 0x4D, 0x20, 0x52, 0x20, 0x4F, 0x49, 0x52, 0x20, 0x20, 0x34, 0x36, - 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x45, 0x4D, 0x53 -}; - -/* first 64 bytes of DOSHIN THE GIANT in ndd format */ -static uint8_t test_rom_ndd[64] = { - 0xE8, 0x48, 0xD3, 0x16, 0x10, 0x13, 0x00, 0x45, 0x0C, 0x18, 0x24, 0x30, 0x3C, 0x48, 0x54, 0x60, - 0x6C, 0x78, 0x84, 0x90, 0x9C, 0xA8, 0xB4, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x02, 0x5C, 0x00, - 0x10, 0x16, 0x1C, 0x22, 0x28, 0x2A, 0x31, 0x32, 0x3A, 0x40, 0x46, 0x4C, 0x04, 0x0C, 0x14, 0x1C, - 0x24, 0x2C, 0x34, 0x3C, 0x44, 0x4C, 0x54, 0x5C, 0x04, 0x0C, 0x14, 0x1C, 0x24, 0x2C, 0x34, 0x3C -}; - -static void test_hash_n64(uint8_t* buffer, size_t buffer_size, const char* expected_hash) -{ - char hash[33]; - int result; - - rc_hash_reset_filereader(); /* explicitly unset the filereader */ - result = rc_hash_generate_from_buffer(hash, RC_CONSOLE_NINTENDO_64, buffer, buffer_size); - init_mock_filereader(); /* restore the mock filereader */ - - ASSERT_NUM_EQUALS(result, 1); - ASSERT_STR_EQUALS(hash, expected_hash); -} - -static void test_hash_n64_file(const char* filename, uint8_t* buffer, size_t buffer_size, const char* expected_hash) -{ - char hash_file[33], hash_iterator[33]; - mock_file(0, filename, buffer, buffer_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_64, filename); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, filename, NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_hash); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_hash); -} - -/* ========================================================================= */ - -static uint8_t* generate_nds_file(size_t mb, uint32_t arm9_size, uint32_t arm7_size, size_t* image_size) -{ - uint8_t* image; - const size_t size_needed = mb * 1024 * 1024; - - image = (uint8_t*)calloc(size_needed, 1); - if (image != NULL) { - uint32_t arm9_addr = 65536; - uint32_t arm7_addr = arm9_addr + arm9_size; - uint32_t icon_addr = arm7_addr + arm7_size; - - fill_image(image, size_needed); - - image[0x20] = (arm9_addr & 0xFF); - image[0x21] = ((arm9_addr >> 8) & 0xFF); - image[0x22] = ((arm9_addr >> 16) & 0xFF); - image[0x23] = ((arm9_addr >> 24) & 0xFF); - image[0x2C] = (arm9_size & 0xFF); - image[0x2D] = ((arm9_size >> 8) & 0xFF); - image[0x2E] = ((arm9_size >> 16) & 0xFF); - image[0x2F] = ((arm9_size >> 24) & 0xFF); - - image[0x30] = (arm7_addr & 0xFF); - image[0x31] = ((arm7_addr >> 8) & 0xFF); - image[0x32] = ((arm7_addr >> 16) & 0xFF); - image[0x33] = ((arm7_addr >> 24) & 0xFF); - image[0x3C] = (arm7_size & 0xFF); - image[0x3D] = ((arm7_size >> 8) & 0xFF); - image[0x3E] = ((arm7_size >> 16) & 0xFF); - image[0x3F] = ((arm7_size >> 24) & 0xFF); - - image[0x68] = (icon_addr & 0xFF); - image[0x69] = ((icon_addr >> 8) & 0xFF); - image[0x6A] = ((icon_addr >> 16) & 0xFF); - image[0x6B] = ((icon_addr >> 24) & 0xFF); - } - - if (image_size) - *image_size = size_needed; - return image; -} - -static void test_hash_nds() -{ - size_t image_size; - uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; - - mock_file(0, "game.nds", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DS, "game.nds"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_hash); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_hash); -} - -static void test_hash_nds_supercard() -{ - size_t image_size, image2_size; - uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; - ASSERT_PTR_NOT_NULL(image); - ASSERT_NUM_GREATER(image_size, 0); - - /* inject the SuperCard header (512 bytes) */ - image2_size = image_size + 512; - uint8_t* image2 = malloc(image2_size); - ASSERT_PTR_NOT_NULL(image2); - memcpy(&image2[512], &image[0], image_size); - memset(&image2[0], 0, 512); - image2[0] = 0x2E; - image2[1] = 0x00; - image2[2] = 0x00; - image2[3] = 0xEA; - image2[0xB0] = 0x44; - image2[0xB1] = 0x46; - image2[0xB2] = 0x96; - image2[0xB3] = 0x00; - - mock_file(0, "game.nds", image2, image2_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DS, "game.nds"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - free(image2); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_hash); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_hash); -} - -static void test_hash_nds_buffered() -{ - size_t image_size; - uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); - char hash_buffer[33], hash_iterator[33]; - const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; - - /* test file hash */ - int result_buffer = rc_hash_generate_from_buffer(hash_buffer, RC_CONSOLE_NINTENDO_DS, image, image_size); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.nds", image, image_size); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_buffer, 1); - ASSERT_STR_EQUALS(hash_buffer, expected_hash); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_hash); -} - -/* ========================================================================= */ - -static void test_hash_dsi() -{ - size_t image_size; - uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; - - mock_file(0, "game.nds", image, image_size); - - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_NINTENDO_DSI, "game.nds"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.nds", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_hash); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_hash); -} - -static void test_hash_dsi_buffered() -{ - size_t image_size; - uint8_t* image = generate_nds_file(2, 1234567, 654321, &image_size); - char hash_buffer[33], hash_iterator[33]; - const char* expected_hash = "56b30c276cba4affa886bd38e8e34d7e"; - - /* test file hash */ - int result_buffer = rc_hash_generate_from_buffer(hash_buffer, RC_CONSOLE_NINTENDO_DSI, image, image_size); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.nds", image, image_size); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_buffer, 1); - ASSERT_STR_EQUALS(hash_buffer, expected_hash); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_hash); -} - -/* ========================================================================= */ - -static void test_hash_scv_cart() -{ - size_t image_size = 32768 + 32; - uint8_t* image = generate_generic_file(image_size); - char hash_file[33], hash_iterator[33]; - const char* expected_md5 = "4309c9844b44f9ff8256dfc04687b8fd"; - - memcpy(image, "EmuSCV....CART..................", 32); - - mock_file(0, "game.cart", image, image_size); - /* test file hash */ - int result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_SUPER_CASSETTEVISION, "game.cart"); - - /* test file identification from iterator */ - int result_iterator; - struct rc_hash_iterator iterator; - - rc_hash_initialize_iterator(&iterator, "game.cart", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* cleanup */ - free(image); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -void test_hash_rom(void) { - TEST_SUITE_BEGIN(); - - init_mock_filereader(); - - /* Arcade */ - TEST_PARAMS2(test_hash_arcade, "game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "game.7z", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "roms\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "C:\\roms\\game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "/home/user/roms/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "/home/user/games/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "/home/user/roms/game.7z", "c8d46d341bea4fd5bff866a65ff8aea9"); - - TEST_PARAMS2(test_hash_arcade, "/home/user/nes_game.zip", "9b7aad36b365712fc93728088de4c209"); - TEST_PARAMS2(test_hash_arcade, "/home/user/nes/game.zip", "9b7aad36b365712fc93728088de4c209"); - TEST_PARAMS2(test_hash_arcade, "C:\\roms\\nes\\game.zip", "9b7aad36b365712fc93728088de4c209"); - TEST_PARAMS2(test_hash_arcade, "C:\\roms\\NES\\game.zip", "9b7aad36b365712fc93728088de4c209"); - TEST_PARAMS2(test_hash_arcade, "nes\\game.zip", "9b7aad36b365712fc93728088de4c209"); - TEST_PARAMS2(test_hash_arcade, "/home/user/snes/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - TEST_PARAMS2(test_hash_arcade, "/home/user/nes2/game.zip", "c8d46d341bea4fd5bff866a65ff8aea9"); - - /* we don't care that multiple aliases for the same system generate different hashes - the point is - * that they don't generate the same hash as an actual arcade ROM with the same filename. */ - TEST_PARAMS2(test_hash_arcade, "/home/user/chf/game.zip", "6ef57f16562ea0c7f49d93853b313e32"); - TEST_PARAMS2(test_hash_arcade, "/home/user/channelf/game.zip", "7b6506637a0cc79bd1d24a43a34fa3b9"); - TEST_PARAMS2(test_hash_arcade, "/home/user/coleco/game.zip", "c546f63ae7de98add4b9f221a4749260"); - TEST_PARAMS2(test_hash_arcade, "/home/user/colecovision/game.zip", "47279207b94dbf2a45cb13efa56d685e"); - TEST_PARAMS2(test_hash_arcade, "/home/user/msx/game.zip", "59ab85f6b56324fd81b4e324b804c29f"); - TEST_PARAMS2(test_hash_arcade, "/home/user/msx1/game.zip", "33328d832dcb0854383cdd4a4565c459"); - TEST_PARAMS2(test_hash_arcade, "/home/user/pce/game.zip", "c414a783f3983bbe2e9e01d9d5320c7e"); - TEST_PARAMS2(test_hash_arcade, "/home/user/pcengine/game.zip", "49370c3cbe98bdcdce545c68379487db"); - TEST_PARAMS2(test_hash_arcade, "/home/user/sgx/game.zip", "db545ab29694bfda1010317d4bac83b8"); - TEST_PARAMS2(test_hash_arcade, "/home/user/supergrafx/game.zip", "5665c9ef4c2f6609d8e420c4d86ba692"); - TEST_PARAMS2(test_hash_arcade, "/home/user/tg16/game.zip", "8b6c5c2e54915be2cdba63973862e143"); - TEST_PARAMS2(test_hash_arcade, "/home/user/fds/game.zip", "c0c135a97e8c577cfdf9204823ff211f"); - TEST_PARAMS2(test_hash_arcade, "/home/user/gamegear/game.zip", "f6f471e952b8103032b723f57bdbe767"); - TEST_PARAMS2(test_hash_arcade, "/home/user/mastersystem/game.zip", "f4805afe0ff5647140a26bd0a1057373"); - TEST_PARAMS2(test_hash_arcade, "/home/user/sms/game.zip", "43f35f575dead94dd2f42f9caf69fe5a"); - TEST_PARAMS2(test_hash_arcade, "/home/user/megadriv/game.zip", "f99d0aaf12ba3eb6ced9878c76692c63"); - TEST_PARAMS2(test_hash_arcade, "/home/user/megadrive/game.zip", "73eb5d7034b382093b1d36414d9e84e4"); - TEST_PARAMS2(test_hash_arcade, "/home/user/genesis/game.zip", "b62f810c63e1cba7f5b7569643bec236"); - TEST_PARAMS2(test_hash_arcade, "/home/user/sg1000/game.zip", "e8f6c711c4371f09537b4f2a7a304d6c"); - TEST_PARAMS2(test_hash_arcade, "/home/user/spectrum/game.zip", "a5f62157b2617bd728c4b1bc885c29e9"); - TEST_PARAMS2(test_hash_arcade, "/home/user/ngp/game.zip", "d4133b74c4e57274ca514e27a370dcb6"); - - /* Arduboy */ - TEST(test_hash_arduboy); - TEST(test_hash_arduboy_crlf); - TEST(test_hash_arduboy_no_final_lf); - - /* Arcadia 2001 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ARCADIA_2001, "test.bin", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); - - /* Atari 2600 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ATARI_2600, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); - - /* Atari 7800 */ - TEST(test_hash_atari_7800); - TEST(test_hash_atari_7800_with_header); - - /* Atari Jaguar */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ATARI_JAGUAR, "test.jag", 0x400000, "a247ec8a8c42e18fcb80702dfadac14b"); - - /* Colecovision */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_COLECOVISION, "test.col", 16384, "455f07d8500f3fabc54906737866167f"); - - /* Elektor TV Games Computer */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, "test.pgm", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER, "test.tvc", 1861, "37097124a29aff663432d049654a17dc"); - - /* Fairchild Channel F */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_FAIRCHILD_CHANNEL_F, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_FAIRCHILD_CHANNEL_F, "test.chf", 2048, "02c3f2fa186388ba8eede9147fb431c4"); - - /* Gameboy */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY, "test.gb", 131072, "a0f425b23200568132ba76b2405e3933"); - - /* Gameboy Color */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY_COLOR, "test.gbc", 2097152, "cf86acf519625a25a17b1246975e90ae"); - - /* Gameboy Advance */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAMEBOY_COLOR, "test.gba", 4194304, "a247ec8a8c42e18fcb80702dfadac14b"); - - /* Game Gear */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_GAME_GEAR, "test.gg", 524288, "68f0f13b598e0b66461bc578375c3888"); - - /* Intellivision */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_INTELLIVISION, "test.bin", 8192, "ce1127f881b40ce6a67ecefba50e2835"); - - /* Interton VC 4000 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_INTERTON_VC_4000, "test.bin", 2048, "02c3f2fa186388ba8eede9147fb431c4"); - - /* Magnavox Odyssey 2 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MAGNAVOX_ODYSSEY2, "test.bin", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); - - /* Master System */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MASTER_SYSTEM, "test.sms", 131072, "a0f425b23200568132ba76b2405e3933"); - - /* Mega Drive */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MEGA_DRIVE, "test.md", 1048576, "da9461b3b0f74becc3ccf6c2a094c516"); - TEST_PARAMS4(test_hash_m3u, RC_CONSOLE_MEGA_DRIVE, "test.md", 1048576, "da9461b3b0f74becc3ccf6c2a094c516"); - - /* Mega Duck */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_MEGADUCK, "test.bin", 65536, "8e6576cd5c21e44e0bbfc4480577b040"); - - /* Neo Geo Pocket */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_NEOGEO_POCKET, "test.ngc", 2097152, "cf86acf519625a25a17b1246975e90ae"); - - /* NES */ - TEST(test_hash_nes_32k); - TEST(test_hash_nes_32k_with_header); - TEST(test_hash_nes_256k); - TEST(test_hash_fds_two_sides); - TEST(test_hash_fds_two_sides_with_header); - - TEST(test_hash_nes_file_32k); - TEST(test_hash_nes_file_iterator_32k); - TEST(test_hash_nes_iterator_32k); - - /* Nintendo 64 */ - TEST_PARAMS3(test_hash_n64, test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); - TEST_PARAMS3(test_hash_n64, test_rom_v64, sizeof(test_rom_v64), "06096d7ce21cb6bcde38391534c4eb91"); - TEST_PARAMS3(test_hash_n64, test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); - TEST_PARAMS4(test_hash_n64_file, "game.z64", test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); - TEST_PARAMS4(test_hash_n64_file, "game.v64", test_rom_v64, sizeof(test_rom_v64), "06096d7ce21cb6bcde38391534c4eb91"); - TEST_PARAMS4(test_hash_n64_file, "game.n64", test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); - TEST_PARAMS4(test_hash_n64_file, "game.n64", test_rom_z64, sizeof(test_rom_z64), "06096d7ce21cb6bcde38391534c4eb91"); /* misnamed */ - TEST_PARAMS4(test_hash_n64_file, "game.z64", test_rom_n64, sizeof(test_rom_n64), "06096d7ce21cb6bcde38391534c4eb91"); /* misnamed */ - TEST_PARAMS3(test_hash_n64, test_rom_ndd, sizeof(test_rom_ndd), "a698b32a52970d8a52a5a52c83acc2a9"); - - /* Nintendo DS */ - TEST(test_hash_nds); - TEST(test_hash_nds_supercard); - TEST(test_hash_nds_buffered); - - /* Nintendo DSi */ - TEST(test_hash_dsi); - TEST(test_hash_dsi_buffered); - - /* Oric (no fixed file size) */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_ORIC, "test.tap", 18119, "953a2baa3232c63286aeae36b2172cef"); - - /* PC Engine */ - /* NOTE: because the data after the header doesn't match, the headered and non-headered hashes won't match - * but the test results ensure that we're only hashing the portion after the header when detected */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 524288, "68f0f13b598e0b66461bc578375c3888"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 524288 + 512, "258c93ebaca1c3f488ab48218e5e8d38"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_PC_ENGINE, "test.pce", 491520 + 512, "ebb565a7f964ccdfaecdce0d6ed540af"); - - /* Pokemon Mini */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_POKEMON_MINI, "test.min", 524288, "68f0f13b598e0b66461bc578375c3888"); - - /* Sega 32X */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SEGA_32X, "test.bin", 3145728, "07d733f252896ec41b4fd521fe610e2c"); - - /* SG-1000 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.sg", 32768, "6a2305a2b6675a97ff792709be1ca857"); - - /* SNES */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_NINTENDO, "test.smc", 524288, "68f0f13b598e0b66461bc578375c3888"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_NINTENDO, "test.smc", 524288 + 512, "258c93ebaca1c3f488ab48218e5e8d38"); - - /* Super Cassette Vision */ - TEST(test_hash_scv_cart); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPER_CASSETTEVISION, "test.bin", 32768, "6a2305a2b6675a97ff792709be1ca857"); - - /* TI-83 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TI83, "test.83g", 1695, "bfb6048395a425c69743900785987c42"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TI83, "test.83p", 2500, "6e81d530ee9a79d4f4f505729ad74bb5"); - - /* TIC-80 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_TIC80, "test.tic", 67682, "79b96f4ffcedb3ce8210a83b22cd2c69"); - - /* Uzebox */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_UZEBOX, "test.uze", 53654, "a9aab505e92edc034d3c732869159789"); - - /* Vectrex */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.vec", 4096, "572686c3a073162e4ec6eff86e6f6e3a"); - - /* VirtualBoy */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SG1000, "test.vb", 524288, "68f0f13b598e0b66461bc578375c3888"); - - /* Watara Supervision */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_SUPERVISION, "test.sv", 32768, "6a2305a2b6675a97ff792709be1ca857"); - - /* WASM-4 */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WASM4, "test.wasm", 33454, "bce38bb5f05622fc7e0e56757059d180"); - - /* WonderSwan */ - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WONDERSWAN, "test.ws", 524288, "68f0f13b598e0b66461bc578375c3888"); - TEST_PARAMS4(test_hash_full_file, RC_CONSOLE_WONDERSWAN, "test.wsc", 4194304, "a247ec8a8c42e18fcb80702dfadac14b"); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/rhash/test_hash_zip.c b/src/rcheevos/test/rhash/test_hash_zip.c deleted file mode 100644 index 10cc2040d5..0000000000 --- a/src/rcheevos/test/rhash/test_hash_zip.c +++ /dev/null @@ -1,551 +0,0 @@ -#include "rc_hash.h" - -#include "../rhash/rc_hash_internal.h" - -#include "../rc_compat.h" -#include "../test_framework.h" -#include "data.h" -#include "mock_filereader.h" - -#include - -typedef struct mock_zip_file_t { - uint8_t* buffer; - uint8_t* ptr; - uint8_t* file_ptr[8]; - uint32_t num_files; - uint8_t is_zip64; -} mock_zip_file_t; - -static void mock_zip_add_file(mock_zip_file_t* zip, const char* filename, uint32_t crc32, uint32_t size) -{ - const size_t filename_len = strlen(filename); - uint8_t* out = zip->ptr; - - zip->file_ptr[zip->num_files++] = out; - - /* local file signature */ - *out++ = 'P'; - *out++ = 'K'; - *out++ = 0x03; - *out++ = 0x04; - - /* version needed to extract */ - *out++ = 0x14; - *out++ = 0x00; - - /* general purpose bit flag*/ - *out++ = 0x02; - *out++ = 0x00; - - /* compression method */ - *out++ = 0x08; - *out++ = 0x00; - - /* file last modified time */ - *out++ = 0x00; - *out++ = 0xBC; - - /* file list modified date */ - *out++ = 0x98; - *out++ = 0x21; - - /* CRC-32 */ - *out++ = crc32 & 0xFF; - *out++ = (crc32 >> 8) & 0xFF; - *out++ = (crc32 >> 16) & 0xFF; - *out++ = (crc32 >> 24) & 0xFF; - - /* compressed size */ - *out++ = size & 0xFF; - *out++ = (size >> 8) & 0xFF; - *out++ = (size >> 16) & 0xFF; - *out++ = (size >> 24) & 0xFF; - - /* uncompressed size */ - *out++ = size & 0xFF; - *out++ = (size >> 8) & 0xFF; - *out++ = (size >> 16) & 0xFF; - *out++ = (size >> 24) & 0xFF; - - /* file name length */ - *out++ = filename_len & 0xFF; - *out++ = (filename_len >> 8) & 0xFF; - - /* extra field length */ - *out++ = 0; - *out++ = 0; - - /* file name */ - memcpy(out, filename, filename_len); - out += filename_len; - - /* compressed content */ - *out++ = 0x73; - *out++ = 0x02; - *out++ = 0x00; - - zip->ptr = out; -} - -static size_t mock_zip_finalize(mock_zip_file_t* zip, const char* comment) -{ - size_t comment_len = strlen(comment); - uint8_t* out = zip->ptr; - uint8_t* first_cdir_entry = zip->ptr; - size_t offset; - uint32_t filename_len; - uint32_t i; - - for (i = 0; i < zip->num_files; i++) { - uint8_t* in = zip->file_ptr[i]; - - /* central directory file header */ - *out++ = 'P'; - *out++ = 'K'; - *out++ = 0x01; - *out++ = 0x02; - - /* version made by */ - *out++ = 0x14; - *out++ = 0x00; - - /* version needed to extract (2) */ - /* general purpose bit flag (2) */ - /* compression method (2) */ - /* file last modified time (2) */ - /* file list modified date (2) */ - /* CRC-32 (4) */ - /* compressed size (4) */ - /* uncompressed size (4) */ - /* file name length (2) */ - /* extra field length (2) */ - memcpy(out, &in[4], 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2); - if (zip->is_zip64) { - /* in zip64 mode, blank out the size and the actual size will be appended in an extended field */ - memset(&out[14], 0xFF, 8); - out[24] = 0x14; /* extra field length */ - } - out += 2 + 2 + 2 + 2 + 2 + 4 + 4 + 4 + 2 + 2; - - /* file comment length */ - *out++ = 0; - *out++ = 0; - - /* disk number where file starts */ - *out++ = 0; - *out++ = 0; - - /* internal file attributes */ - *out++ = 0; - *out++ = 0; - - /* external file attributes */ - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* local file header offset */ - offset = in - zip->buffer; - *out++ = offset & 0xFF; - *out++ = (offset >> 8) & 0xFF; - *out++ = (offset >> 16) & 0xFF; - *out++ = (offset >> 24) & 0xFF; - - /* file name */ - filename_len = (in[27] << 8) | in[26]; - memcpy(out, &in[30], filename_len); - out += filename_len; - - if (zip->is_zip64) { - /* zip64 extended information extra field header id */ - *out++ = 0x01; - *out++ = 0x00; - - /* size of extra field chunk */ - *out++ = 0x10; /* only providing file sizes */ - *out++ = 0x00; - - /* uncompressed file size */ - memset(out, 0, 28); - memcpy(out, &in[22], 4); - out += 8; - - /* compressed file size */ - memset(out, 0, 16); - memcpy(out, &in[18], 4); - out += 8; - } - } - - zip->ptr = out; - - if (zip->is_zip64) { - /* end of central directory header */ - *out++ = 'P'; - *out++ = 'K'; - *out++ = 0x06; - *out++ = 0x06; - - /* size of EOCD64 minus 12 */ - *out++ = 0x2C; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* version made by */ - *out++ = 0x2D; - *out++ = 0x00; - - /* version needed to extract */ - *out++ = 0x2D; - *out++ = 0x00; - - /* disk number */ - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* disk number of central directory */ - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* number of central directory records on this disk */ - *out++ = zip->num_files & 0xFF; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* total number of central directory records */ - *out++ = zip->num_files & 0xFF; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* size of central directory */ - offset = zip->ptr - first_cdir_entry; - *out++ = offset & 0xFF; - *out++ = (offset >> 8) & 0xFF; - *out++ = (offset >> 16) & 0xFF; - *out++ = (offset >> 24) & 0xFF; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* address of first central directory entry */ - offset = first_cdir_entry - zip->buffer; - *out++ = offset & 0xFF; - *out++ = (offset >> 8) & 0xFF; - *out++ = (offset >> 16) & 0xFF; - *out++ = (offset >> 24) & 0xFF; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* end of central directory locator header */ - *out++ = 'P'; - *out++ = 'K'; - *out++ = 0x06; - *out++ = 0x07; - - /* disk number */ - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* address of central directory 64 */ - offset = zip->ptr - zip->buffer; - *out++ = offset & 0xFF; - *out++ = (offset >> 8) & 0xFF; - *out++ = (offset >> 16) & 0xFF; - *out++ = (offset >> 24) & 0xFF; - *out++ = 0; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - /* total number of disks */ - *out++ = 1; - *out++ = 0; - *out++ = 0; - *out++ = 0; - - zip->ptr = out; - } - - /* end of central directory header */ - *out++ = 'P'; - *out++ = 'K'; - *out++ = 0x05; - *out++ = 0x06; - - /* disk number */ - *out++ = 0; - *out++ = 0; - - /* central directory disk number */ - *out++ = 0; - *out++ = 0; - - /* number of central directory records on this disk */ - *out++ = zip->num_files & 0xFF; - *out++ = 0; - - /* total number of central directory records */ - *out++ = zip->num_files & 0xFF; - *out++ = 0; - - if (zip->is_zip64) { - /* size and address of central directory are -1 in zip64 */ - memset(out, 0xFF, 8); - out += 8; - } - else { - /* size of central directory */ - offset = zip->ptr - first_cdir_entry; - *out++ = offset & 0xFF; - *out++ = (offset >> 8) & 0xFF; - *out++ = (offset >> 16) & 0xFF; - *out++ = (offset >> 24) & 0xFF; - - /* address of first central directory entry */ - offset = first_cdir_entry - zip->buffer; - *out++ = offset & 0xFF; - *out++ = (offset >> 8) & 0xFF; - *out++ = (offset >> 16) & 0xFF; - *out++ = (offset >> 24) & 0xFF; - } - - /* comment length */ - *out++ = comment_len & 0xFF; - *out++ = (comment_len >> 8) & 0xFF; - - if (comment_len) { - memcpy(out, comment, comment_len); - out += comment_len; - } - - zip->ptr = out; - - return (zip->ptr - zip->buffer); -} - -/* ========================================================================= */ - -static void test_hash_arduboy_fx() -{ - char hash_file[33], hash_iterator[33]; - mock_zip_file_t zip; - uint8_t zip_contents[768]; - size_t zip_size; - const char* expected_md5 = "e696445c353e9d6b3d60bf5d194b82cf"; - int result_file, result_iterator; - - memset(&zip, 0, sizeof(zip)); - zip.ptr = zip.buffer = zip_contents; - mock_zip_add_file(&zip, "info.json", 0xA40B2541, 35); - mock_zip_add_file(&zip, "game.bin", 0x5AA654C0, 96); - mock_zip_add_file(&zip, "save.bin", 0xFF000000, 1); - mock_zip_add_file(&zip, "interp_s2_ArduboyFX.hex", 0x50648360, 71); - mock_zip_add_file(&zip, "screenshot.png", 0x30056694, 48); - zip_size = mock_zip_finalize(&zip, ""); - ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); - - mock_file(0, "game.arduboy", zip_contents, zip_size); - - /* test file hash */ - result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_ARDUBOY, "game.arduboy"); - - /* test file identification from iterator */ - struct rc_hash_iterator iterator; - rc_hash_initialize_iterator(&iterator, "game.arduboy", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -/* ========================================================================= */ - -static void test_hash_msdos_dosz() -{ - char hash_file[33], hash_iterator[33]; - mock_zip_file_t zip; - uint8_t zip_contents[512]; - size_t zip_size; - const char* expected_md5 = "59a255662262f5ada32791b8c36e8ea7"; - int result_file, result_iterator; - - memset(&zip, 0, sizeof(zip)); - zip.ptr = zip.buffer = zip_contents; - mock_zip_add_file(&zip, "FOLDER/", 0, 0); - mock_zip_add_file(&zip, "FOLDER/SUB.TXT", 0x4AD0CF31, 1); - mock_zip_add_file(&zip, "ROOT.TXT", 0xD3D99E8B, 1); - zip_size = mock_zip_finalize(&zip, "TORRENTZIPPED-FD07C52C"); - ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); - - mock_file(0, "game.dosz", zip_contents, zip_size); - - /* test file hash */ - result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_MS_DOS, "game.dosz"); - - /* test file identification from iterator */ - struct rc_hash_iterator iterator; - rc_hash_initialize_iterator(&iterator, "game.dosz", NULL, 0); - result_iterator = rc_hash_iterate(hash_iterator, &iterator); - rc_hash_destroy_iterator(&iterator); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); - - ASSERT_NUM_EQUALS(result_iterator, 1); - ASSERT_STR_EQUALS(hash_iterator, expected_md5); -} - -static void test_hash_msdos_dosz_zip64() -{ - char hash_file[33]; - mock_zip_file_t zip; - uint8_t zip_contents[512]; - size_t zip_size; - const char* expected_md5 = "927dad0a57a2860267ab7bcdb8bc3f61"; - int result_file; - - memset(&zip, 0, sizeof(zip)); - zip.ptr = zip.buffer = zip_contents; - zip.is_zip64 = 1; - mock_zip_add_file(&zip, "README", 0x69FFE77E, 36); - zip_size = mock_zip_finalize(&zip, ""); - ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); - - mock_file(0, "game.dosz", zip_contents, zip_size); - - /* test file hash */ - result_file = rc_hash_generate_from_file(hash_file, RC_CONSOLE_MS_DOS, "game.dosz"); - - /* validation */ - ASSERT_NUM_EQUALS(result_file, 1); - ASSERT_STR_EQUALS(hash_file, expected_md5); -} - -static void test_hash_msdos_dosz_with_dosc() -{ - char hash_dosc[33]; - const char* expected_dosc_md5 = "dd0c0b0c170c30722784e5e962764c35"; - mock_zip_file_t zip; - uint8_t zip_contents[512]; - size_t zip_size; - int result_dosc; - - memset(&zip, 0, sizeof(zip)); - zip.ptr = zip.buffer = zip_contents; - mock_zip_add_file(&zip, "FOLDER/", 0, 0); - mock_zip_add_file(&zip, "FOLDER/SUB.TXT", 0x4AD0CF31, 1); - mock_zip_add_file(&zip, "ROOT.TXT", 0xD3D99E8B, 1); - zip_size = mock_zip_finalize(&zip, "TORRENTZIPPED-FD07C52C"); - ASSERT_NUM_LESS_EQUALS(zip_size, sizeof(zip_contents)); - - /* Add main dosz file and overlay dosc file which will get hashed together */ - /* Just use the same file for both to simplify the test */ - mock_file(0, "game.dosz", zip_contents, zip_size); - mock_file(1, "game.dosc", zip_contents, zip_size); - - /* test file hash */ - result_dosc = rc_hash_generate_from_file(hash_dosc, RC_CONSOLE_MS_DOS, "game.dosz"); - - /* validation */ - ASSERT_NUM_EQUALS(result_dosc, 1); - ASSERT_STR_EQUALS(hash_dosc, expected_dosc_md5); -} - -static void test_hash_msdos_dosz_with_parent() -{ - char hash_dosz[33], hash_dosc[33], hash_dosc2[33]; - const char* expected_dosz_md5 = "623c759476b8b5adb46362f8f0b60769"; - const char* expected_dosc_md5 = "ecd9d776cbaad63094829d7b8dbe5959"; - const char* expected_dosc2_md5 = "cb55c123936ad84479032ea6444cb1a1"; - mock_zip_file_t dosz, dosc; - uint8_t dosz_contents[512], dosc_contents[512]; - size_t dosz_size, dosc_size; - int result_dosz, result_dosc, result_dosc2; - - memset(&dosz, 0, sizeof(dosz)); - dosz.ptr = dosz.buffer = dosz_contents; - mock_zip_add_file(&dosz, "FOLDER/", 0, 0); - mock_zip_add_file(&dosz, "FOLDER/SUB.TXT", 0x4AD0CF31, 1); - mock_zip_add_file(&dosz, "ROOT.TXT", 0xD3D99E8B, 1); - dosz_size = mock_zip_finalize(&dosz, "TORRENTZIPPED-FD07C52C"); - ASSERT_NUM_LESS_EQUALS(dosz_size, sizeof(dosz_contents)); - - memset(&dosc, 0, sizeof(dosz)); - dosc.ptr = dosc.buffer = dosc_contents; - mock_zip_add_file(&dosc, "base.dosz.parent", 0, 0); - mock_zip_add_file(&dosc, "CHILD.TXT", 0x22B35429, 5); - dosc_size = mock_zip_finalize(&dosc, ""); - ASSERT_NUM_LESS_EQUALS(dosz_size, sizeof(dosc_contents)); - - /* Add base dosz file and child dosz file which will get hashed together */ - mock_file(0, "base.dosz", dosz_contents, dosz_size); - mock_file(1, "child.dosz", dosc_contents, dosc_size); - - /* test file hash */ - result_dosz = rc_hash_generate_from_file(hash_dosz, RC_CONSOLE_MS_DOS, "child.dosz"); - - /* test file hash with base.dosc also existing */ - mock_file(2, "base.dosc", dosz_contents, dosz_size); - result_dosc = rc_hash_generate_from_file(hash_dosc, RC_CONSOLE_MS_DOS, "child.dosz"); - - /* test file hash with child.dosc also existing */ - mock_file(3, "child.dosc", dosz_contents, dosz_size); - result_dosc2 = rc_hash_generate_from_file(hash_dosc2, RC_CONSOLE_MS_DOS, "child.dosz"); - - /* validation */ - ASSERT_NUM_EQUALS(result_dosz, 1); - ASSERT_NUM_EQUALS(result_dosc, 1); - ASSERT_NUM_EQUALS(result_dosc2, 1); - ASSERT_STR_EQUALS(hash_dosz, expected_dosz_md5); - ASSERT_STR_EQUALS(hash_dosc, expected_dosc_md5); - ASSERT_STR_EQUALS(hash_dosc2, expected_dosc2_md5); -} - -/* ========================================================================= */ - -void test_hash_zip(void) { - TEST_SUITE_BEGIN(); - - /* Arduboy FX */ - TEST(test_hash_arduboy_fx); - - /* MS DOS */ - TEST(test_hash_msdos_dosz); - TEST(test_hash_msdos_dosz_zip64); - TEST(test_hash_msdos_dosz_with_dosc); - TEST(test_hash_msdos_dosz_with_parent); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/test.c b/src/rcheevos/test/test.c deleted file mode 100644 index 25c88fd2f1..0000000000 --- a/src/rcheevos/test/test.c +++ /dev/null @@ -1,113 +0,0 @@ -#include "rc_internal.h" - -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION -#include "rc_client_raintegration.h" -#endif - -#include "test_framework.h" - -#include - -#define TIMING_TEST 0 - -extern void test_timing(); - -extern void test_condition(); -extern void test_memref(); -extern void test_operand(); -extern void test_condset(); -extern void test_trigger(); -extern void test_value(); -extern void test_format(); -extern void test_lboard(); -extern void test_richpresence(); -extern void test_runtime(); -extern void test_runtime_progress(); - -extern void test_client(); -#ifdef RC_CLIENT_SUPPORTS_EXTERNAL -extern void test_client_external(); -#endif -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION -extern void test_client_raintegration(); -#endif - -extern void test_consoleinfo(); -extern void test_rc_libretro(); -extern void test_rc_validate(); - -extern void test_hash(); -#ifndef RC_HASH_NO_ROM -extern void test_hash_rom(); -#endif -#ifndef RC_HASH_NO_DISC -extern void test_cdreader(); -extern void test_hash_disc(); -#endif -#ifndef RC_HASH_NO_ZIP -extern void test_hash_zip(); -#endif - -extern void test_rapi_common(); -extern void test_rapi_user(); -extern void test_rapi_runtime(); -extern void test_rapi_info(); -extern void test_rapi_editor(); - -TEST_FRAMEWORK_DECLARATIONS() - -int main(void) { - TEST_FRAMEWORK_INIT(); - -#if TIMING_TEST - test_timing(); -#else - test_memref(); - test_operand(); - test_condition(); - test_condset(); - test_trigger(); - test_value(); - test_format(); - test_lboard(); - test_richpresence(); - test_runtime(); - test_runtime_progress(); - - test_consoleinfo(); - test_rc_validate(); - - test_rapi_common(); - test_rapi_user(); - test_rapi_runtime(); - test_rapi_info(); - test_rapi_editor(); - - test_client(); -#ifdef RC_CLIENT_SUPPORTS_EXTERNAL - test_client_external(); -#endif -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - test_client_raintegration(); -#endif -#ifdef RC_CLIENT_SUPPORTS_HASH - /* no direct compile option for hash support, so leverage RC_CLIENT_SUPPORTS_HASH */ - test_rc_libretro(); /* libretro extensions require hash support */ - test_hash(); - #ifndef RC_HASH_NO_ROM - test_hash_rom(); - #endif - #ifndef RC_HASH_NO_DISC - test_cdreader(); - test_hash_disc(); - #endif - #ifndef RC_HASH_NO_ZIP - test_hash_zip(); - #endif -#endif -#endif - - TEST_FRAMEWORK_SHUTDOWN(); - - return TEST_FRAMEWORK_PASSED() ? 0 : 1; -} diff --git a/src/rcheevos/test/test_framework.h b/src/rcheevos/test/test_framework.h deleted file mode 100644 index 4757c1ee59..0000000000 --- a/src/rcheevos/test/test_framework.h +++ /dev/null @@ -1,205 +0,0 @@ -#ifndef TEST_FRAMEWORK_H -#define TEST_FRAMEWORK_H - -#include -#include -#include -#include - -typedef struct -{ - const char* current_suite; - const char* current_test; - const char* current_test_file_stack[16]; - const char* current_test_func_stack[16]; - uint32_t current_test_line_stack[16]; - uint32_t current_test_stack_index; - int current_test_fail; - uint32_t fail_count; - uint32_t run_count; - time_t time_start; -} test_framework_state_t; - -extern test_framework_state_t __test_framework_state; -extern const char* test_framework_basename(const char* path); - -#define TEST_FRAMEWORK_DECLARATIONS() \ - test_framework_state_t __test_framework_state; \ - \ - const char* test_framework_basename(const char* path) { \ - const char* last_slash = path; \ - while (*path) { \ - if (*path == '/' || *path == '\\') last_slash = path + 1; \ - ++path; \ - } \ - return last_slash; \ - } - -#define TEST_FRAMEWORK_INIT() \ - memset(&__test_framework_state, 0, sizeof(__test_framework_state)); \ - __test_framework_state.time_start = time(0) - -#define TEST_FRAMEWORK_SHUTDOWN() \ - printf("\nDone. %d/%d passed in %u seconds\n", \ - __test_framework_state.run_count - __test_framework_state.fail_count, __test_framework_state.run_count, \ - (unsigned)(time(0) - __test_framework_state.time_start)) - -#define TEST_FRAMEWORK_PASSED() (__test_framework_state.fail_count == 0) - -#define TEST_SUITE_BEGIN() \ - __test_framework_state.current_suite = __func__; \ - printf("%s ", __test_framework_state.current_suite); \ - fflush(stdout) - -#define TEST_SUITE_END() __test_framework_state.current_suite = 0; \ - printf("\n"); \ - fflush(stdout) - -#define TEST_PUSH_CURRENT_LINE(func_name) \ - __test_framework_state.current_test_file_stack[__test_framework_state.current_test_stack_index] = __FILE__; \ - __test_framework_state.current_test_line_stack[__test_framework_state.current_test_stack_index] = __LINE__; \ - __test_framework_state.current_test_func_stack[__test_framework_state.current_test_stack_index] = func_name; \ - ++__test_framework_state.current_test_stack_index; - -#define TEST_POP_CURRENT_LINE() --__test_framework_state.current_test_stack_index; - -#define TEST_INIT() \ - __test_framework_state.current_test_stack_index = 0; \ - TEST_PUSH_CURRENT_LINE(__func__); \ - __test_framework_state.current_test_fail = 0; \ - ++__test_framework_state.run_count; \ - printf("."); \ - fflush(stdout); - -#define TEST(func) \ - __test_framework_state.current_test = #func; \ - TEST_INIT() \ - func(); - -#define TEST_PARAMS1(func, p1) \ - __test_framework_state.current_test = #func "(" #p1 ")"; \ - TEST_INIT() \ - func(p1); - -#define TEST_PARAMS2(func, p1, p2) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ")"; \ - TEST_INIT() \ - func(p1, p2); - -#define TEST_PARAMS3(func, p1, p2, p3) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ")"; \ - TEST_INIT() \ - func(p1, p2, p3); - -#define TEST_PARAMS4(func, p1, p2, p3, p4) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ")"; \ - TEST_INIT() \ - func(p1, p2, p3, p4); - -#define TEST_PARAMS5(func, p1, p2, p3, p4, p5) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ")"; \ - TEST_INIT() \ - func(p1, p2, p3, p4, p5); - -#define TEST_PARAMS6(func, p1, p2, p3, p4, p5, p6) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ")"; \ - TEST_INIT() \ - func(p1, p2, p3, p4, p5, p6); - -#define TEST_PARAMS7(func, p1, p2, p3, p4, p5, p6, p7) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ")"; \ - TEST_INIT() \ - func(p1, p2, p3, p4, p5, p6, p7); - -#define TEST_PARAMS8(func, p1, p2, p3, p4, p5, p6, p7, p8) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ", " #p8 ")"; \ - TEST_INIT() \ - func(p1, p2, p3, p4, p5, p6, p7, p8); - -#define TEST_PARAMS9(func, p1, p2, p3, p4, p5, p6, p7, p8, p9) \ - __test_framework_state.current_test = #func "(" #p1 ", " #p2 ", " #p3 ", " #p4 ", " #p5 ", " #p6 ", " #p7 ", " #p8 ", " #p9 ")"; \ - TEST_INIT() \ - func(p1, p2, p3, p4, p5, p6, p7, p8, p9); - -#define ASSERT_HELPER(func_call, func_name) { \ - TEST_PUSH_CURRENT_LINE(func_name); \ - func_call; \ - TEST_POP_CURRENT_LINE(); \ - if (__test_framework_state.current_test_fail) \ - return; \ -} - -#define ASSERT_MESSAGE(message, ...) { \ - uint32_t __stack_index; \ - if (!__test_framework_state.current_test_fail) { \ - __test_framework_state.current_test_fail = 1; \ - ++__test_framework_state.fail_count; \ - fprintf(stderr, "\n* %s/%s (%s:%d)", __test_framework_state.current_suite, __test_framework_state.current_test, \ - test_framework_basename(__test_framework_state.current_test_file_stack[0]), __test_framework_state.current_test_line_stack[0]); \ - } \ - for (__stack_index = 1; __stack_index < __test_framework_state.current_test_stack_index; ++__stack_index) { \ - fprintf(stderr, "\n via %s (%s:%d)", \ - __test_framework_state.current_test_func_stack[__stack_index], \ - test_framework_basename(__test_framework_state.current_test_file_stack[__stack_index]), \ - __test_framework_state.current_test_line_stack[__stack_index]); \ - } \ - fprintf(stderr, "\n "); \ - fprintf(stderr, message "\n", ## __VA_ARGS__); \ - fflush(stderr); \ -} - -#define ASSERT_FAIL(message, ...) { ASSERT_MESSAGE(message, ## __VA_ARGS__); return; } - -#define ASSERT_COMPARE(value, compare, expected, type, format) { \ - type __v = (type)(value); \ - type __e = (type)(expected); \ - if (!(__v compare __e)) { \ - ASSERT_FAIL("Expected: " #value " " #compare " " #expected " (%s:%d)\n Found: " format " " #compare " " format, \ - test_framework_basename(__FILE__), __LINE__, __v, __e); \ - } \ -} - -#define ASSERT_NUM_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, int, "%d") -#define ASSERT_NUM_NOT_EQUALS(value, expected) ASSERT_COMPARE(value, !=, expected, int, "%d") -#define ASSERT_NUM_GREATER(value, expected) ASSERT_COMPARE(value, >, expected, int, "%d") -#define ASSERT_NUM_GREATER_EQUALS(value, expected) ASSERT_COMPARE(value, >=, expected, int, "%d") -#define ASSERT_NUM_LESS(value, expected) ASSERT_COMPARE(value, <, expected, int, "%d") -#define ASSERT_NUM_LESS_EQUALS(value, expected) ASSERT_COMPARE(value, <=, expected, int, "%d") - -#define ASSERT_FLOAT_EQUALS(value, expected) { \ - float __v = (float)value; \ - float __e = (float)expected; \ - double diff = (__v > __e) ? ((double)__v - (double)__e) : ((double)__e - (double)__v); \ - if (diff >= 0.0000002) { /* FLT_EPSILON is ~1.19e-7 */ \ - ASSERT_FAIL("Expected: " #value " = " #expected " (%s:%d)\n Found: %f = %f", \ - test_framework_basename(__FILE__), __LINE__, __v, __e); \ - } \ -} - -/* TODO: figure out some way to detect c89 so we can use int64_t and %lld on non-c89 builds */ -#define ASSERT_NUM64_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, int, "%d") - -#define ASSERT_TRUE(value) ASSERT_NUM_NOT_EQUALS(value, 0) -#define ASSERT_FALSE(value) ASSERT_NUM_EQUALS(value, 0) - -#define ASSERT_UNUM_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, unsigned, "%u") -#define ASSERT_DBL_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, double, "%g") -#define ASSERT_PTR_EQUALS(value, expected) ASSERT_COMPARE(value, ==, expected, void*, "%p") -#define ASSERT_PTR_NOT_NULL(value) ASSERT_COMPARE(value, !=, NULL, void*, "%p") -#define ASSERT_PTR_NULL(value) ASSERT_COMPARE(value, ==, NULL, void*, "%p") - -#define ASSERT_STR_EQUALS(value, expected) { \ - const char* __v = (const char*)(value); \ - const char* __e = (const char*)(expected); \ - if (strcmp(__v, __e) != 0) { \ - ASSERT_FAIL( "String mismatch for: " #value " (%s:%d)\n Expected: %s\n Found: %s", test_framework_basename(__FILE__), __LINE__, __e, __v); \ - }} - -#define ASSERT_STR_NOT_EQUALS(value, expected) { \ - const char* __v = (const char*)(value); \ - const char* __e = (const char*)(expected); \ - if (strcmp(__v, __e) == 0) { \ - ASSERT_FAIL( "String match for: " #value " (%s:%d)\n Found, but not expected: %s", test_framework_basename(__FILE__), __LINE__, __v); \ - }} - -#endif /* TEST_FRAMEWORK_H */ diff --git a/src/rcheevos/test/test_rc_client.c b/src/rcheevos/test/test_rc_client.c deleted file mode 100644 index 50f9a2c852..0000000000 --- a/src/rcheevos/test/test_rc_client.c +++ /dev/null @@ -1,10329 +0,0 @@ -#include "rc_client.h" - -#include "rc_consoles.h" -#include "rc_internal.h" -#include "rc_api_runtime.h" - -#include "../src/rc_client_internal.h" -#include "../src/rc_client_external.h" -#include "../src/rc_version.h" - -#include "test_framework.h" - -#ifdef RC_CLIENT_SUPPORTS_HASH -#include "rc_hash.h" -#include "rhash/data.h" -#include "rhash/mock_filereader.h" -#endif - -#if defined(_WIN32) -#include -#elif defined(__unix__) && __STDC_VERSION__ >= 199309L -#include -#else -#define RC_NO_SLEEP -#endif - -static rc_client_t* g_client; -static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ - -#define GENERIC_ACHIEVEMENT_JSON(id, memaddr) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ - "\"Description\":\"Desc " id "\",\"Flags\":3,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ - "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" - -#define UNOFFICIAL_ACHIEVEMENT_JSON(id, memaddr) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ - "\"Description\":\"Desc " id "\",\"Flags\":5,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ - "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" - -#define TYPED_ACHIEVEMENT_JSON(id, memaddr, type, rarity, rarity_hardcore) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ - "\"Description\":\"Desc " id "\",\"Flags\":3,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ - "\"Type\":\"" type "\",\"Rarity\":" rarity ",\"RarityHardcore\":" rarity_hardcore "," \ - "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" - -#define GENERIC_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ - "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\"}" - -static const char* patchdata_empty = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[]," - "\"Leaderboards\":[]" - "}]}"; - -static const char* patchdata_2ach_0lbd = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," - "\"Created\":1376970283,\"Modified\":1376970283}" - "]," - "\"Leaderboards\":[]" - "}]}"; - -static const char* patchdata_2ach_1lbd = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," - "\"Created\":1376970283,\"Modified\":1376970283}" - "]," - "\"Leaderboards\":[" - "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," - "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" - "]" - "}]}"; - -static const char* patchdata_rich_presence_only = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[]," - "\"Leaderboards\":[]" - "}]}"; - -static const char* patchdata_leaderboard_only = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") - "]" - "}]}"; - -static const char* patchdata_leaderboard_immediate_submit = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0=1::SUB:1=1::VAL:0x 000E", "SCORE") - "]" - "}]}"; - -static const char* patchdata_bounds_check_system = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":7," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("1", "0xH0000=5") "," - GENERIC_ACHIEVEMENT_JSON("2", "0xHFFFF=5") "," - GENERIC_ACHIEVEMENT_JSON("3", "0xH10000=5") "," - GENERIC_ACHIEVEMENT_JSON("4", "0x FFFE=5") "," - GENERIC_ACHIEVEMENT_JSON("5", "0x FFFF=5") "," - GENERIC_ACHIEVEMENT_JSON("6", "0x 10000=5") "," - GENERIC_ACHIEVEMENT_JSON("7", "I:0xH0000_0xHFFFF=5") - "]," - "\"Leaderboards\":[]" - "}]}"; - -static const char* patchdata_bounds_check_8 = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":7," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("408", "0xH0004=5") "," - GENERIC_ACHIEVEMENT_JSON("508", "0xH0005=5") "," - GENERIC_ACHIEVEMENT_JSON("608", "0xH0006=5") "," - GENERIC_ACHIEVEMENT_JSON("708", "0xH0007=5") "," - GENERIC_ACHIEVEMENT_JSON("808", "0xH0008=5") "," - GENERIC_ACHIEVEMENT_JSON("416", "0x 0004=5") "," - GENERIC_ACHIEVEMENT_JSON("516", "0x 0005=5") "," - GENERIC_ACHIEVEMENT_JSON("616", "0x 0006=5") "," - GENERIC_ACHIEVEMENT_JSON("716", "0x 0007=5") "," - GENERIC_ACHIEVEMENT_JSON("816", "0x 0008=5") "," - GENERIC_ACHIEVEMENT_JSON("424", "0xW0004=5") "," - GENERIC_ACHIEVEMENT_JSON("524", "0xW0005=5") "," - GENERIC_ACHIEVEMENT_JSON("624", "0xW0006=5") "," - GENERIC_ACHIEVEMENT_JSON("724", "0xW0007=5") "," - GENERIC_ACHIEVEMENT_JSON("824", "0xW0008=5") "," - GENERIC_ACHIEVEMENT_JSON("432", "0xX0004=5") "," - GENERIC_ACHIEVEMENT_JSON("532", "0xX0005=5") "," - GENERIC_ACHIEVEMENT_JSON("632", "0xX0006=5") "," - GENERIC_ACHIEVEMENT_JSON("732", "0xX0007=5") "," - GENERIC_ACHIEVEMENT_JSON("832", "0xX0008=5") - "]," - "\"Leaderboards\":[]" - "}]}"; - -static const char* patchdata_exhaustive = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," - GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," - GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," - GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," - GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," - GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," - GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") - "]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ - GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ - GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ - GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ - GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ - GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ - "]" - "}]}"; - - -static const char* patchdata_exhaustive_typed = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - TYPED_ACHIEVEMENT_JSON("5", "0xH0005=5", "", "100.0", "99.5") "," - TYPED_ACHIEVEMENT_JSON("6", "M:0xH0006=6", "progression", "95.3", "84.7") "," - TYPED_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1", "missable", "47.6", "38.2") "," - TYPED_ACHIEVEMENT_JSON("8", "0xH0008=8", "progression", "86.0", "73.1") "," - TYPED_ACHIEVEMENT_JSON("9", "0xH0009=9", "win_condition", "81.4", "66.4") "," - TYPED_ACHIEVEMENT_JSON("70", "M:0xX0010=100000", "missable", "11.4", "6.3") "," - TYPED_ACHIEVEMENT_JSON("71", "G:0xX0010=100000", "", "8.7", "3.8") - "]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ - GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ - GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ - GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ - GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ - GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ - "]" - "}]}"; - -static const char* patchdata_big_ids = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," - GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," - GENERIC_ACHIEVEMENT_JSON("4294967295", "0xH0009=9") "," /* UINT_MAX */ - GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") - "]," - "\"Leaderboards\":[]" - "}]}"; - -#define HIDDEN_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ - "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"Hidden\":true}" - -static const char* patchdata_leaderboards_hidden = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - HIDDEN_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," - GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," - HIDDEN_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") - "]" - "}]}"; - -static const char* patchdata_unofficial_unsupported = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0xH0001=1_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}," - "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,\"Points\":2," - "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," - "\"Created\":1376970283,\"Modified\":1376970283}," - "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":3,\"Points\":2," - "\"MemAddr\":\"0xHFEFEFEFE=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," - "\"Created\":1376971283,\"Modified\":1376971283}" - "]," - "\"Leaderboards\":[" - "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," - "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xHFEFEFEFE=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" - "]" - "}]}"; - -static const char* patchdata_subset = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," - GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," - GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," - GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," - GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," - GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," - GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") - "]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ - GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ - GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ - GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ - GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ - GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ - "]" - "},{" - "\"AchievementSetId\":2345,\"GameId\":1235,\"Title\":\"Bonus\",\"Type\":\"bonus\"," - "\"ImageIconUrl\":\"http://server/Images/112234.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("5501", "0xH0017=7") "," - GENERIC_ACHIEVEMENT_JSON("5502", "0xH0018=8") "," - GENERIC_ACHIEVEMENT_JSON("5503", "0xH0019=9") - "]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("81", "STA:0xH0008=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("82", "STA:0xH0008=2::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") - "]" - "}]}"; - -static const char* patchdata_subset_unofficial_unsupported = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," - GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," - GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," - UNOFFICIAL_ACHIEVEMENT_JSON("8", "0xH0008=8") "," - GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," - GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," - GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") - "]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ - GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ - GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ - GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ - GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ - GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ - "]" - "},{" - "\"AchievementSetId\":2345,\"GameId\":1235,\"Title\":\"Bonus\",\"Type\":\"bonus\"," - "\"ImageIconUrl\":\"http://server/Images/112234.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("5501", "0xH0017=7") "," - UNOFFICIAL_ACHIEVEMENT_JSON("5502", "0xH0018=8") "," - GENERIC_ACHIEVEMENT_JSON("5503", "0xHFEFEFEFE=9") - "]," - "\"Leaderboards\":[" - GENERIC_LEADERBOARD_JSON("81", "STA:0xH0008=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," - GENERIC_LEADERBOARD_JSON("82", "STA:0xH0008=2::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") - "]" - "}]}"; - -static const char* patchdata_not_found = "{\"Success\":false,\"Error\":\"Unknown game\",\"Code\":\"not_found\",\"Status\":404}"; - -static const char* no_unlocks = "{\"Success\":true,\"Unlocks\":[],\"HardcoreUnlocks\":[]}"; - -/* startsession API only returns HardcoreUnlocks if an achievement has been earned in hardcore, - * even if the softcore unlock has a different timestamp */ -static const char* unlock_5501h_and_5502 = "{\"Success\":true,\"Unlocks\":[" - "{\"ID\":5502,\"When\":1234567899}" - "],\"HardcoreUnlocks\":[" - "{\"ID\":5501,\"When\":1234567890}" - "]}"; -static const char* unlock_5501_and_5502 = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":5501,\"When\":1234567890}," - "{\"ID\":5502,\"When\":1234567899}" - "]}"; -static const char* unlock_5501_5502_and_5503 = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":5501,\"When\":1234567890}," - "{\"ID\":5502,\"When\":1234567899}," - "{\"ID\":5503,\"When\":1234567999}" - "]}"; -static const char* unlock_8 = "{\"Success\":true,\"HardcoreUnlocks\":[{\"ID\":8,\"When\":1234567890}]}"; -static const char* unlock_8_and_5502 = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":8,\"When\":1234567890}," - "{\"ID\":5502,\"When\":1234567890}" - "]}"; -static const char* unlock_6_8h_and_9 = "{\"Success\":true,\"Unlocks\":[" - "{\"ID\":6,\"When\":1234567890}," - "{\"ID\":9,\"When\":1234567899}" - "],\"HardcoreUnlocks\":[" - "{\"ID\":8,\"When\":1234567895}" - "]}"; - -static const char* response_429 = - "\n" - "429 Too Many Requests\n" - "\n" - "

429 Too Many Requests

\n" - "
nginx
\n" - "\n" - ""; - -static const char* response_502 = - "\n" - "502 Bad Gateway\n" - "\n" - "

502 Bad Gateway

\n" - "
nginx
\n" - "\n" - ""; - -static const char* response_503 = - "\n" - "503 Service Temporarily Unavailable\n" - "\n" - "

503 Service Temporarily Unavailable

\n" - "
nginx
\n" - "\n" - ""; - -static const char* default_game_badge = "https://media.retroachievements.org/Images/000001.png"; - -/* ----- helpers ----- */ - -static void _assert_achievement_state(rc_client_t* client, uint32_t id, int expected_state) -{ - const rc_client_achievement_t* achievement = rc_client_get_achievement_info(client, id); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, expected_state); -} -#define assert_achievement_state(client, id, expected_state) ASSERT_HELPER(_assert_achievement_state(client, id, expected_state), "assert_achievement_state") - -typedef struct rc_client_captured_event_t -{ - rc_client_event_t event; - rc_client_server_error_t server_error; /* server_error goes out of scope, it needs to be copied too */ - uint32_t id; -} rc_client_captured_event_t; - -static rc_client_captured_event_t events[16]; -static int event_count = 0; - -static void rc_client_event_handler(const rc_client_event_t* e, rc_client_t* client) -{ - memcpy(&events[event_count], e, sizeof(rc_client_event_t)); - memset(&events[event_count].server_error, 0, sizeof(events[event_count].server_error)); - - switch (e->type) { - case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: - case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: - case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: - events[event_count].id = e->achievement->id; - break; - - case RC_CLIENT_EVENT_LEADERBOARD_STARTED: - case RC_CLIENT_EVENT_LEADERBOARD_FAILED: - case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: - events[event_count].id = e->leaderboard->id; - break; - - case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: { - /* scoreboard is not maintained out of scope, copy it */ - static rc_client_leaderboard_scoreboard_t scoreboard; - static rc_client_leaderboard_scoreboard_entry_t scoreboard_entries[2]; - static char scoreboard_top_usernames[2][128]; - uint32_t i; - - /* cap the entries at 2, none of the mocked responses have anything larger */ - memcpy(&scoreboard, e->leaderboard_scoreboard, sizeof(scoreboard)); - scoreboard.num_top_entries = (scoreboard.num_top_entries > 2) ? 2 : scoreboard.num_top_entries; - memcpy(scoreboard_entries, e->leaderboard_scoreboard->top_entries, scoreboard.num_top_entries * sizeof(scoreboard_entries[0])); - for (i = 0; i < scoreboard.num_top_entries; i++) { - strcpy_s(scoreboard_top_usernames[i], sizeof(scoreboard_top_usernames[i]), scoreboard_entries[i].username); - scoreboard_entries[i].username = scoreboard_top_usernames[i]; - } - scoreboard.top_entries = scoreboard_entries; - - events[event_count].id = e->leaderboard_scoreboard->leaderboard_id; - events[event_count].event.leaderboard_scoreboard = &scoreboard; - break; - } - - case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: - case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: - case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: - events[event_count].id = e->leaderboard_tracker->id; - break; - - case RC_CLIENT_EVENT_GAME_COMPLETED: - events[event_count].id = rc_client_get_game_info(client)->id; - break; - - case RC_CLIENT_EVENT_SERVER_ERROR: { - static char event_server_error_message[128]; - - /* server error data is not maintained out of scope, copy it */ - memcpy(&events[event_count].server_error, e->server_error, sizeof(events[event_count].server_error)); - strcpy_s(event_server_error_message, sizeof(event_server_error_message), e->server_error->error_message); - events[event_count].server_error.error_message = event_server_error_message; - events[event_count].event.server_error = &events[event_count].server_error; - events[event_count].id = 0; - break; - } - - case RC_CLIENT_EVENT_SUBSET_COMPLETED: - events[event_count].id = e->subset->id; - break; - - default: - events[event_count].id = 0; - break; - } - - ++event_count; -} - -static rc_client_event_t* find_event(uint8_t type, uint32_t id) -{ - int i; - - for (i = 0; i < event_count; ++i) { - if (events[i].id == id && events[i].event.type == type) - return &events[i].event; - } - - return NULL; -} - -static uint8_t* g_memory = NULL; -static uint32_t g_memory_size = 0; - -static void mock_memory(uint8_t* memory, uint32_t size) -{ - g_memory = memory; - g_memory_size = size; -} - -static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) -{ - if (g_memory_size > 0) { - if (address >= g_memory_size) - return 0; - - uint32_t num_avail = g_memory_size - address; - if (num_avail < num_bytes) - num_bytes = num_avail; - - memcpy(buffer, &g_memory[address], num_bytes); - return num_bytes; - } - - memset(&buffer, 0, num_bytes); - return num_bytes; -} - -static rc_clock_t g_now; - -static rc_clock_t rc_client_get_now_millisecs(const rc_client_t* client) -{ - return g_now; -} - -/* ----- API mocking ----- */ - -typedef struct rc_mock_api_response -{ - const char* request_params; - rc_api_server_response_t server_response; - int seen; - rc_client_server_callback_t async_callback; - void* async_callback_data; -} rc_mock_api_response; - -static rc_mock_api_response g_mock_api_responses[12]; -static int g_num_mock_api_responses = 0; - -void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) -{ - rc_api_server_response_t server_response; - - int i; - for (i = 0; i < g_num_mock_api_responses; i++) { - if (strcmp(g_mock_api_responses[i].request_params, request->post_data) == 0) { - g_mock_api_responses[i].seen++; - callback(&g_mock_api_responses[i].server_response, callback_data); - return; - } - } - - ASSERT_FAIL("No API response for: %s", request->post_data); - - /* still call the callback to prevent memory leak */ - memset(&server_response, 0, sizeof(server_response)); - server_response.body = ""; - server_response.http_status_code = 500; - callback(&server_response, callback_data); -} - -void rc_client_server_call_async(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) -{ - g_mock_api_responses[g_num_mock_api_responses].request_params = strdup(request->post_data); - g_mock_api_responses[g_num_mock_api_responses].async_callback = callback; - g_mock_api_responses[g_num_mock_api_responses].async_callback_data = callback_data; - g_mock_api_responses[g_num_mock_api_responses].seen = -1; - g_num_mock_api_responses++; -} - -static void _async_api_response(const char* request_params, const char* response_body, int http_status_code) -{ - int i; - for (i = 0; i < g_num_mock_api_responses; i++) - { - if (g_mock_api_responses[i].request_params && strcmp(g_mock_api_responses[i].request_params, request_params) == 0) - { - /* request_params are set to NULL, so we can't find the requests to get a count anyway */ - g_mock_api_responses[i].seen++; - g_mock_api_responses[i].server_response.body = response_body; - g_mock_api_responses[i].server_response.body_length = strlen(response_body); - g_mock_api_responses[i].server_response.http_status_code = http_status_code; - g_mock_api_responses[i].async_callback(&g_mock_api_responses[i].server_response, g_mock_api_responses[i].async_callback_data); - free((void*)g_mock_api_responses[i].request_params); - g_mock_api_responses[i].request_params = NULL; - - while (g_num_mock_api_responses > 0 && g_mock_api_responses[g_num_mock_api_responses - 1].request_params == NULL) - --g_num_mock_api_responses; - return; - } - } - - ASSERT_FAIL("No pending API request for: %s", request_params); -} - -void async_api_response(const char* request_params, const char* response_body) -{ - _async_api_response(request_params, response_body, 200); -} - -void async_api_error(const char* request_params, const char* response_body, int http_status_code) -{ - _async_api_response(request_params, response_body, http_status_code); -} - -static void _assert_api_called(const char* request_params, int count) -{ - int i; - for (i = 0; i < g_num_mock_api_responses; i++) { - if (g_mock_api_responses[i].request_params && - strcmp(g_mock_api_responses[i].request_params, request_params) == 0) { - ASSERT_NUM_EQUALS(g_mock_api_responses[i].seen, count); - return; - } - } - - ASSERT_NUM_EQUALS(0, count); -} -#define assert_api_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 1), "assert_api_called") -#define assert_api_not_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_called") -#define assert_api_call_count(request_params, num) ASSERT_HELPER(_assert_api_called(request_params, num), "assert_api_call_count") -#define assert_api_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, -1), "assert_api_pending") -#define assert_api_not_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_pending") - -void reset_mock_api_handlers(void) -{ - g_num_mock_api_responses = 0; - memset(g_mock_api_responses, 0, sizeof(g_mock_api_responses)); -} - -void mock_api_response(const char* request_params, const char* response_body) -{ - g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; - g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; - g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); - g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = 200; - g_num_mock_api_responses++; -} - -void mock_api_error(const char* request_params, const char* response_body, int http_status_code) -{ - g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; - g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; - g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); - g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = http_status_code; - g_num_mock_api_responses++; -} - -static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_OK); - ASSERT_PTR_NULL(error_message); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_callback_expect_timeout(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); - ASSERT_STR_EQUALS(error_message, "Request has timed out."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_callback_expect_no_internet(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); - ASSERT_STR_EQUALS(error_message, "Internet not available."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_callback_expect_no_game_loaded(int result, const char* error_message, rc_client_t* client, void* callback_data) -{ - ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); - ASSERT_STR_EQUALS(error_message, "No game loaded"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); -} - -static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_FAIL("Callback should not have been called."); -} - -static rc_client_t* mock_client_not_logged_in(void) -{ - rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); - rc_client_set_event_handler(client, rc_client_event_handler); - rc_client_set_get_time_millisecs_function(client, rc_client_get_now_millisecs); - - mock_memory(NULL, 0); - rc_api_set_host(NULL); - reset_mock_api_handlers(); - g_now = 100000; - - return client; -} - -static rc_client_t* mock_client_not_logged_in_async(void) -{ - rc_client_t* client = mock_client_not_logged_in(); - client->callbacks.server_call = rc_client_server_call_async; - return client; -} - -static rc_client_t* mock_client_logged_in(void) -{ - rc_client_t* client = mock_client_not_logged_in(); - client->user.username = "Username"; - client->user.display_name = "DisplayName"; - client->user.token = "ApiToken"; - client->user.score = 12345; - client->user.avatar_url = "http://server/UserPic/Username.png"; - client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; - - return client; -} - -static rc_client_t* mock_client_logged_in_async(void) -{ - rc_client_t* client = mock_client_logged_in(); - client->callbacks.server_call = rc_client_server_call_async; - return client; -} - -static void mock_client_load_game(const char* patchdata, const char* unlocks) -{ - reset_mock_api_handlers(); - event_count = 0; - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlocks); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - if (!g_client->game) - ASSERT_MESSAGE("client->game is NULL"); -} - -static void mock_client_load_game_softcore(const char* patchdata, const char* unlocks) -{ - reset_mock_api_handlers(); - event_count = 0; - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=0&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlocks); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - if (!g_client->game) - ASSERT_MESSAGE("client->game is NULL"); -} - -static rc_client_t* mock_client_game_loaded(const char* patchdata, const char* unlocks) -{ - g_client = mock_client_logged_in(); - - mock_client_load_game(patchdata, unlocks); - - return g_client; -} - -/* ----- login ----- */ - -static void test_login_with_password(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_response("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "User"); - ASSERT_STR_EQUALS(user->display_name, "User"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->score_softcore, 123); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - - rc_client_destroy(g_client); -} - -static void test_login_with_token(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_response("r=login2&u=User&t=ApiToken", - "{\"Success\":true,\"User\":\"User\",\"AvatarUrl\":\"http://server/UserPic/USER.png\",\"Token\":\"ApiToken\",\"Score\":12345,\"Messages\":2}"); - - rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "User"); - ASSERT_STR_EQUALS(user->display_name, "User"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_STR_EQUALS(user->avatar_url, "http://server/UserPic/USER.png"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_username_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); - ASSERT_STR_EQUALS(error_message, "username is required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_callback_expect_password_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); - ASSERT_STR_EQUALS(error_message, "password is required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_callback_expect_token_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); - ASSERT_STR_EQUALS(error_message, "token is required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_login_required_fields(void) -{ - g_client = mock_client_not_logged_in(); - - rc_client_begin_login_with_password(g_client, "User", "", rc_client_callback_expect_password_required, g_callback_userdata); - rc_client_begin_login_with_password(g_client, "", "Pa$$word", rc_client_callback_expect_username_required, g_callback_userdata); - rc_client_begin_login_with_password(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); - - rc_client_begin_login_with_token(g_client, "User", "", rc_client_callback_expect_token_required, g_callback_userdata); - rc_client_begin_login_with_token(g_client, "", "ApiToken", rc_client_callback_expect_username_required, g_callback_userdata); - rc_client_begin_login_with_token(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); - - ASSERT_NUM_EQUALS(g_client->state.user, RC_CLIENT_USER_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_credentials_error(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_CREDENTIALS); - ASSERT_STR_EQUALS(error_message, "Invalid User/Password combination. Please try again"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_login_with_incorrect_password(void) -{ - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_error("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"," - "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_token_error(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_CREDENTIALS); - ASSERT_STR_EQUALS(error_message, "Invalid User/Token combination."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_login_with_incorrect_token(void) -{ - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_error("r=login2&u=User&t=TOKEN", - "{\"Success\":false,\"Error\":\"Invalid User/Token combination.\"," - "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); - - rc_client_begin_login_with_token(g_client, "User", "TOKEN", rc_client_callback_expect_token_error, g_callback_userdata); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_expired_token(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_EXPIRED_TOKEN); - ASSERT_STR_EQUALS(error_message, "The access token has expired. Please log in again."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_login_with_expired_token(void) -{ - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_error("r=login2&u=User&t=EXPIRED", - "{\"Success\":false,\"Error\":\"The access token has expired. Please log in again.\"," - "\"Status\":401,\"Code\":\"expired_token\"}", 403); - - rc_client_begin_login_with_token(g_client, "User", "EXPIRED", rc_client_callback_expect_expired_token, g_callback_userdata); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_access_denied(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_ACCESS_DENIED); - ASSERT_STR_EQUALS(error_message, "Access denied."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_login_with_banned_account(void) -{ - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_error("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":false,\"Error\":\"Access denied.\"," - "\"Status\":403,\"Code\":\"access_denied\"}", 403); - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_access_denied, g_callback_userdata); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_missing_token(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); - ASSERT_STR_EQUALS(error_message, "Token not found in response"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_login_incomplete_response(void) -{ - g_client = mock_client_not_logged_in(); - reset_mock_api_handlers(); - mock_api_response("r=login2&u=User&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\"}"); - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_missing_token, g_callback_userdata); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void test_login_with_password_async(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NULL(user); - - async_api_response("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "User"); - ASSERT_STR_EQUALS(user->display_name, "User"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - - rc_client_destroy(g_client); -} - -static void test_login_with_password_async_aborted(void) -{ - const rc_client_user_t* user; - rc_client_async_handle_t* handle; - - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", - rc_client_callback_expect_uncalled, g_callback_userdata); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NULL(user); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NULL(user); - - rc_client_destroy(g_client); -} - -static void test_login_with_password_async_destroyed(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", - rc_client_callback_expect_uncalled, g_callback_userdata); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NULL(user); - - rc_client_destroy(g_client); - - async_api_response("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); -} - -static void test_login_with_password_client_error(void) -{ - const rc_client_user_t* user; - rc_client_async_handle_t* handle; - - g_client = mock_client_not_logged_in(); - - mock_api_error("r=login2&u=User&p=Pa%24%24word", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); - - handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", - rc_client_callback_expect_no_internet, g_callback_userdata); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NULL(user); - - ASSERT_PTR_NULL(handle); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_login_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); - ASSERT_STR_EQUALS(error_message, "Login required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_logout(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_logged_in(); - - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - - rc_client_logout(g_client); - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - /* reference pointer should be NULLed out */ - ASSERT_PTR_NULL(user->display_name); - ASSERT_PTR_NULL(user->username); - ASSERT_PTR_NULL(user->token); - - /* attempt to load game should fail */ - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_logout_with_game_loaded(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); - ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); - - rc_client_logout(g_client); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_login_aborted(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_ABORTED); - ASSERT_STR_EQUALS(error_message, "Login aborted"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_logout_during_login(void) -{ - g_client = mock_client_not_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_login_aborted, g_callback_userdata); - rc_client_logout(g_client); - - async_api_response("r=login2&u=User&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_no_longer_active(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_ABORTED); - ASSERT_STR_EQUALS(error_message, "The requested game is no longer active"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_logout_during_fetch_game(void) -{ - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", - rc_client_callback_expect_no_longer_active, g_callback_userdata); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - - rc_client_logout(g_client); - - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); - - rc_client_destroy(g_client); -} - -static void test_user_get_image_url(void) -{ - char buffer[256]; - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - ASSERT_STR_EQUALS(g_client->user.avatar_url, "http://server/UserPic/Username.png"); - - ASSERT_NUM_EQUALS(rc_client_user_get_image_url(rc_client_get_user_info(g_client), buffer, sizeof(buffer)), RC_OK); - ASSERT_STR_EQUALS(buffer, "http://server/UserPic/Username.png"); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive, unlock_6_8h_and_9); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 5); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_softcore(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive, unlock_6_8h_and_9); - rc_client_set_hardcore_enabled(g_client, 0); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 3); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 15); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_encore_mode(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_exhaustive); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlock_6_8h_and_9); - - rc_client_set_encore_mode_enabled(g_client, 1); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 5); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_with_unsupported_and_unofficial(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); - - ASSERT_NUM_EQUALS(summary.points_core, 7); - ASSERT_NUM_EQUALS(summary.points_unlocked, 0); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_with_unsupported_unlocks(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_unofficial_unsupported, unlock_5501_5502_and_5503); - - /* unlocked unsupported achievement should be counted in both unlocked and unsuppored buckets */ - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 2); - - ASSERT_NUM_EQUALS(summary.points_core, 7); - ASSERT_NUM_EQUALS(summary.points_unlocked, 7); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 1234567999); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_with_unofficial_off(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 0); - mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); - - /* unofficial achievements are not copied from the patch data to the runtime if unofficial is off */ - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); - - ASSERT_NUM_EQUALS(summary.points_core, 7); - ASSERT_NUM_EQUALS(summary.points_unlocked, 0); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_no_achievements(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_empty, no_unlocks); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); - - ASSERT_NUM_EQUALS(summary.points_core, 0); - ASSERT_NUM_EQUALS(summary.points_unlocked, 0); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_unknown_game(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); - ASSERT_STR_EQUALS(error_message, "Unknown game"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_get_user_game_summary_unknown_game(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); - - ASSERT_NUM_EQUALS(summary.points_core, 0); - ASSERT_NUM_EQUALS(summary.points_unlocked, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_progress_incomplete(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive_typed, unlock_8); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 5); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_progress_progression_no_win(void) -{ - rc_client_user_game_summary_t summary; - /* 6 and 8 are progression, 9 is win condition */ - const char* unlock_5_6_and_8 = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":5,\"When\":1234568890}," - "{\"ID\":6,\"When\":1234567999}," - "{\"ID\":8,\"When\":1234567895}" - "]}"; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive_typed, unlock_5_6_and_8); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 3); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 15); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_progress_win_only(void) -{ - rc_client_user_game_summary_t summary; - /* 6 and 8 are progression, 9 is win condition */ - const char* unlock_5_and_9 = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":5,\"When\":1234568890}," - "{\"ID\":9,\"When\":1234567999}" - "]}"; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive_typed, unlock_5_and_9); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 2); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 10); - - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_beat(void) -{ - rc_client_user_game_summary_t summary; - /* 6 and 8 are progression, 9 is win condition */ - const char* unlocks = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":5,\"When\":1234568890}," - "{\"ID\":6,\"When\":1234567999}," - "{\"ID\":8,\"When\":1234567890}," - "{\"ID\":9,\"When\":1234568765}" - "]}"; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive_typed, unlocks); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 4); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 20); - - ASSERT_NUM_EQUALS(summary.beaten_time, 1234568765); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_mastery(void) -{ - rc_client_user_game_summary_t summary; - const char* unlocks = "{\"Success\":true,\"HardcoreUnlocks\":[" - "{\"ID\":5,\"When\":1234568890}," - "{\"ID\":6,\"When\":1234567999}," - "{\"ID\":7,\"When\":1234569123}," - "{\"ID\":8,\"When\":1234567890}," - "{\"ID\":9,\"When\":1234568765}," - "{\"ID\":70,\"When\":1234568901}," - "{\"ID\":71,\"When\":1234566789}" - "]}"; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_exhaustive_typed, unlocks); - - rc_client_get_user_game_summary(g_client, &summary); - ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 7); - - ASSERT_NUM_EQUALS(summary.points_core, 35); - ASSERT_NUM_EQUALS(summary.points_unlocked, 35); - - ASSERT_NUM_EQUALS(summary.beaten_time, 1234568765); - ASSERT_NUM_EQUALS(summary.completed_time, 1234569123); - - rc_client_destroy(g_client); -} - -/* ----- load game ----- */ - -static void rc_client_callback_expect_hash_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); - ASSERT_STR_EQUALS(error_message, "hash is required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_load_game_required_fields(void) -{ - g_client = mock_client_logged_in(); - - rc_client_begin_load_game(g_client, NULL, rc_client_callback_expect_hash_required, g_callback_userdata); - rc_client_begin_load_game(g_client, "", rc_client_callback_expect_hash_required, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_load_game_unknown_hash(void) -{ - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); - - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 0); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.id, 0); - ASSERT_STR_EQUALS(g_client->game->subsets->public_.title, ""); - ASSERT_NUM_EQUALS(g_client->game->subsets->active, 0); - } - rc_client_destroy(g_client); -} - -static void test_load_game_unknown_hash_repeated(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* first request should resolve the hash asynchronously */ - handle = rc_client_begin_load_game(g_client, - "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NOT_NULL(handle); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - - /* second request should use the hash cache and not need an asynchronous call */ - handle = rc_client_begin_load_game(g_client, - "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NULL(handle); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - - rc_client_destroy(g_client); -} - -static void test_load_game_unknown_hash_multiple(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* first request */ - handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", - rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NOT_NULL(handle); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - - /* second request */ - handle = rc_client_begin_load_game(g_client, "FEDCBA9876543210", - rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NOT_NULL(handle); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=FEDCBA9876543210", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "FEDCBA9876543210"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - - rc_client_destroy(g_client); -} - -static void test_load_game_not_logged_in(void) -{ - g_client = mock_client_not_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game(void) -{ - rc_client_achievement_info_t* achievement; - rc_client_leaderboard_info_t* leaderboard; - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 1); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, "http://server/Images/112233.png"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - - achievement = &g_client->game->subsets->achievements[0]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5501); - ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); - ASSERT_STR_EQUALS(achievement->public_.badge_url, "https://media.retroachievements.org/Badge/00234.png"); - ASSERT_STR_EQUALS(achievement->public_.badge_locked_url, "https://media.retroachievements.org/Badge/00234_lock.png"); - ASSERT_NUM_EQUALS(achievement->public_.points, 5); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - achievement = &g_client->game->subsets->achievements[1]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5502); - ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); - ASSERT_STR_EQUALS(achievement->public_.badge_url, "https://media.retroachievements.org/Badge/00235.png"); - ASSERT_STR_EQUALS(achievement->public_.badge_locked_url, "https://media.retroachievements.org/Badge/00235_lock.png"); - ASSERT_NUM_EQUALS(achievement->public_.points, 2); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - leaderboard = &g_client->game->subsets->leaderboards[0]; - ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); - ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); - ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); - ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_PTR_NOT_NULL(leaderboard->lboard); - ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); - ASSERT_PTR_NULL(leaderboard->tracker); - } - - rc_client_destroy(g_client); -} - -static void test_load_game_async_load_different_game(void) -{ - static const char* patchdata_alternate = "{\"Success\":true," - "\"GameId\":2345,\"Title\":\"Other Game\",\"ConsoleId\":7," - "\"ImageIconUrl\":\"http://server/Images/555555.png\"," - "\"RichPresenceGameId\":2345,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":2222,\"GameId\":2345,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/555555.png\"," - "\"Achievements\":[" - GENERIC_ACHIEVEMENT_JSON("1", "0xH0000=5") "," - GENERIC_ACHIEVEMENT_JSON("2", "0xHFFFF=5") "," - GENERIC_ACHIEVEMENT_JSON("3", "0xH10000=5") - "]," - "\"Leaderboards\":[]" - "}]}"; - - g_client = mock_client_logged_in_async(); - reset_mock_api_handlers(); - - /* start loading first game */ - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_no_longer_active, g_callback_userdata); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); - - /* receive data for first game, start session for first game */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); - - /* start loading second game*/ - rc_client_begin_load_game(g_client, "ABCDEF0123456789", rc_client_callback_expect_success, g_callback_userdata); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789"); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); - - /* session started for first game, should abort */ - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); - - /* receive data for second game, start session for second game */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789", patchdata_alternate); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); - - /* session started for second game, should succeed */ - async_api_response("r=startsession&u=Username&t=ApiToken&g=2345&h=1&m=ABCDEF0123456789&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - - /* verify second game was loaded */ - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 2345); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 7); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Other Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "ABCDEF0123456789"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "555555"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 3); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); - } - - rc_client_destroy(g_client); -} - -static void test_load_game_async_login(void) -{ - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN); - - /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - /* login completion will trigger process to continue */ - async_api_response("r=login2&u=Username&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); - - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - - ASSERT_STR_EQUALS(g_client->user.username, "Username"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); -} - -static void test_load_game_async_login_with_incorrect_password(void) -{ - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN); - - /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - /* login failure will trigger process to continue */ - async_api_error("r=login2&u=Username&p=Pa%24%24word", - "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"," - "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - ASSERT_PTR_NULL(g_client->user.username); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_async_login_logout(void) -{ - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_login_aborted, g_callback_userdata); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_aborted, g_callback_userdata); - - /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - /* logout will cancel login and allow game load to proceed with failure */ - rc_client_logout(g_client); - async_api_response("r=login2&u=Username&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - ASSERT_PTR_NULL(g_client->user.username); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_async_login_aborted(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_not_logged_in_async(); - reset_mock_api_handlers(); - - handle = rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_uncalled, g_callback_userdata); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_aborted, g_callback_userdata); - - /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - /* login abort will trigger game load process to continue */ - rc_client_abort_async(g_client, handle); - async_api_response("r=login2&u=Username&p=Pa%24%24word", - "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); - - ASSERT_PTR_NULL(g_client->user.username); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_too_many_requests(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); - ASSERT_STR_EQUALS(error_message, "429 Too Many Requests"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_load_game_patch_failure(void) -{ - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_error("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", response_429, 429); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_startsession_failure(void) -{ - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, response_429, 429); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_startsession_timeout(void) -{ - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "", 504); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_timeout, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_startsession_custom_timeout(void) -{ - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, - "Request has timed out.", RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_timeout, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_patch_aborted(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", - rc_client_callback_expect_uncalled, g_callback_userdata); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - assert_api_not_called("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_startsession_aborted(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", - rc_client_callback_expect_uncalled, g_callback_userdata); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); - - rc_client_destroy(g_client); -} - -static void test_load_game_while_spectating(void) -{ - rc_client_achievement_info_t* achievement; - rc_client_leaderboard_info_t* leaderboard; - g_client = mock_client_logged_in(); - rc_client_set_spectator_mode_enabled(g_client, 1); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - /* spectator mode should not start a session or fetch unlocks */ - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - - achievement = &g_client->game->subsets->achievements[0]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5501); - ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->public_.points, 5); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - achievement = &g_client->game->subsets->achievements[1]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5502); - ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); - ASSERT_NUM_EQUALS(achievement->public_.points, 2); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - leaderboard = &g_client->game->subsets->leaderboards[0]; - ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); - ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); - ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); - ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_PTR_NOT_NULL(leaderboard->lboard); - ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); - ASSERT_PTR_NULL(leaderboard->tracker); - } - - /* spectator mode cannot be disabled if it was enabled before loading the game */ - rc_client_set_spectator_mode_enabled(g_client, 0); - ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); - - rc_client_unload_game(g_client); - - /* spectator mode can be disabled after unloading game */ - rc_client_set_spectator_mode_enabled(g_client, 0); - ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); - - rc_client_destroy(g_client); -} - -static int rc_client_callback_process_game_sets_called = 0; -static void rc_client_callback_process_game_sets(const rc_api_server_response_t* server_response, - struct rc_api_fetch_game_sets_response_t* game_sets_response, rc_client_t* client, void* userdata) -{ - ASSERT_STR_EQUALS(server_response->body, patchdata_2ach_1lbd); - ASSERT_NUM_EQUALS(game_sets_response->id, 1234); - ASSERT_NUM_EQUALS(game_sets_response->num_sets, 1); - ASSERT_NUM_EQUALS(game_sets_response->sets[0].num_achievements, 2); - ASSERT_NUM_EQUALS(game_sets_response->sets[0].num_leaderboards, 1); - rc_client_callback_process_game_sets_called = 1; -} - -static void test_load_game_process_game_sets(void) -{ - rc_client_achievement_info_t* achievement; - rc_client_leaderboard_info_t* leaderboard; - g_client = mock_client_logged_in(); - g_client->callbacks.post_process_game_sets_response = rc_client_callback_process_game_sets; - rc_client_callback_process_game_sets_called = 0; - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - - achievement = &g_client->game->subsets->achievements[0]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5501); - ASSERT_STR_EQUALS(achievement->public_.title, "Ach1"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc1"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "00234"); - ASSERT_NUM_EQUALS(achievement->public_.points, 5); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - achievement = &g_client->game->subsets->achievements[1]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5502); - ASSERT_STR_EQUALS(achievement->public_.title, "Ach2"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc2"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "00235"); - ASSERT_NUM_EQUALS(achievement->public_.points, 2); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - leaderboard = &g_client->game->subsets->leaderboards[0]; - ASSERT_NUM_EQUALS(leaderboard->public_.id, 4401); - ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard1"); - ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc1"); - ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_PTR_NOT_NULL(leaderboard->lboard); - ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); - ASSERT_PTR_NULL(leaderboard->tracker); - - ASSERT_NUM_NOT_EQUALS(rc_client_callback_process_game_sets_called, 0); - - rc_client_destroy(g_client); -} - -static void test_load_game_destroy_while_fetching_game_data(void) -{ - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); - - rc_client_destroy(g_client); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); -} - -static void test_load_unknown_game(void) -{ - const char* hash = "0123456789ABCDEFFEDCBA9876543210"; - g_client = mock_client_logged_in(); - - rc_client_load_unknown_game(g_client, hash); - - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); - - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, hash); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 0); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.id, 0); - ASSERT_STR_EQUALS(g_client->game->subsets->public_.title, ""); - ASSERT_NUM_EQUALS(g_client->game->subsets->active, 0); - - rc_client_destroy(g_client); -} - -static void test_load_unknown_game_multihash(void) -{ - const char* hash = "0123456789ABCDEFFEDCBA9876543210,FEDCBA98765432100123456789ABCDEF"; - g_client = mock_client_logged_in(); - - rc_client_load_unknown_game(g_client, hash); - - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_DONE); - ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); - - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, hash); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 0); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 0); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.id, 0); - ASSERT_STR_EQUALS(g_client->game->subsets->public_.title, ""); - ASSERT_NUM_EQUALS(g_client->game->subsets->active, 0); - - rc_client_destroy(g_client); -} - -static void test_load_game_dispatched_read_memory(void) -{ - g_client = mock_client_logged_in(); - rc_client_set_allow_background_memory_reads(g_client, 0); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); - ASSERT_PTR_NOT_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_idle(g_client); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); -} - -/* ----- unload game ----- */ - -static void test_unload_game(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); - ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); - ASSERT_PTR_NOT_NULL(rc_client_get_achievement_info(g_client, 5501)); - ASSERT_PTR_NOT_NULL(rc_client_get_leaderboard_info(g_client, 4401)); - - event_count = 0; - rc_client_unload_game(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - ASSERT_PTR_NULL(g_client->game); - ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); - ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); - ASSERT_PTR_NULL(rc_client_get_achievement_info(g_client, 5501)); - ASSERT_PTR_NULL(rc_client_get_leaderboard_info(g_client, 4401)); - - rc_client_destroy(g_client); -} - -static void test_unload_game_hides_ui(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[0x01] = 1; /* show indicator */ - memory[0x06] = 3; /* progress tracker */ - memory[0x0B] = 1; /* start leaderboard */ - memory[0x0E] = 17; /* leaderboard value */ - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 4); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - event_count = 0; - - rc_client_unload_game(g_client); - - ASSERT_NUM_EQUALS(event_count, 3); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - event_count = 0; - - rc_client_destroy(g_client); - ASSERT_NUM_EQUALS(event_count, 0); -} - -static void test_unload_game_while_fetching_game_data(void) -{ - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); - - rc_client_unload_game(g_client); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_unload_game_while_starting_session(void) -{ - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); - - rc_client_unload_game(g_client); - - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -#ifdef RC_CLIENT_SUPPORTS_HASH - -/* ----- identify and load game ----- */ - -static void rc_client_callback_expect_data_or_file_path_required(int result, const char* error_message, rc_client_t* client, void* callback_data) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); - ASSERT_STR_EQUALS(error_message, "either data or file_path is required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); -} - -static void test_identify_and_load_game_required_fields(void) -{ - g_client = mock_client_logged_in(); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, NULL, NULL, 0, - rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_identify_and_load_game_console_specified(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_console_not_specified(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -#ifndef RC_HASH_NO_ROM -uint8_t* generate_nes_file(size_t kb, int with_header, size_t* image_size); - -static void test_identify_and_load_game_multiconsole_first(void) -{ - rc_hash_iterator_t* iterator; - size_t image_size; - uint8_t* image = generate_nes_file(32, 1, &image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - /* first hash lookup should be pending. inject a secondary console into the iterator */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); - iterator = rc_client_get_load_state_hash_iterator(g_client); - ASSERT_NUM_EQUALS(iterator->index, 1); - ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); - iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ - iterator->consoles[iterator->index + 1] = 0; - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - assert_api_not_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* actual console ID returned from server */ - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_multiconsole_second(void) -{ - rc_hash_iterator_t* iterator; - size_t image_size; - uint8_t* image = generate_nes_file(32, 1, &image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - /* first hash lookup should be pending. inject a secondary console into the iterator */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); - iterator = rc_client_get_load_state_hash_iterator(g_client); - ASSERT_NUM_EQUALS(iterator->index, 1); - ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); - iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ - iterator->consoles[iterator->index + 1] = 0; - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e" ,patchdata_2ach_1lbd); - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=64b131c5c7fec32985d9c99700babb7e&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* actual console ID returned from server */ - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "64b131c5c7fec32985d9c99700babb7e"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -#endif /* RC_HASH_NO_ROM */ - -#ifndef RC_HASH_NO_DISC - -static void test_identify_and_load_game_from_disc(void) -{ - rc_hash_callbacks_t hash_callbacks; - - size_t image_size; - uint8_t* image = generate_psx_bin("SLUS_007.45", 0x07D800, &image_size); - const char cue_single_track[] = - "FILE \"game.bin\" BINARY\n" - " TRACK 01 MODE2/2352\n" - " INDEX 01 00:00:00\n"; - - g_client = mock_client_logged_in(); - - memset(&hash_callbacks, 0, sizeof(hash_callbacks)); - get_mock_filereader(&hash_callbacks.filereader); - rc_client_set_hash_callbacks(g_client, &hash_callbacks); - - mock_file(0, "game.bin", image, image_size); - mock_file(1, "game.cue", (uint8_t*)cue_single_track, sizeof(cue_single_track)); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=db433fb038cde4fb15c144e8c7dea6e3", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=db433fb038cde4fb15c144e8c7dea6e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_PLAYSTATION, "game.cue", - NULL, 0, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "db433fb038cde4fb15c144e8c7dea6e3"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -#endif - -static void test_identify_and_load_game_unknown_hash(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_unknown_hash_repeated(void) -{ - rc_client_async_handle_t* handle; - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* first request should resolve the hash asynchronously */ - handle = rc_client_begin_identify_and_load_game(g_client, - RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NOT_NULL(handle); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - - /* second request should use the hash cache and not need an asynchronous call */ - handle = rc_client_begin_identify_and_load_game(g_client, - RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NULL(handle); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_unknown_hash_multiple(void) -{ - rc_client_async_handle_t* handle; - - const size_t disk_size = 256 * 16 * 35; /* 140KB - Apple II disk size */ - uint8_t* disk = generate_generic_file(disk_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* first request */ - handle = rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_APPLE_II, - "disk1.dsk", disk, disk_size, rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NOT_NULL(handle); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=88be638f4d78b4072109e55f13e8a0ac", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "88be638f4d78b4072109e55f13e8a0ac"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - - /* second request - modify file so new hash is generated */ - disk[16]++; - handle = rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_APPLE_II, - "disk1.dsk", disk, disk_size, rc_client_callback_expect_unknown_game, g_callback_userdata); - ASSERT_PTR_NOT_NULL(handle); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=8e39c6077108cafd6193d1c649b5d695", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "8e39c6077108cafd6193d1c649b5d695"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - ASSERT_STR_EQUALS(g_client->game->public_.badge_url, default_game_badge); - - free(disk); - rc_client_destroy(g_client); -} - -#ifndef RC_HASH_NO_ROM - -static void test_identify_and_load_game_unknown_hash_multiconsole(void) -{ - rc_hash_iterator_t* iterator; - size_t image_size; - uint8_t* image = generate_nes_file(32, 1, &image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", - image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); - - /* first hash lookup should be pending. inject a secondary console into the iterator */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); - iterator = rc_client_get_load_state_hash_iterator(g_client); - ASSERT_NUM_EQUALS(iterator->index, 1); - ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); - iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ - iterator->consoles[iterator->index + 1] = 0; - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - /* when multiple hashes are tried, console will be unknown and hash will be a CSV */ - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_UNKNOWN); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857,64b131c5c7fec32985d9c99700babb7e"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - } - - rc_client_destroy(g_client); - free(image); -} - -#endif - -static void test_identify_and_load_game_unknown_hash_console_specified(void) -{ - rc_hash_iterator_t* iterator; - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* explicitly specify we only want the NES hash processed */ - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); - - /* first hash lookup should be pending. iterator should not have been initialized */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); - iterator = rc_client_get_load_state_hash_iterator(g_client); - ASSERT_NUM_EQUALS(iterator->index, 0); - ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); - - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_GAMEBOY); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - } - - rc_client_destroy(g_client); - free(image); -} - -static void assert_unknown_hash_parameters(uint32_t console_id, const char* hash, rc_client_t* client, void* callback_data) -{ - ASSERT_NUM_EQUALS(console_id, RC_CONSOLE_GAMEBOY); - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); -} - -static uint32_t rc_client_identify_unknown_hash(uint32_t console_id, const char* hash, rc_client_t* client, void* callback_data) -{ - assert_unknown_hash_parameters(console_id, hash, client, callback_data); - return 1234; -} - -static void test_identify_and_load_game_unknown_hash_client_provided(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.identify_unknown_hash = rc_client_identify_unknown_hash; - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); /* patchdata returns 17 */ - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_multihash(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_multihash_unknown_game(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", - image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 0); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, RC_CONSOLE_APPLE_II); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Unknown Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, ""); - } - - /* same hash generated for all dsk consoles - only one server call should be made */ - assert_api_call_count("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", 1); - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game_multihash_differ(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - /* modify the checksum so callback for first lookup will generate a new lookup */ - memset(&image[256], 0, 32); - - /* first lookup fails */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - ASSERT_PTR_NOT_NULL(g_client->state.load); - - /* second lookup should succeed */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -#endif /* RC_CLIENT_SUPPORTS_HASH */ - -/* ----- change media ----- */ - -static void rc_client_callback_expect_hardcore_disabled_undentified_media(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_HARDCORE_DISABLED); - ASSERT_STR_EQUALS(error_message, "Hardcore disabled. Unidentified media inserted."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -#ifdef RC_CLIENT_SUPPORTS_HASH - -static void test_change_media_required_fields(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - rc_client_begin_identify_and_change_media(g_client, NULL, NULL, 0, - rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_no_game_loaded(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_no_game_loaded, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_same_game(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - /* changing known discs within a game set is expected to succeed */ - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - /* resetting with a disc from the current game is allowed */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_known_game(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":5555}"); - - /* changing to a known disc from another game is allowed */ - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - /* resetting with a disc from another game will disable the client */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_unknown_game(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - ASSERT_TRUE(rc_client_get_hardcore_enabled(g_client)); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); - - /* changing to an unknown disc is not allowed - could be a hacked version of one of the game's discs */ - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_hardcore_disabled_undentified_media, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - ASSERT_FALSE(rc_client_get_hardcore_enabled(g_client)); - - /* resetting with a disc not from the current game will disable the client */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_unhashable(void) -{ - const char* original_hash = NULL; - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - original_hash = g_client->game->public_.hash; - - /* N64 hash will fail with Not a Nintendo 64 ROM */ - g_client->game->public_.console_id = RC_CONSOLE_NINTENDO_64; - - /* changing to a disc not supported by the system is allowed */ - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - /* switch back to the original media so we can detect a switch back to the unhashable media */ - rc_client_begin_change_media(g_client, original_hash, rc_client_callback_expect_success, g_callback_userdata); - - /* re-entrant call won't try to regenerate the hash */ - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - - /* resetting with a disc not from the current game will disable the client */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_unhashable_without_generation(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - /* changing to a disc not supported by the system is allowed */ - rc_client_begin_change_media(g_client, "[NO HASH]", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "[NO HASH]"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - /* resetting with a disc not from the current game will disable the client */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_back_and_forth(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - uint8_t* image2 = generate_generic_file(image_size); - memset(&image2[256], 0, 32); /* force image2 to be different */ - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - mock_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); - - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); - assert_api_call_count("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", 1); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image2); - free(image); -} - -static void test_change_media_while_loading(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - /* media request won't occur until patch data is received */ - assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); - assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - - /* finish loading game */ - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - /* secondary hash resolution does not occur until game is fully loaded or hash can't be compared to loaded game */ - assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_while_loading_later(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", - rc_client_callback_expect_success, g_callback_userdata); - - /* get past fetching the patch data so there's a valid console for the change media call */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); - - /* change_media should immediately attempt to resolve the new hash */ - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - - /* finish loading game - session will be started with the old hash because the new hash hasn't resolved yet */ - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_async_aborted(void) -{ - rc_client_async_handle_t* handle; - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* changing known discs within a game set is expected to succeed */ - handle = rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_uncalled, g_callback_userdata); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - /* hash should still have been captured and lookup should succeed without having to call server again */ - reset_mock_api_handlers(); - - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); - - rc_client_destroy(g_client); - free(image); -} - -static void test_change_media_client_error(void) -{ - rc_client_async_handle_t* handle; - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - mock_api_error("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); - - handle = rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, - rc_client_callback_expect_no_internet, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 2); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 1); - } - - ASSERT_PTR_NULL(handle); - - rc_client_destroy(g_client); - free(image); -} - -#endif - -static void test_change_media_from_hash_required_fields(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - rc_client_begin_change_media(g_client, NULL, - rc_client_callback_expect_hash_required, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - - rc_client_destroy(g_client); -} - - -static void test_change_media_from_hash_no_game_loaded(void) -{ - g_client = mock_client_logged_in(); - - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_no_game_loaded, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_same_game(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - /* changing known discs within a game set is expected to succeed */ - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - - /* resetting with a disc from the current game is allowed */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_known_game(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":5555}"); - - /* changing to a known disc from another game is allowed */ - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - - /* resetting with a disc from another game will disable the client */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_unknown_game(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - ASSERT_TRUE(rc_client_get_hardcore_enabled(g_client)); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); - - /* changing to an unknown disc is not allowed - could be a hacked version of one of the game's discs */ - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_hardcore_disabled_undentified_media, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - - ASSERT_FALSE(rc_client_get_hardcore_enabled(g_client)); - - /* resetting with a disc not from the current game will disable the client */ - rc_client_reset(g_client); - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_back_and_forth(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - mock_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); - - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_change_media(g_client, "4989b063a40dcfa28291ff8d675050e3", - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_change_media(g_client, "4989b063a40dcfa28291ff8d675050e3", - rc_client_callback_expect_success, g_callback_userdata); - - assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); - assert_api_call_count("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", 1); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "4989b063a40dcfa28291ff8d675050e3"); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_while_loading(void) -{ - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", - rc_client_callback_expect_success, g_callback_userdata); - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - - /* media request won't occur until patch data is received */ - assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); - assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - - /* finish loading game */ - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - /* secondary hash resolution does not occur until game is fully loaded or hash can't be compared to loaded game */ - assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_while_loading_later(void) -{ - g_client = mock_client_logged_in(); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", - rc_client_callback_expect_success, g_callback_userdata); - - /* get past fetching the patch data so there's a valid console for the change media call */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); - - /* change_media should immediately attempt to resolve the new hash */ - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - - /* finish loading game - session will be started with the old hash because the new hash hasn't resolved yet */ - async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_async_aborted(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - reset_mock_api_handlers(); - - /* changing known discs within a game set is expected to succeed */ - handle = rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_uncalled, g_callback_userdata); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ - - /* hash should still have been captured and lookup should succeed without having to call server again */ - reset_mock_api_handlers(); - - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - - rc_client_destroy(g_client); -} - -static void test_change_media_from_hash_client_error(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - mock_api_error("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); - - handle = rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", - rc_client_callback_expect_no_internet, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); /* old hash retained */ - - ASSERT_PTR_NULL(handle); - - rc_client_destroy(g_client); -} - -/* ----- get game image ----- */ - -static void test_game_get_image_url(void) -{ - char buffer[256]; - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); - ASSERT_STR_EQUALS(buffer, "http://server/Images/112233.png"); - - rc_client_destroy(g_client); -} - -/* ----- fetch hash library ----- */ - -static void test_fetch_hash_library_response(int result, const char* error_message, - rc_client_hash_library_t* list, rc_client_t* client, void* callback_userdata) -{ - rc_client_callback_expect_success(result, error_message, client, callback_userdata); - if (result != RC_OK) - return; - - ASSERT_NUM_EQUALS(list->num_entries, 3); - ASSERT_NUM_EQUALS(list->entries[0].game_id, 1); - ASSERT_STR_EQUALS(list->entries[0].hash, "aabbccddeeff00112233445566778899"); - ASSERT_NUM_EQUALS(list->entries[1].game_id, 1); - ASSERT_STR_EQUALS(list->entries[1].hash, "99aabbccddeeff001122334455667788"); - ASSERT_NUM_EQUALS(list->entries[2].game_id, 2); - ASSERT_STR_EQUALS(list->entries[2].hash, "8899aabbccddeeff0011223344556677"); - - rc_client_destroy_hash_library(list); -} - -static void test_fetch_hash_library(void) -{ - g_client = mock_client_not_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=hashlibrary&c=1", "{\"Success\":true,\"MD5List\":{" - "\"aabbccddeeff00112233445566778899\":1," - "\"99aabbccddeeff001122334455667788\":1," - "\"8899aabbccddeeff0011223344556677\":2" - "}}"); - - rc_client_begin_fetch_hash_library(g_client, 1, test_fetch_hash_library_response, g_callback_userdata); - rc_client_destroy(g_client); -} - -/* ----- fetch game titles ----- */ - -static void test_fetch_game_titles_response(int result, const char* error_message, - rc_client_game_title_list_t* list, rc_client_t* client, void* callback_userdata) -{ - rc_client_callback_expect_success(result, error_message, client, callback_userdata); - if (result != RC_OK) - return; - - ASSERT_NUM_EQUALS(list->num_entries, 3); - ASSERT_NUM_EQUALS(list->entries[0].game_id, 3); - ASSERT_STR_EQUALS(list->entries[0].title, "Game Name 3"); - ASSERT_STR_EQUALS(list->entries[0].badge_name, "010003"); - ASSERT_NUM_EQUALS(list->entries[1].game_id, 4); - ASSERT_STR_EQUALS(list->entries[1].title, "Game Name 4"); - ASSERT_STR_EQUALS(list->entries[1].badge_name, "010004"); - ASSERT_NUM_EQUALS(list->entries[2].game_id, 7); - ASSERT_STR_EQUALS(list->entries[2].title, "Game Name 7"); - ASSERT_STR_EQUALS(list->entries[2].badge_name, "010007"); - - rc_client_destroy_game_title_list(list); -} - -static void test_fetch_game_titles(void) -{ - const uint32_t game_ids[] = { 3, 4, 7 }; - g_client = mock_client_not_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=gameinfolist&g=3,4,7", "{\"Success\":true,\"Response\":[" - "{\"ID\": 3, \"Title\":\"Game Name 3\", \"ImageIcon\": \"/Images/010003.png\"}," - "{\"ID\": 4, \"Title\":\"Game Name 4\", \"ImageIcon\": \"/Images/010004.png\"}," - "{\"ID\": 7, \"Title\":\"Game Name 7\", \"ImageIcon\": \"/Images/010007.png\"}" - "]}"); - - rc_client_begin_fetch_game_titles(g_client, game_ids, 3, test_fetch_game_titles_response, g_callback_userdata); - rc_client_destroy(g_client); -} - -/* ----- all user progress ----- */ - -static void test_fetch_all_user_progress_response(int result, const char* error_message, - rc_client_all_user_progress_t* list, rc_client_t* client, void* callback_userdata) -{ - rc_client_callback_expect_success(result, error_message, client, callback_userdata); - if (result != RC_OK) - return; - - ASSERT_NUM_EQUALS(list->num_entries, 4); - ASSERT_NUM_EQUALS(list->entries[0].game_id, 10); - ASSERT_NUM_EQUALS(list->entries[0].num_achievements, 11); - ASSERT_NUM_EQUALS(list->entries[0].num_unlocked_achievements, 0); - ASSERT_NUM_EQUALS(list->entries[0].num_unlocked_achievements_hardcore, 0); - ASSERT_NUM_EQUALS(list->entries[1].game_id, 20); - ASSERT_NUM_EQUALS(list->entries[1].num_achievements, 21); - ASSERT_NUM_EQUALS(list->entries[1].num_unlocked_achievements, 22); - ASSERT_NUM_EQUALS(list->entries[1].num_unlocked_achievements_hardcore, 0); - ASSERT_NUM_EQUALS(list->entries[2].game_id, 30); - ASSERT_NUM_EQUALS(list->entries[2].num_achievements, 31); - ASSERT_NUM_EQUALS(list->entries[2].num_unlocked_achievements, 32); - ASSERT_NUM_EQUALS(list->entries[2].num_unlocked_achievements_hardcore, 33); - ASSERT_NUM_EQUALS(list->entries[3].game_id, 40); - ASSERT_NUM_EQUALS(list->entries[3].num_achievements, 41); - ASSERT_NUM_EQUALS(list->entries[3].num_unlocked_achievements, 0); - ASSERT_NUM_EQUALS(list->entries[3].num_unlocked_achievements_hardcore, 43); - - rc_client_destroy_all_user_progress(list); -} - -static void test_fetch_all_user_progress(void) -{ - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=allprogress&u=Username&t=ApiToken&c=1", "{\"Success\":true,\"Response\":{\"10\":{\"Achievements\":11}," - "\"20\":{\"Achievements\":21,\"Unlocked\":22}," - "\"30\":{\"Achievements\":31,\"Unlocked\":32,\"UnlockedHardcore\":33}," - "\"40\":{\"Achievements\":41,\"UnlockedHardcore\":43}}}"); - - rc_client_begin_fetch_all_user_progress(g_client, 1, test_fetch_all_user_progress_response, g_callback_userdata); - rc_client_destroy(g_client); -} - -/* ----- subset ----- */ - -static void test_load_subset(void) -{ - rc_client_achievement_info_t* achievement; - rc_client_leaderboard_info_t* leaderboard; - rc_client_subset_info_t* subset_info; - const rc_client_subset_t* subset; - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_subset); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_PTR_NULL(g_client->state.load); - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public_); - - ASSERT_NUM_EQUALS(g_client->game->public_.id, 1234); - ASSERT_NUM_EQUALS(g_client->game->public_.console_id, 17); - ASSERT_STR_EQUALS(g_client->game->public_.title, "Sample Game"); - ASSERT_STR_EQUALS(g_client->game->public_.hash, "0123456789ABCDEF"); - ASSERT_STR_EQUALS(g_client->game->public_.badge_name, "112233"); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_achievements, 7); - ASSERT_NUM_EQUALS(g_client->game->subsets->public_.num_leaderboards, 7); - } - - subset = rc_client_get_subset_info(g_client, 2345); - ASSERT_PTR_NOT_NULL(subset); - if (subset) { - subset_info = g_client->game->subsets->next; - ASSERT_PTR_EQUALS(subset, &subset_info->public_); - - ASSERT_NUM_EQUALS(subset->id, 2345); - ASSERT_STR_EQUALS(subset->title, "Bonus"); - ASSERT_STR_EQUALS(subset->badge_name, "112234"); - ASSERT_STR_EQUALS(subset->badge_url, "http://server/Images/112234.png"); - ASSERT_NUM_EQUALS(subset->num_achievements, 3); - ASSERT_NUM_EQUALS(subset->num_leaderboards, 2); - - achievement = &subset_info->achievements[0]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5501); - ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 5501"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc 5501"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "005501"); - ASSERT_STR_EQUALS(achievement->public_.badge_url, "https://media.retroachievements.org/Badge/005501.png"); - ASSERT_STR_EQUALS(achievement->public_.badge_locked_url, "https://media.retroachievements.org/Badge/005501_lock.png"); - ASSERT_NUM_EQUALS(achievement->public_.points, 5); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_STR_EQUALS(achievement->author, "User1"); - ASSERT_NUM_EQUALS(achievement->created_time, 1367266583); - ASSERT_NUM_EQUALS(achievement->updated_time, 1376929305); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - achievement = &subset_info->achievements[1]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5502); - ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 5502"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc 5502"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "005502"); - ASSERT_NUM_EQUALS(achievement->public_.points, 5); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - achievement = &subset_info->achievements[2]; - ASSERT_NUM_EQUALS(achievement->public_.id, 5503); - ASSERT_STR_EQUALS(achievement->public_.title, "Achievement 5503"); - ASSERT_STR_EQUALS(achievement->public_.description, "Desc 5503"); - ASSERT_STR_EQUALS(achievement->public_.badge_name, "005503"); - ASSERT_NUM_EQUALS(achievement->public_.points, 5); - ASSERT_NUM_EQUALS(achievement->public_.unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->public_.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->public_.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_PTR_NOT_NULL(achievement->trigger); - - leaderboard = &subset_info->leaderboards[0]; - ASSERT_NUM_EQUALS(leaderboard->public_.id, 81); - ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard 81"); - ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc 81"); - ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_PTR_NOT_NULL(leaderboard->lboard); - ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); - ASSERT_PTR_NULL(leaderboard->tracker); - - leaderboard = &subset_info->leaderboards[1]; - ASSERT_NUM_EQUALS(leaderboard->public_.id, 82); - ASSERT_STR_EQUALS(leaderboard->public_.title, "Leaderboard 82"); - ASSERT_STR_EQUALS(leaderboard->public_.description, "Desc 82"); - ASSERT_NUM_EQUALS(leaderboard->public_.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); - ASSERT_PTR_NOT_NULL(leaderboard->lboard); - ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); - ASSERT_PTR_NULL(leaderboard->tracker); - } - - rc_client_destroy(g_client); -} - -static void test_subset_list(void) -{ - rc_client_subset_list_t* list; - const rc_client_subset_t* subset; - - g_client = mock_client_game_loaded(patchdata_subset, no_unlocks); - - list = rc_client_create_subset_list(g_client); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_subsets, 2); - - subset = list->subsets[0]; - ASSERT_NUM_EQUALS(subset->id, 1111); - ASSERT_STR_EQUALS(subset->title, "Sample Game"); - ASSERT_STR_EQUALS(subset->badge_name, "112233"); - ASSERT_NUM_EQUALS(subset->num_achievements, 7); - ASSERT_NUM_EQUALS(subset->num_leaderboards, 7); - ASSERT_STR_EQUALS(subset->badge_url, "http://server/Images/112233.png"); - - subset = list->subsets[1]; - ASSERT_NUM_EQUALS(subset->id, 2345); - ASSERT_STR_EQUALS(subset->title, "Bonus"); - ASSERT_STR_EQUALS(subset->badge_name, "112234"); - ASSERT_NUM_EQUALS(subset->num_achievements, 3); - ASSERT_NUM_EQUALS(subset->num_leaderboards, 2); - ASSERT_STR_EQUALS(subset->badge_url, "http://server/Images/112234.png"); - - rc_client_destroy_subset_list(list); - } - - rc_client_unload_game(g_client); - ASSERT_PTR_NULL(g_client->game); - - list = rc_client_create_subset_list(g_client); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_subsets, 0); - rc_client_destroy_subset_list(list); - } - - rc_client_destroy(g_client); -} - -/* ----- achievement list ----- */ - -static void test_achievement_list_simple(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); - - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5501); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5502); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 0); - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_simple_with_unlocks(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - /* in hardcore mode, 5501 should be unlocked, but 5502 will be locked */ - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - iter = list->buckets[1].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - - rc_client_destroy_achievement_list(list); - } - - rc_client_set_hardcore_enabled(g_client, 0); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - /* in softcore mode, both should be unlocked */ - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); - - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_simple_with_unlocks_encore_mode(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_logged_in(); - rc_client_set_encore_mode_enabled(g_client, 1); - mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - /* in hardcore mode, 5501 should be unlocked, but both will appear locked due to encore mode */ - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); - - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - - rc_client_destroy_achievement_list(list); - } - - rc_client_set_hardcore_enabled(g_client, 0); - mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=0&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlock_5501h_and_5502); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - /* in softcore mode, both should be unlocked, but will appear locked due to encore mode */ - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); - - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - - rc_client_destroy_achievement_list(list); - } - - /* unlock 5501, should appear unlocked again */ - mock_memory(memory, sizeof(memory)); - rc_client_do_frame(g_client); - memory[1] = 3; - memory[2] = 7; - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=0&m=0123456789ABCDEF&v=7f066800cd3962efeb1f479e5671b59c", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501)); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - - achievement = list->buckets[0].achievements[0]; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - - achievement = list->buckets[1].achievements[0]; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Recently Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - - achievement = list->buckets[0].achievements[0]; - ASSERT_NUM_EQUALS(achievement->id, 5501); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - - achievement = list->buckets[1].achievements[0]; - ASSERT_NUM_EQUALS(achievement->id, 5502); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_simple_with_unofficial_and_unsupported(void) -{ - rc_client_achievement_list_t* list; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Unofficial"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5502); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 3); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unofficial"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_simple_with_unofficial_off(void) -{ - rc_client_achievement_list_t* list; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 0); - mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 0); - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_buckets(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); /* advance achievements out of waiting state */ - event_count = 0; - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 6); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 7); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 9); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 71); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - memory[5] = 5; /* trigger achievement 5 */ - memory[6] = 2; /* start measuring achievement 6 */ - memory[1] = 1; /* begin challenge achievement 7 */ - memory[0x11] = 100; /* start measuring achievements 70 and 71 */ - rc_client_do_frame(g_client); - event_count = 0; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 4); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); - iter = list->buckets[2].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 6); - ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 9); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 71); - ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - /* also check mapping to lock state */ - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 5); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 7); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 9); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 70); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 71); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - /* recently unlocked achievement no longer recent */ - ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; - memory[6] = 5; /* almost there achievement 6 */ - memory[1] = 0; /* stop challenge achievement 7 */ - rc_client_do_frame(g_client); - event_count = 0; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 3); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); - ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); - ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); - iter = list->buckets[1].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 7); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 9); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 71); - ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_buckets_progress_sort(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); /* advance achievements out of waiting state */ - event_count = 0; - - g_client->game->subsets->achievements[0].trigger->measured_target = 100; - g_client->game->subsets->achievements[0].trigger->measured_value = 86; - g_client->game->subsets->achievements[1].trigger->measured_target = 100; - g_client->game->subsets->achievements[1].trigger->measured_value = 85; - g_client->game->subsets->achievements[2].trigger->measured_target = 1000; - g_client->game->subsets->achievements[2].trigger->measured_value = 855; - g_client->game->subsets->achievements[3].trigger->measured_target = 100; - g_client->game->subsets->achievements[3].trigger->measured_value = 87; - g_client->game->subsets->achievements[4].trigger->measured_target = 100; - g_client->game->subsets->achievements[4].trigger->measured_value = 85; - g_client->game->subsets->achievements[5].trigger->measured_target = 100; - g_client->game->subsets->achievements[5].trigger->measured_value = 75; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 5); - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 87.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "87/100"); - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 86.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "86/100"); - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.5f); - ASSERT_STR_EQUALS(achievement->measured_progress, "855/1,000"); - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); - ASSERT_NUM_EQUALS(achievement->id, 6); - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); - ASSERT_NUM_EQUALS(achievement->id, 9); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); - ASSERT_FLOAT_EQUALS(list->buckets[1].achievements[0]->measured_percent, 75.0f); - ASSERT_STR_EQUALS(list->buckets[1].achievements[0]->measured_progress, "75/100"); - ASSERT_FLOAT_EQUALS(list->buckets[1].achievements[1]->measured_percent, 0.0f); - ASSERT_STR_EQUALS(list->buckets[1].achievements[1]->measured_progress, ""); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_buckets_progress_sort_big_ids(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_big_ids, no_unlocks); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); /* advance achievements out of waiting state */ - event_count = 0; - - g_client->game->subsets->achievements[0].trigger->measured_target = 100; - g_client->game->subsets->achievements[0].trigger->measured_value = 86; - g_client->game->subsets->achievements[1].trigger->measured_target = 100; - g_client->game->subsets->achievements[1].trigger->measured_value = 85; - g_client->game->subsets->achievements[2].trigger->measured_target = 100; - g_client->game->subsets->achievements[2].trigger->measured_value = 85; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 3); - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 86.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "86/100"); - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); - ASSERT_NUM_EQUALS(achievement->id, 6); - achievement = *iter++; - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 85.0f); - ASSERT_STR_EQUALS(achievement->measured_progress, "85/100"); - ASSERT_NUM_EQUALS(achievement->id, 4294967295UL); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_FLOAT_EQUALS(list->buckets[1].achievements[0]->measured_percent, 0.0f); - ASSERT_STR_EQUALS(list->buckets[1].achievements[0]->measured_progress, ""); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_buckets_with_unsynced(void) -{ - rc_client_achievement_list_t* list; - rc_client_achievement_t* achievement; - const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b"; - const char* unlock_request_params2 = "r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&o=2&v=3cca3f9a9cfd6c0d5b6a17b9bb539070"; - - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); - g_client->callbacks.server_call = rc_client_server_call_async; - mock_memory(memory, sizeof(memory)); - - /* discard the queued ping to make finding the retry easier */ - g_client->state.scheduled_callbacks = NULL; - - rc_client_do_frame(g_client); /* advance achievements out of waiting state */ - event_count = 0; - - memory[5] = 5; /* trigger achievement 5 */ - memory[1] = 1; /* begin challenge achievement 7 */ - rc_client_do_frame(g_client); - event_count = 0; - - /* first failure will immediately requeue the request */ - async_api_error(unlock_request_params, response_503, 503); - assert_api_pending(unlock_request_params); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - rc_client_idle(g_client); - - /* second failure will queue it */ - async_api_error(unlock_request_params, response_503, 503); - assert_api_call_count(unlock_request_params, 0); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - rc_client_idle(g_client); - event_count = 0; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) - { - ASSERT_NUM_EQUALS(list->num_buckets, 4); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - /* adjust unlock time on achievement 5 so it's no longer recent */ - achievement = (rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5); - achievement->unlock_time -= 15 * 60; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) - { - ASSERT_NUM_EQUALS(list->num_buckets, 4); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocks Not Synced to Server"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - /* allow unlock request to succeed */ - g_now += 2 * 1000; - rc_client_idle(g_client); - assert_api_pending(unlock_request_params2); - async_api_response(unlock_request_params2, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) - { - ASSERT_NUM_EQUALS(list->num_buckets, 3); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_subset_with_unofficial_and_unsupported(void) -{ - rc_client_achievement_list_t* list; - - g_client = mock_client_logged_in(); - rc_client_set_unofficial_enabled(g_client, 1); - mock_client_load_game(patchdata_subset_unofficial_unsupported, no_unlocks); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 3); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 6); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 7); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 9); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 70); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[5]->id, 71); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Unofficial"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 8); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Unofficial"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); - - rc_client_destroy_achievement_list(list); - } - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 5); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 6); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[2]->id, 7); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[3]->id, 9); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[4]->id, 70); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[5]->id, 71); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unofficial"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Locked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5501); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Unofficial"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5502); - - ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[4].label, "Bonus - Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5503); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_list_subset_buckets(void) -{ - rc_client_achievement_list_t* list; - const rc_client_achievement_t** iter; - const rc_client_achievement_t* achievement; - - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_subset, unlock_8_and_5502); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); /* advance achievements out of waiting state */ - event_count = 0; - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", - "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 4); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); - iter = list->buckets[0].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 5); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 6); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 7); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 9); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 71); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Locked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 5503); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5502); - - rc_client_destroy_achievement_list(list); - } - - memory[5] = 5; /* trigger achievement 5 */ - memory[6] = 2; /* start measuring achievement 6 */ - memory[1] = 1; /* begin challenge achievement 7 */ - memory[0x11] = 100; /* start measuring achievements 70 and 71 */ - memory[0x17] = 7; /* trigger achievement 5501 */ - rc_client_do_frame(g_client); - event_count = 0; - - /* set the unlock time for achievement 5 back one second to ensure consistent sorting */ - ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time--; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 6); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 5); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Locked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); - iter = list->buckets[2].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 6); - ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 9); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 71); - ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[3].label, "Sample Game - Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); - - ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[4].label, "Bonus - Locked"); - ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5503); - - ASSERT_NUM_EQUALS(list->buckets[5].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[5].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[5].label, "Bonus - Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[5].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[5].achievements[0]->id, 5502); - - rc_client_destroy_achievement_list(list); - } - - /* recently unlocked achievements no longer recent */ - ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; - ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5501))->unlock_time -= 15 * 60; - memory[6] = 5; /* almost there achievement 6 */ - memory[1] = 0; /* stop challenge achievement 7 */ - rc_client_do_frame(g_client); - event_count = 0; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 5); - - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); - ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); - ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Locked"); - ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); - iter = list->buckets[1].achievements; - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 7); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 9); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, "25,600/100,000"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - achievement = *iter++; - ASSERT_NUM_EQUALS(achievement->id, 71); - ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); - ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); - - ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Locked"); - ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); - ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5503); - - ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); - ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[4].label, "Bonus - Unlocked"); - ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5501); - ASSERT_NUM_EQUALS(list->buckets[4].achievements[1]->id, 5502); - - rc_client_destroy_achievement_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_achievement_get_image_url(void) -{ - char buffer[256]; - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), - RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, buffer, sizeof(buffer)), RC_OK); - ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234.png"); - - ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), - RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, buffer, sizeof(buffer)), RC_OK); - ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); - - ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), - RC_CLIENT_ACHIEVEMENT_STATE_DISABLED, buffer, sizeof(buffer)), RC_OK); - ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); - - ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), - RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE, buffer, sizeof(buffer)), RC_OK); - ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); - - rc_client_destroy(g_client); -} - -/* ----- leaderboards ----- */ - -static void test_leaderboard_list_simple(void) -{ - rc_client_leaderboard_list_t* list; - const rc_client_leaderboard_t** iter; - const rc_client_leaderboard_t* leaderboard; - uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; - - g_client = mock_client_logged_in(); - mock_memory(memory, sizeof(memory)); - mock_client_load_game(patchdata_exhaustive, no_unlocks); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "All"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - rc_client_destroy_leaderboard_list(list); - } - - memory[0x0A] = 1; /* start 45,46,47 */ - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "All"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - rc_client_destroy_leaderboard_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_leaderboard_list_simple_with_unsupported(void) -{ - rc_client_leaderboard_list_t* list; - const rc_client_leaderboard_t** iter; - const rc_client_leaderboard_t* leaderboard; - uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; - - g_client = mock_client_logged_in(); - mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ - mock_client_load_game(patchdata_exhaustive, no_unlocks); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "All"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - - rc_client_destroy_leaderboard_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_leaderboard_list_buckets(void) -{ - rc_client_leaderboard_list_t* list; - const rc_client_leaderboard_t** iter; - const rc_client_leaderboard_t* leaderboard; - uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; - - g_client = mock_client_logged_in(); - mock_memory(memory, sizeof(memory)); - mock_client_load_game(patchdata_exhaustive, no_unlocks); - - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - rc_client_destroy_leaderboard_list(list); - } - - memory[0x0A] = 1; /* start 45,46,47 */ - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - rc_client_destroy_leaderboard_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_leaderboard_list_buckets_with_unsupported(void) -{ - rc_client_leaderboard_list_t* list; - const rc_client_leaderboard_t** iter; - const rc_client_leaderboard_t* leaderboard; - uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; - - g_client = mock_client_logged_in(); - mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ - mock_client_load_game(patchdata_exhaustive, no_unlocks); - - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - - rc_client_destroy_leaderboard_list(list); - } - - memory[0x0B] = 3; /* start 52 */ - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 3); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 1); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); - ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 5); - - iter = list->buckets[2].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - - rc_client_destroy_leaderboard_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_leaderboard_list_subset(void) -{ - rc_client_leaderboard_list_t* list; - const rc_client_leaderboard_t** iter; - const rc_client_leaderboard_t* leaderboard; - uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; - - g_client = mock_client_logged_in(); - mock_memory(memory, sizeof(memory)); - mock_client_load_game(patchdata_subset, no_unlocks); - - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Inactive"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Inactive"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 2); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 81); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 82); - - rc_client_destroy_leaderboard_list(list); - } - - memory[0x0A] = 1; /* start 45,46,47 */ - memory[0x08] = 2; /* start 82 */ - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 3); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 4); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 45); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 82); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1111); - ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Inactive"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 48); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 51); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 52); - - ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); - ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Inactive"); - ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 1); - - iter = list->buckets[2].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 81); - - rc_client_destroy_leaderboard_list(list); - } - - rc_client_destroy(g_client); -} - -static void test_leaderboard_list_hidden(void) -{ - rc_client_leaderboard_list_t* list; - const rc_client_leaderboard_t** iter; - const rc_client_leaderboard_t* leaderboard; - uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; - - g_client = mock_client_logged_in(); - mock_memory(memory, sizeof(memory)); - mock_client_load_game(patchdata_leaderboards_hidden, no_unlocks); - - rc_client_do_frame(g_client); - - /* hidden leaderboards (45+48) should not appear in list */ - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - - rc_client_destroy_leaderboard_list(list); - } - - memory[0x0A] = 1; /* start 45,46,47 */ - rc_client_do_frame(g_client); - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - if (list) { - ASSERT_NUM_EQUALS(list->num_buckets, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); - - iter = list->buckets[0].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 46); - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 47); - - ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); - ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); - ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); - - iter = list->buckets[1].leaderboards; - leaderboard = *iter++; - ASSERT_NUM_EQUALS(leaderboard->id, 44); - - rc_client_destroy_leaderboard_list(list); - } - - rc_client_destroy(g_client); -} - -static const char* lbinfo_4401_top_10 = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," - "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," - "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," - "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":78," - "\"Entries\":[" - "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," - "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," - "{\"User\":\"DisplayName\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," - "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," - "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," - "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," - "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," - "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," - "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," - "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" - "]" - "}}"; - -static const char* lbinfo_4401_top_10_no_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," - "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," - "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," - "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":78," - "\"Entries\":[" - "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," - "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," - "{\"User\":\"PlayerJ\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," - "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," - "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," - "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," - "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," - "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," - "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," - "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" - "]" - "}}"; - -static const char* lbinfo_4401_near_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," - "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," - "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," - "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\",\"TotalEntries\":78," - "\"Entries\":[" - "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":17,\"Index\":17,\"DateSubmitted\":1615654895}," - "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":18,\"Index\":18,\"DateSubmitted\":1615634566}," - "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":19,\"Index\":19,\"DateSubmitted\":1615653844}," - "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":19,\"Index\":20,\"DateSubmitted\":1615623878}," - "{\"User\":\"DisplayName\",\"Score\":3811,\"Rank\":19,\"Index\":21,\"DateSubmitted\":1615234553}," - "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":19,\"Index\":22,\"DateSubmitted\":1615653284}," - "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":23,\"Index\":23,\"DateSubmitted\":1615632174}," - "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":24,\"Index\":24,\"DateSubmitted\":1616384834}," - "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":25,\"Index\":25,\"DateSubmitted\":1615238383}," - "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":26,\"Index\":26,\"DateSubmitted\":1615638984}" - "]" - "}}"; - -static rc_client_leaderboard_entry_list_t* g_leaderboard_entries = NULL; -static void rc_client_callback_expect_leaderboard_entry_list(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_OK); - ASSERT_PTR_NULL(error_message); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); - - ASSERT_PTR_NOT_NULL(list); - g_leaderboard_entries = list; -} - -static void test_fetch_leaderboard_entries(void) -{ - rc_client_leaderboard_entry_t* entry; - char url[256]; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_leaderboard_entries = NULL; - - mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); - - rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, - rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); - ASSERT_PTR_NOT_NULL(g_leaderboard_entries); - - ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); - ASSERT_NUM_EQUALS(g_leaderboard_entries->total_entries, 78); - - entry = g_leaderboard_entries->entries; - ASSERT_STR_EQUALS(entry->user, "PlayerG"); - ASSERT_STR_EQUALS(entry->display, "003524"); - ASSERT_NUM_EQUALS(entry->index, 1); - ASSERT_NUM_EQUALS(entry->rank, 1); - ASSERT_NUM_EQUALS(entry->submitted, 1615654895); - - ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); - ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerG.png"); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerB"); - ASSERT_STR_EQUALS(entry->display, "003645"); - ASSERT_NUM_EQUALS(entry->index, 2); - ASSERT_NUM_EQUALS(entry->rank, 2); - ASSERT_NUM_EQUALS(entry->submitted, 1615634566); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "DisplayName"); - ASSERT_STR_EQUALS(entry->display, "003754"); - ASSERT_NUM_EQUALS(entry->index, 3); - ASSERT_NUM_EQUALS(entry->rank, 3); - ASSERT_NUM_EQUALS(entry->submitted, 1615234553); - - ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); - ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/DisplayName.png"); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerC"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 4); - ASSERT_NUM_EQUALS(entry->rank, 4); - ASSERT_NUM_EQUALS(entry->submitted, 1615653844); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerF"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 5); - ASSERT_NUM_EQUALS(entry->rank, 4); - ASSERT_NUM_EQUALS(entry->submitted, 1615623878); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerA"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 6); - ASSERT_NUM_EQUALS(entry->rank, 4); - ASSERT_NUM_EQUALS(entry->submitted, 1615653284); - - ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); - ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerA.png"); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerI"); - ASSERT_STR_EQUALS(entry->display, "003902"); - ASSERT_NUM_EQUALS(entry->index, 7); - ASSERT_NUM_EQUALS(entry->rank, 7); - ASSERT_NUM_EQUALS(entry->submitted, 1615632174); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerE"); - ASSERT_STR_EQUALS(entry->display, "003956"); - ASSERT_NUM_EQUALS(entry->index, 8); - ASSERT_NUM_EQUALS(entry->rank, 8); - ASSERT_NUM_EQUALS(entry->submitted, 1616384834); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerD"); - ASSERT_STR_EQUALS(entry->display, "003985"); - ASSERT_NUM_EQUALS(entry->index, 9); - ASSERT_NUM_EQUALS(entry->rank, 9); - ASSERT_NUM_EQUALS(entry->submitted, 1615238383); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerH"); - ASSERT_STR_EQUALS(entry->display, "004012"); - ASSERT_NUM_EQUALS(entry->index, 10); - ASSERT_NUM_EQUALS(entry->rank, 10); - ASSERT_NUM_EQUALS(entry->submitted, 1615638984); - - ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 2); - - rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); - rc_client_destroy(g_client); -} - -static void test_fetch_leaderboard_entries_no_user(void) -{ - rc_client_leaderboard_entry_t* entry; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_leaderboard_entries = NULL; - - mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10_no_user); - - rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, - rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); - ASSERT_PTR_NOT_NULL(g_leaderboard_entries); - - ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); - - entry = g_leaderboard_entries->entries; - ASSERT_STR_EQUALS(entry->user, "PlayerG"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerB"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerJ"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerC"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerF"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerA"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerI"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerE"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerD"); - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerH"); - - ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, -1); - - rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); - rc_client_destroy(g_client); -} - -static void test_fetch_leaderboard_entries_around_user(void) -{ - rc_client_leaderboard_entry_t* entry; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_leaderboard_entries = NULL; - - mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); - - rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, - rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); - ASSERT_PTR_NOT_NULL(g_leaderboard_entries); - - ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); - ASSERT_NUM_EQUALS(g_leaderboard_entries->total_entries, 78); - - entry = g_leaderboard_entries->entries; - ASSERT_STR_EQUALS(entry->user, "PlayerG"); - ASSERT_STR_EQUALS(entry->display, "003524"); - ASSERT_NUM_EQUALS(entry->index, 17); - ASSERT_NUM_EQUALS(entry->rank, 17); - ASSERT_NUM_EQUALS(entry->submitted, 1615654895); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerB"); - ASSERT_STR_EQUALS(entry->display, "003645"); - ASSERT_NUM_EQUALS(entry->index, 18); - ASSERT_NUM_EQUALS(entry->rank, 18); - ASSERT_NUM_EQUALS(entry->submitted, 1615634566); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerC"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 19); - ASSERT_NUM_EQUALS(entry->rank, 19); - ASSERT_NUM_EQUALS(entry->submitted, 1615653844); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerF"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 20); - ASSERT_NUM_EQUALS(entry->rank, 19); - ASSERT_NUM_EQUALS(entry->submitted, 1615623878); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "DisplayName"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 21); - ASSERT_NUM_EQUALS(entry->rank, 19); - ASSERT_NUM_EQUALS(entry->submitted, 1615234553); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerA"); - ASSERT_STR_EQUALS(entry->display, "003811"); - ASSERT_NUM_EQUALS(entry->index, 22); - ASSERT_NUM_EQUALS(entry->rank, 19); - ASSERT_NUM_EQUALS(entry->submitted, 1615653284); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerI"); - ASSERT_STR_EQUALS(entry->display, "003902"); - ASSERT_NUM_EQUALS(entry->index, 23); - ASSERT_NUM_EQUALS(entry->rank, 23); - ASSERT_NUM_EQUALS(entry->submitted, 1615632174); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerE"); - ASSERT_STR_EQUALS(entry->display, "003956"); - ASSERT_NUM_EQUALS(entry->index, 24); - ASSERT_NUM_EQUALS(entry->rank, 24); - ASSERT_NUM_EQUALS(entry->submitted, 1616384834); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerD"); - ASSERT_STR_EQUALS(entry->display, "003985"); - ASSERT_NUM_EQUALS(entry->index, 25); - ASSERT_NUM_EQUALS(entry->rank, 25); - ASSERT_NUM_EQUALS(entry->submitted, 1615238383); - - ++entry; - ASSERT_STR_EQUALS(entry->user, "PlayerH"); - ASSERT_STR_EQUALS(entry->display, "004012"); - ASSERT_NUM_EQUALS(entry->index, 26); - ASSERT_NUM_EQUALS(entry->rank, 26); - ASSERT_NUM_EQUALS(entry->submitted, 1615638984); - - ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 4); - - rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_leaderboard_entry_list_login_required(int result, const char* error_message, - rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); - ASSERT_STR_EQUALS(error_message, "Login required"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); - ASSERT_PTR_NULL(list); -} - -static void test_fetch_leaderboard_entries_around_user_not_logged_in(void) -{ - g_client = mock_client_not_logged_in(); - g_leaderboard_entries = NULL; - - mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); - - rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, - rc_client_callback_expect_leaderboard_entry_list_login_required, g_callback_userdata); - ASSERT_PTR_NULL(g_leaderboard_entries); - - assert_api_not_called("r=lbinfo&i=4401&u=Username&c=10"); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_leaderboard_uncalled(int result, const char* error_message, - rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) -{ - ASSERT_FAIL("Callback should not have been called.") -} - -static void test_fetch_leaderboard_entries_async_aborted(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - g_leaderboard_entries = NULL; - - handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, - rc_client_callback_expect_leaderboard_uncalled, g_callback_userdata); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); - ASSERT_PTR_NULL(g_leaderboard_entries); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_leaderboard_internet_not_available(int result, const char* error_message, - rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_NO_RESPONSE); - ASSERT_STR_EQUALS(error_message, "Internet not available."); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); - ASSERT_PTR_NULL(list); -} - -static void test_fetch_leaderboard_entries_client_error(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - g_leaderboard_entries = NULL; - - mock_api_error("r=lbinfo&i=4401&c=10", "Internet not available.", RC_API_SERVER_RESPONSE_CLIENT_ERROR); - - handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, - rc_client_callback_expect_leaderboard_internet_not_available, g_callback_userdata); - - ASSERT_PTR_NULL(handle); - ASSERT_PTR_NULL(g_leaderboard_entries); - - rc_client_destroy(g_client); -} - -static void test_map_leaderboard_format(void) -{ - int i; - - for (i = 0; i < 30; ++i) { - switch (i) { - case RC_FORMAT_SECONDS: - case RC_FORMAT_CENTISECS: - case RC_FORMAT_MINUTES: - case RC_FORMAT_SECONDS_AS_MINUTES: - case RC_FORMAT_FRAMES: - ASSERT_NUM_EQUALS(rc_client_map_leaderboard_format(i), RC_CLIENT_LEADERBOARD_FORMAT_TIME); - break; - - case RC_FORMAT_SCORE: - ASSERT_NUM_EQUALS(rc_client_map_leaderboard_format(i), RC_CLIENT_LEADERBOARD_FORMAT_SCORE); - break; - - default: - ASSERT_NUM_EQUALS(rc_client_map_leaderboard_format(i), RC_CLIENT_LEADERBOARD_FORMAT_VALUE); - break; - } - } -} - -/* ----- do frame ----- */ - -static void test_do_frame_bounds_check_system(void) -{ - const uint32_t memory_size = 0x10010; /* provide more memory than system expects */ - uint8_t* memory = (uint8_t*)calloc(1, memory_size); - ASSERT_PTR_NOT_NULL(memory); - - g_client = mock_client_game_loaded(patchdata_bounds_check_system, no_unlocks); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":7,\"AchievementsRemaining\":4}"); - - ASSERT_PTR_NOT_NULL(g_client->game); - ASSERT_NUM_EQUALS(g_client->game->max_valid_address, 0xFFFF); - - assert_achievement_state(g_client, 1, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 2, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 3, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ - assert_achievement_state(g_client, 4, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* cannot read two bytes from 0xFFFF, but size isn't enforced until do_frame */ - assert_achievement_state(g_client, 6, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ - assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - - /* verify that reading at the edge of the memory bounds fails */ - mock_memory(memory, 0x10000); - rc_client_do_frame(g_client); - assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* cannot read two bytes from 0xFFFF */ - - /* set up memory so achievement 7 would trigger if the pointed at address were valid */ - /* achievement should not trigger - invalid address should be ignored */ - memory[0x10000] = 5; - memory[0x00000] = 1; /* byte(0xFFFF + byte(0x0000)) == 5 */ - rc_client_do_frame(g_client); - assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - - /* even if the extra memory is available, it shouldn't try to read beyond the system defined max address */ - mock_memory(memory, memory_size); - rc_client_do_frame(g_client); - assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - - /* change max valid address so memory will be evaluated. achievement should trigger */ - g_client->game->max_valid_address = memory_size - 1; - rc_client_do_frame(g_client); - assert_achievement_state(g_client, 7, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - - rc_client_destroy(g_client); - free(memory); -} - -static void test_do_frame_bounds_check_available(void) -{ - const rc_trigger_t* trigger; - uint8_t memory[8] = { 0,0,0,0,0,0,0,0 }; - g_client = mock_client_game_loaded(patchdata_bounds_check_8, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - /* all addresses are valid according to the system, so no achievements should be disabled yet. */ - assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - trigger = ((rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 808))->trigger; - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_WAITING); - - /* limit the memory that's actually exposed and try to process a frame */ - mock_memory(memory, sizeof(memory)); - rc_client_do_frame(g_client); - - assert_achievement_state(g_client, 408, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 508, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 608, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 708, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_DISABLED); - - assert_achievement_state(g_client, 416, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 516, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 616, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 716, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ - assert_achievement_state(g_client, 816, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ - - assert_achievement_state(g_client, 424, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 524, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 24-bit read actually fetches 32-bits */ - assert_achievement_state(g_client, 624, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ - assert_achievement_state(g_client, 724, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ - assert_achievement_state(g_client, 824, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ - - assert_achievement_state(g_client, 432, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - assert_achievement_state(g_client, 532, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only three bytes available */ - assert_achievement_state(g_client, 632, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ - assert_achievement_state(g_client, 732, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ - assert_achievement_state(g_client, 832, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger_already_awarded(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger_server_error(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":false,\"Error\":\"Achievement not found\"}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - /* achievement still counts as triggered */ - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ - - /* but an error should have been reported */ - event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); - ASSERT_PTR_NOT_NULL(event); - ASSERT_STR_EQUALS(event->server_error->api, "award_achievement"); - ASSERT_STR_EQUALS(event->server_error->error_message, "Achievement not found"); - ASSERT_NUM_EQUALS(event->server_error->result, RC_API_FAILURE); - ASSERT_NUM_EQUALS(event->server_error->related_id, 8); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger_while_spectating(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); - rc_client_set_spectator_mode_enabled(g_client, 1); - ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":false,\"Error\":\"Achievement should not have been unlocked in spectating mode\"}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - /* achievement still counts as triggered */ - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ - - /* expect API not called */ - assert_api_not_called("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - rc_client_set_spectator_mode_enabled(g_client, 0); - ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); - } - - rc_client_destroy(g_client); -} - -static int rc_client_callback_deny_unlock(uint32_t achievement_id, rc_client_t* client) -{ - return 0; -} - -static int rc_client_callback_allow_unlock(uint32_t achievement_id, rc_client_t* client) -{ - return 1; -} - -static void test_do_frame_achievement_trigger_blocked(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - uint32_t num_active; - const char* api_call8 = "r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"; - const char* api_call9 = "r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6"; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.can_submit_achievement_unlock = rc_client_callback_deny_unlock; - - ASSERT_PTR_NOT_NULL(g_client->game); - mock_memory(memory, sizeof(memory)); - num_active = g_client->game->runtime.trigger_count; - - mock_api_response(api_call8, - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - mock_api_response(api_call9, - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 12350); /* 12345+5 - not updated by server response that didn't happen */ - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - assert_api_not_called(api_call8); - - g_client->callbacks.can_submit_achievement_unlock = rc_client_callback_allow_unlock; - - memory[9] = 9; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - assert_api_called(api_call9); - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger_automatic_retry(const char* response, int status_code) -{ - const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; - const char* unlock_request_params1 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=1&v=4b4af09722aeb0e8a16c152f9a646281"; - const char* unlock_request_params3 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=3&v=d5bc287a030084aa77a911b6c2c5fc96"; - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - /* discard the queued ping to make finding the retry easier */ - g_client->state.scheduled_callbacks = NULL; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[1] = 3; - memory[2] = 7; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* first failure will immediately requeue the request */ - async_api_error(unlock_request_params, response, status_code); - assert_api_pending(unlock_request_params); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second failure will queue it */ - async_api_error(unlock_request_params, response, status_code); - assert_api_call_count(unlock_request_params, 0); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_DISCONNECTED, 0)); - event_count = 0; - - /* advance time so queued request gets processed */ - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 1 * 1000); - g_now += 1 * 1000; - rc_client_idle(g_client); - assert_api_pending(unlock_request_params1); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - /* third failure will requeue it */ - async_api_error(unlock_request_params1, response, status_code); - assert_api_call_count(unlock_request_params, 0); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 2 * 1000); - g_now += 2 * 1000; - - rc_client_idle(g_client); - assert_api_pending(unlock_request_params3); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - /* success should not requeue it and update player score */ - async_api_response(unlock_request_params3, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - /* reconnected event should be pending, watch for it */ - ASSERT_NUM_EQUALS(event_count, 0); - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RECONNECTED, 0)); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger_automatic_retry_empty(void) -{ - test_do_frame_achievement_trigger_automatic_retry("", 200); -} - -static void test_do_frame_achievement_trigger_automatic_retry_429(void) -{ - test_do_frame_achievement_trigger_automatic_retry(response_429, 429); -} - -static void test_do_frame_achievement_trigger_automatic_retry_502(void) -{ - test_do_frame_achievement_trigger_automatic_retry(response_502, 502); -} - -static void test_do_frame_achievement_trigger_automatic_retry_503(void) -{ - test_do_frame_achievement_trigger_automatic_retry(response_503, 503); -} - -static void test_do_frame_achievement_trigger_automatic_retry_custom_timeout(void) -{ - test_do_frame_achievement_trigger_automatic_retry("Request has timed out.", RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR); -} - -static void test_do_frame_achievement_trigger_automatic_retry_generic_empty_response(void) -{ - test_do_frame_achievement_trigger_automatic_retry("", 0); -} - -static void test_do_frame_achievement_trigger_subset(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_subset, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", - "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - memory[0x17] = 7; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); - ASSERT_NUM_EQUALS(g_client->user.score, 5437); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_trigger_rarity(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive_typed, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->type, RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION); - ASSERT_FLOAT_EQUALS(event->achievement->rarity, 86.0f); - ASSERT_FLOAT_EQUALS(event->achievement->rarity_hardcore, 73.1f); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_measured(void) -{ - const rc_client_achievement_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=70&h=1&m=0123456789ABCDEF&v=61e40027573e2cde88b49d27f6804879", - "{\"Success\":true,\"Score\":5432,\"AchievementID\":70,\"AchievementsRemaining\":11}"); - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=71&h=1&m=0123456789ABCDEF&v=3a8d55b81d391557d5111306599a2b0d", - "{\"Success\":true,\"Score\":5432,\"AchievementID\":71,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[0x10] = 0x39; memory[0x11] = 0x30; /* 12345 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ - - achievement = rc_client_get_achievement_info(g_client, 70); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, "12,345/100,000"); - - achievement = rc_client_get_achievement_info(g_client, 71); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, "12%"); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* increment measured value - raw counter will report progress change, percentage will not */ - memory[0x10] = 0x3A; /* 12346 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ - - achievement = rc_client_get_achievement_info(g_client, 70); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, "12,346/100,000"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* increment measured value - raw counter will report progress change, percentage will not */ - memory[0x11] = 0x33; /* 13114 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ - - achievement = rc_client_get_achievement_info(g_client, 70); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, "13,114/100,000"); - - achievement = rc_client_get_achievement_info(g_client, 71); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, "13%"); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* trigger measured achievements - progress becomes blank */ - memory[0x10] = 0xA0; memory[0x11] = 0x86; memory[0x12] = 0x01; /* 100000 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); /* two TRIGGERED events, and no PROGRESS_INDICATOR_SHOW events */ - - achievement = rc_client_get_achievement_info(g_client, 70); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - - achievement = rc_client_get_achievement_info(g_client, 71); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_measured_progress_event(void) -{ - rc_client_event_t* event; - const rc_client_achievement_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=6&h=1&m=0123456789ABCDEF&v=65206f4290098ecd30c7845e895057d0", - "{\"Success\":true,\"Score\":5432,\"AchievementID\":6,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[0x06] = 3; /* 3/6 */ - memory[0x11] = 0xC3; memory[0x10] = 0x4F; /* 49999/100000 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - /* 3/6 = 50%, 49999/100000 = 49.999% */ - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); - ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); - - /* both achievements should have been updated, */ - achievement = rc_client_get_achievement_info(g_client, 6); - ASSERT_STR_EQUALS(achievement->measured_progress, "3/6"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 50.0); - - achievement = rc_client_get_achievement_info(g_client, 70); - ASSERT_STR_EQUALS(achievement->measured_progress, "49,999/100,000"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 49.999); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* any change will trigger the popup - even dropping */ - memory[0x10] = 0x4E; /* 49998 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 70); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); - ASSERT_STR_EQUALS(event->achievement->measured_progress, "49,998/100,000"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* don't trigger popup when value changes to 0 as the measured_progress string will be blank */ - memory[0x06] = 0; /* 0 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = rc_client_get_achievement_info(g_client, 6); - ASSERT_STR_EQUALS(achievement->measured_progress, ""); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); - - /* both at 50%, only report first */ - memory[0x06] = 3; /* 3/6 */ - memory[0x11] = 0xC3; memory[0x10] = 0x50; /* 50000/100000 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 6); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); - ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second slightly ahead */ - memory[0x6] = 4; /* 4/6 */ - memory[0x12] = 1; memory[0x11] = 0x04; memory[0x10] = 0x6B; /* 66667/100000 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 70); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); - ASSERT_STR_EQUALS(event->achievement->measured_progress, "66,667/100,000"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* don't show popup on trigger */ - memory[0x06] = 6; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); - ASSERT_STR_EQUALS(event->achievement->measured_progress, ""); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - ASSERT_PTR_NOT_NULL(g_client->game->progress_tracker.hide_callback); - g_now += 2 * 1000; - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_measured_progress_reshown(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_PTR_NOT_NULL(g_client->game); - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[0x06] = 3; /* 3/6 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - event_count = 0; - - /* should be two callbacks queued - hiding the progress indicator, and the rich presence update */ - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks, g_client->game->progress_tracker.hide_callback); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks->next); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); - - /* advance time to hide the progress indicator */ - g_now = g_client->game->progress_tracker.hide_callback->when; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - event_count = 0; - - /* only the rich presence update should be scheduled */ - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next); - - /* advance time to just before the rich presence update */ - g_now = g_client->state.scheduled_callbacks->when - 10; - - /* reschedule the progress indicator */ - memory[0x06] = 4; /* 4/6 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - event_count = 0; - - /* should be two callbacks queued - rich presence update, then hiding the progress indicator */ - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->next, g_client->game->progress_tracker.hide_callback); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_challenge_indicator(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", - "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[1] = 1; /* show indicator */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[1] = 0; /* hide indicator */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[1] = 1; /* show indicator */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* trigger achievement - expect both hide and trigger events. both should have triggered achievement data */ - memory[7] = 7; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_challenge_indicator_primed_while_reset(void) -{ - static const char* patchdata = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":7,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0xH0001=3_T:0xH0002=4_R:0xH0003=3\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}" - "]," - "\"Leaderboards\":[]" - "}]}"; - - rc_client_event_t* event; - rc_client_achievement_info_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata, no_unlocks); - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", - "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_ACTIVE); - - memory[1] = 3; /* primed */ - memory[3] = 3; /* but reset */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_ACTIVE); - - memory[3] = 0; /* disable reset */ - - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 1); /* show indicator event */ - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_PRIMED); - - memory[3] = 3; /* reset active */ - - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 1); /* hide indicator event */ - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - rc_client_destroy(g_client); -} - -static void test_do_frame_achievement_challenge_indicator_primed_while_reset_next(void) -{ - static const char* patchdata = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"\",\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[" - "{\"ID\":7,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," - "\"MemAddr\":\"0xH0001=3_T:0xH0002=4_Z:0xH0003=3_P:0xH0006=1.10.\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," - "\"Created\":1367266583,\"Modified\":1376929305}" - "]," - "\"Leaderboards\":[]" - "}]}"; - - rc_client_event_t* event; - rc_client_achievement_info_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata, no_unlocks); - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", - "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); - - memory[6] = 1; /* pause condition will gain a hit, but not enough to pause it */ - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_ACTIVE); - - /* ResetNextIf will clear hits on condition 4, _and_ the trigger will be primed at the same time. - * The ResetNextIf will cause rc_evaluate_trigger to return RESET, but we still want to raise a - * PRIMED event out of rc_client. */ - memory[1] = 3; /* primed */ - memory[3] = 3; /* reset pause condition */ - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 1); /* show indicator event */ - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); - - ASSERT_NUM_EQUALS(achievement->trigger->state, RC_TRIGGER_STATE_PRIMED); - - memory[3] = 0; /* disable reset, pause counter increases */ - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[3] = 0; /* re-enable reset. don't expect any events */ - - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 0); - - rc_client_destroy(g_client); -} - -static void test_do_frame_mastery(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); - - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); - ASSERT_PTR_NOT_NULL(event); - - memory[9] = 9; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); - ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", - "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); - - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_mastery_encore(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[8] = 8; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", - "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); - - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); - ASSERT_PTR_NOT_NULL(event); - - memory[9] = 9; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); - ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", - "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); - - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_mastery_subset(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_subset, unlock_5501_and_5502); - g_client->callbacks.server_call = rc_client_server_call_async; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - const uint32_t num_active = g_client->game->runtime.trigger_count; - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[0x19] = 9; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5503); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); - ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); - ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5503)); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); - ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - async_api_response("r=awardachievement&u=Username&t=ApiToken&a=5503&h=1&m=0123456789ABCDEF&v=50486c3ea9e2e32bb3683017b1488f88", - "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5503,\"AchievementsRemaining\":0}"); - - ASSERT_NUM_EQUALS(event_count, 0); - ASSERT_NUM_EQUALS(g_client->user.score, 5432); - ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_SUBSET_COMPLETED, 2345); - ASSERT_PTR_NOT_NULL(event); - ASSERT_PTR_EQUALS(event->subset, rc_client_get_subset_info(g_client, 2345)); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_started(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_update(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* update the leaderboard */ - memory[0x0E] = 18; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000018"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_failed(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* cancel the leaderboard */ - memory[0x0C] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->leaderboard_id, 44); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->submitted_score, "000017"); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->best_score, "000023"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->new_rank, 2); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_entries, 2); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_top_entries, 2); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].username, "Player1"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[0].rank, 1); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].score, "000044"); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].username, "Username"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[1].rank, 2); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].score, "000023"); - ASSERT_PTR_NOT_NULL(event->leaderboard_scoreboard->top_entries); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit_server_error(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", - "{\"Success\":false,\"Error\":\"Leaderboard not found\"}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - /* an error should have also been reported */ - event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); - ASSERT_PTR_NOT_NULL(event); - ASSERT_STR_EQUALS(event->server_error->api, "submit_lboard_entry"); - ASSERT_STR_EQUALS(event->server_error->error_message, "Leaderboard not found"); - ASSERT_NUM_EQUALS(event->server_error->result, RC_API_FAILURE); - ASSERT_NUM_EQUALS(event->server_error->related_id, 44); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit_while_spectating(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); - rc_client_set_spectator_mode_enabled(g_client, 1); - ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); - - mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", - "{\"Success\":false,\"Error\":\"Leaderboard entry should not have been submitted in spectating mode\"}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* expect API not called */ - assert_api_not_called("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit_immediate(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_leaderboard_immediate_submit, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard (it will immediately submit) */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); /* don't expect start or tracker events - only submit and scoreboard */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->leaderboard_id, 44); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->submitted_score, "000017"); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->best_score, "000023"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->new_rank, 2); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_entries, 2); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_top_entries, 2); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].username, "Player1"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[0].rank, 1); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].score, "000044"); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].username, "Username"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[1].rank, 2); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].score, "000023"); - ASSERT_PTR_NOT_NULL(event->leaderboard_scoreboard->top_entries); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit_hidden(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_leaderboards_hidden, no_unlocks); - - /* hidden leaderboards should still start/track/submit normally. they just don't appear in list */ - - ASSERT_PTR_NOT_NULL(g_client->game); - mock_memory(memory, sizeof(memory)); - - mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=48&s=17&m=0123456789ABCDEF&v=468a1f9e9475d8c4d862f48cc8806018", - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0A] = 2; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 48); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 48); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->leaderboard_id, 48); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->submitted_score, "000017"); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->best_score, "000023"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->new_rank, 2); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_entries, 2); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->num_top_entries, 2); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].username, "Player1"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[0].rank, 1); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[0].score, "000044"); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].username, "Username"); - ASSERT_NUM_EQUALS(event->leaderboard_scoreboard->top_entries[1].rank, 2); - ASSERT_STR_EQUALS(event->leaderboard_scoreboard->top_entries[1].score, "000023"); - ASSERT_PTR_NOT_NULL(event->leaderboard_scoreboard->top_entries); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - rc_client_destroy(g_client); -} - -static int rc_client_callback_deny_leaderboard(uint32_t leaderboard_id, rc_client_t* client) -{ - return 0; -} - -static int rc_client_callback_allow_leaderboard(uint32_t leaderboard_id, rc_client_t* client) -{ - return 1; -} - -static void test_do_frame_leaderboard_submit_blocked(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - const char* api_call = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.can_submit_leaderboard_entry = rc_client_callback_deny_leaderboard; - - ASSERT_PTR_NOT_NULL(g_client->game); - mock_memory(memory, sizeof(memory)); - - mock_api_response(api_call, - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0B] = 0; - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - /* but make sure the server wasn't called */ - assert_api_not_called(api_call); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - g_client->callbacks.can_submit_leaderboard_entry = rc_client_callback_allow_leaderboard; - - /* restart the leaderboard - will immediately submit */ - memory[0x0B] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44)); - - /* make sure the server was called */ - assert_api_called(api_call); - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit_softcore(void) -{ - rc_client_leaderboard_info_t* leaderboard; - rc_client_event_t* event; - uint8_t memory[64]; - const char* api_call = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - rc_client_set_hardcore_enabled(g_client, 0); - - ASSERT_PTR_NOT_NULL(g_client->game); - mock_memory(memory, sizeof(memory)); - - mock_api_response(api_call, - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard - will be ignored in softcore */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* allow leaderboards to be processed in softcore. have to manually activate as it wasn't activated on game load */ - g_client->state.allow_leaderboards_in_softcore = 1; - leaderboard = (rc_client_leaderboard_info_t*)rc_client_get_leaderboard_info(g_client, 44); - leaderboard->public_.state = RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; - leaderboard->lboard->state = RC_LBOARD_STATE_ACTIVE; - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0B] = 0; - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - /* but make sure the server wasn't called */ - assert_api_not_called(api_call); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - g_client->state.hardcore = 1; - - /* restart the leaderboard - will immediately submit */ - memory[0x0B] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD, 44)); - - /* make sure the server was called */ - assert_api_called(api_call); - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_tracker_sharing(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start one leaderboard (one tracker) */ - memory[0x0B] = 1; - memory[0x0E] = 17; - memory[0x0F] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start additional leaderboards (45,46,47) - 45 and 46 should generate new trackers */ - memory[0x0A] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 5); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 45); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 45 */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 46); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 46 */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 47); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 44,47 */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); /* 45 has different size */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 3); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); /* 46 has different format */ - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start additional leaderboard (48) - should share tracker with 44 */ - memory[0x0A] = 2; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 3); /* 44,47,48 */ - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* cancel leaderboard 44 */ - memory[0x0C] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 47,48 */ - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* cancel leaderboard 45 */ - memory[0x0C] = 2; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 45); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 2); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* cancel leaderboard 46 */ - memory[0x0C] = 3; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 46); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 3); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* cancel 47, start 51 */ - memory[0x0A] = 3; - memory[0x0B] = 0; - memory[0x0C] = 4; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 47); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 1); /* 48 */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 51 */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* cancel 48 */ - memory[0x0C] = 5; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 48); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); - ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 0); /* */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_tracker_sharing_hits(void) -{ - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start leaderboards 51,52 (share tracker) */ - memory[0x0A] = 3; - memory[0x0B] = 3; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 52); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 52)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); - - /* hit count ticks */ - memory[0x09] = 1; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); - - /* cancel leaderboard 51 */ - memory[0x0C] = 6; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 51); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "2"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "2"); - - /* hit count ticks */ - memory[0x0A] = 0; - memory[0x0C] = 0; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "3"); - - /* restart leaderboard 51 - hit count differs, can't share */ - memory[0x0A] = 3; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "1"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "4"); /* 52 */ - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); /* 51 */ - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_leaderboard_submit_automatic_retry(void) -{ - const char* submit_entry_params = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; - const char* submit_entry_params1 = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&o=1&v=659685352e6d8e14923ea32da6f8c3e4"; - const char* submit_entry_params3 = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&o=3&v=91debb749e90c2beff4f21430716dac9"; - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - /* discard the queued ping to make finding the retry easier */ - g_client->state.scheduled_callbacks = NULL; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* start the leaderboard */ - memory[0x0B] = 1; - memory[0x0E] = 17; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* submit the leaderboard */ - memory[0x0D] = 1; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); - ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* first failure will immediately requeue the request */ - async_api_response(submit_entry_params, ""); - assert_api_pending(submit_entry_params); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second failure will queue it for one second later */ - async_api_response(submit_entry_params, ""); - assert_api_not_pending(submit_entry_params); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - - /* disconnected event should be raised after retry is queued */ - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_DISCONNECTED, 0)); - event_count = 0; - - /* advance time and process scheduled callbacks */ - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 1 * 1000); - g_now += 1 * 1000; - - rc_client_idle(g_client); - assert_api_pending(submit_entry_params1); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - /* third failure will requeue it for two seconds later */ - async_api_response(submit_entry_params1, ""); - assert_api_not_pending(submit_entry_params1); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 2 * 1000); - g_now += 2 * 1000; - - rc_client_idle(g_client); - assert_api_pending(submit_entry_params3); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - /* success should not requeue it and update player score */ - async_api_response(submit_entry_params3, - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - /* reconnected event should be pending, watch for it */ - ASSERT_NUM_EQUALS(event_count, 0); - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RECONNECTED, 0)); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_multiple_automatic_retry(void) -{ - const char* unlock_5501_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; - const char* unlock_5501_request_params1 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=1&v=4b4af09722aeb0e8a16c152f9a646281"; - const char* unlock_5501_request_params4 = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&o=4&v=63646fa1b30797144cc87881f7d95932"; - const char* unlock_5502_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5502&h=1&m=0123456789ABCDEF&v=8d7405f5aacc9b4b334619a0dae62a56"; - const char* unlock_5502_request_params1 = "r=awardachievement&u=Username&t=ApiToken&a=5502&h=1&m=0123456789ABCDEF&o=1&v=46d77e89548126a88c0cdda1980c4dd4"; - const char* submit_entry_params = "r=submitlbentry&u=Username&t=ApiToken&i=4401&s=17&m=0123456789ABCDEF&v=13b4cdfe295a97783e78bb48c8910867"; - const char* submit_entry_params1 = "r=submitlbentry&u=Username&t=ApiToken&i=4401&s=17&m=0123456789ABCDEF&o=1&v=16fe62616f0e39d0a2620b50cc004928"; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - g_client->callbacks.server_call = rc_client_server_call_async; - - /* discard the queued ping to make finding the retry easier */ - g_client->state.scheduled_callbacks = NULL; - - ASSERT_PTR_NOT_NULL(g_client->game); - - mock_memory(memory, sizeof(memory)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - g_now = 100000; /* time 0 */ - memory[1] = 3; /* trigger 5501 */ - memory[2] = 7; /* trigger 5501 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501)); - event_count = 0; - - /* first failure will immediately requeue the request */ - async_api_response(unlock_5501_request_params, ""); - assert_api_pending(unlock_5501_request_params); - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* second failure will queue it for one second later (101000) */ - async_api_response(unlock_5501_request_params, ""); - assert_api_not_pending(unlock_5501_request_params); - - /* disconnected event should be raised after retry is queued */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_DISCONNECTED, 0)); - event_count = 0; - - /* advance time 500ms. trigger second achievement and start leaderboard */ - g_now += 500; /* 100500 */ - memory[1] = 2; /* trigger 5502 */ - memory[2] = 9; /* trigger 5502 */ - memory[0x0C] = 1; /* start leaderboard 1 */ - memory[0x0E] = 17; /* leaderboard 1 value */ - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 3); - /* disconnected event should not be raised again */ - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5502)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 4401)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - event_count = 0; - - /* first failure will immediately requeue the request */ - async_api_response(unlock_5502_request_params, ""); - assert_api_pending(unlock_5502_request_params); - - /* second failure will queue it for one second later (101500) */ - async_api_response(unlock_5502_request_params, ""); - assert_api_not_pending(unlock_5502_request_params); - - /* advance time 250ms. submit leaderboard */ - g_now += 250; /* 100750 */ - memory[0x0D] = 2; /* submit leaderboard 1 */ - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - /* disconnected event should not be raised again */ - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 4401)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - event_count = 0; - - /* first failure will immediately requeue the request */ - async_api_response(submit_entry_params, ""); - assert_api_pending(submit_entry_params); - - /* second failure will queue it for one second later (101750) */ - async_api_response(submit_entry_params, ""); - assert_api_not_pending(submit_entry_params); - - /* advance time. first callback will fail again and be queued for two seconds later (103000) */ - g_now += 350; /* 101100 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - assert_api_pending(unlock_5501_request_params1); - async_api_response(unlock_5501_request_params1, ""); - assert_api_not_pending(unlock_5501_request_params1); - - /* advance time. second callback will succeed */ - g_now += 500; /* 101600 */ - rc_client_do_frame(g_client); - assert_api_pending(unlock_5502_request_params1); - async_api_response(unlock_5502_request_params1, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - assert_api_not_pending(unlock_5502_request_params1); - - /* reconnected event should not be raised until all pending requests are handled */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* advance time. third callback will succeed */ - g_now += 500; /* 102100 */ - rc_client_do_frame(g_client); - assert_api_pending(submit_entry_params1); - async_api_response(submit_entry_params1, - "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); - assert_api_not_pending(submit_entry_params); - - /* reconnected event should not be raised until all pending requests are handled */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - /* advance time. first callback will finally succeed */ - g_now += 2000; /* 104100 */ - rc_client_do_frame(g_client); - assert_api_pending(unlock_5501_request_params4); - async_api_response(unlock_5501_request_params4, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); - assert_api_not_pending(unlock_5501_request_params4); - - /* reconnected event should be pending, watch for it */ - ASSERT_NUM_EQUALS(event_count, 0); - rc_client_idle(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RECONNECTED, 0)); - - rc_client_destroy(g_client); -} - -static void test_do_frame_rich_presence_hitcount(void) -{ - char buffer[8]; - const char* patchdata_rich_presence_hit_count = "{\"Success\":true," - "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\n@Number(M:1=1)\"," - "\"Sets\":[{" - "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," - "\"ImageIconUrl\":\"http://server/Images/112233.png\"," - "\"Achievements\":[]," - "\"Leaderboards\":[]" - "}]}"; - g_client = mock_client_game_loaded(patchdata_rich_presence_hit_count, no_unlocks); - ASSERT_PTR_NOT_NULL(g_client->game); - - ASSERT_NUM_EQUALS(rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)), 1); - ASSERT_STR_EQUALS(buffer, "0"); - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)), 1); - ASSERT_STR_EQUALS(buffer, "1"); - - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)), 1); - ASSERT_STR_EQUALS(buffer, "2"); - - rc_client_destroy(g_client); -} - -static void test_clock_get_now_millisecs(void) -{ - rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); - rc_get_time_millisecs_func_t get_millisecs = client->callbacks.get_time_millisecs; - -#ifdef RC_NO_SLEEP - rc_clock_t time1; - ASSERT_PTR_NOT_NULL(get_millisecs); - time1 = get_millisecs(client); - ASSERT_NUM_NOT_EQUALS(time1, 0); -#else - rc_clock_t time1, time2, diff; - time_t first = time(NULL), now; - - do { - ASSERT_PTR_NOT_NULL(get_millisecs); - time1 = get_millisecs(client); - ASSERT_NUM_NOT_EQUALS(time1, 0); - -#if defined(_WIN32) - Sleep(50); -#else - usleep(50000); -#endif - - time2 = get_millisecs(client); - ASSERT_NUM_NOT_EQUALS(time2, 0); - diff = time2 - time1; - - ASSERT_NUM_GREATER(diff, 49); - if (diff < 100) - break; - - now = time(NULL); - if (now - first >= 3) { - ASSERT_FAIL("could not get a 50ms sleep interval within 3 seconds"); - break; - } - - } while (1); -#endif - - rc_client_destroy(client); -} - -/* ----- ping ----- */ - -static void test_idle_ping(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - rc_client_scheduled_callback_t ping_callback; - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ping_callback = g_client->state.scheduled_callbacks->callback; - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); - g_now += 30 * 1000; - - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - - g_client->state.frames_processed++; /* ping won't get called if no frames have been processed */ - rc_client_idle(g_client); - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&h=1&x=0123456789ABCDEF"); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - } - - /* unloading game should unschedule ping */ - rc_client_unload_game(g_client); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - rc_client_destroy(g_client); -} - -static void test_do_frame_ping_rich_presence(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - rc_client_scheduled_callback_t ping_callback; - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ping_callback = g_client->state.scheduled_callbacks->callback; - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); - g_now += 30 * 1000; - - mock_memory(memory, sizeof(memory)); - memory[0x03] = 25; - - /* before rc_client_do_frame, memory will not have been read. all values will be 0 */ - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a0&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - - g_client->state.frames_processed++; /* ping won't get called if no frames have been processed */ - rc_client_idle(g_client); - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a0&h=1&x=0123456789ABCDEF"); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - g_now += 120 * 1000; - - /* rc_client_do_frame will update the memory, so the message will contain appropriate data */ - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - - rc_client_do_frame(g_client); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - g_now += 120 * 1000; - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); - - /* change the memory to make sure the rich presence gets updated */ - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - memory[0x03] = 75; - - rc_client_do_frame(g_client); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - g_now += 120 * 1000; - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75&h=1&x=0123456789ABCDEF"); - - /* no change to rich presence strings. make sure the callback still gets called again */ - rc_client_do_frame(g_client); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - g_now += 120 * 1000; - - assert_api_call_count("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a75&h=1&x=0123456789ABCDEF", 2); - } - - rc_client_destroy(g_client); -} - -static int rc_client_callback_rich_presence_override_allow(rc_client_t* client, char buffer[], size_t buffersize) -{ - memcpy(buffer, "Custom", 7); - return 0; -} - -static int rc_client_callback_rich_presence_override_replace(rc_client_t* client, char buffer[], size_t buffersize) -{ - memcpy(buffer, "Custom", 7); - return 6; -} - -static void test_do_frame_ping_rich_presence_override_allowed(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.rich_presence_override = rc_client_callback_rich_presence_override_allow; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - rc_client_scheduled_callback_t ping_callback; - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ping_callback = g_client->state.scheduled_callbacks->callback; - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); - g_now += 30 * 1000; - - mock_memory(memory, sizeof(memory)); - memory[0x03] = 25; - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - - /* ping won't get called if no frames have been processed. do_frame will increment frames_processed */ - rc_client_do_frame(g_client); - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - } - - rc_client_destroy(g_client); -} - -static void test_do_frame_ping_rich_presence_override_replaced(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - g_client->callbacks.rich_presence_override = rc_client_callback_rich_presence_override_replace; - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) - { - rc_client_scheduled_callback_t ping_callback; - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ping_callback = g_client->state.scheduled_callbacks->callback; - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); - g_now += 30 * 1000; - - mock_memory(memory, sizeof(memory)); - memory[0x03] = 25; - - /* before rc_client_do_frame, can_submit only ignores the m parameter. ping still occurs */ - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Custom&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - - /* ping won't get called if no frames have been processed. do_frame will increment frames_processed */ - rc_client_do_frame(g_client); - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Custom&h=1&x=0123456789ABCDEF"); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - g_now += 120 * 1000; - - /* ping still happens every two minutes, even if message not provided */ - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Custom&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - } - - rc_client_destroy(g_client); -} - -static void test_idle_ping_while_not_running(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - - ASSERT_PTR_NOT_NULL(g_client->game); - if (g_client->game) { - rc_client_scheduled_callback_t ping_callback; - memory[0x03] = 25; - mock_memory(memory, sizeof(memory)); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ping_callback = g_client->state.scheduled_callbacks->callback; - - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 30 * 1000); - g_now += 30 * 1000; - - mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", "{\"Success\":true}"); - - /* if no frames have been processed since the last ping, a ping shouldn't be sent */ - g_client->state.frames_processed = g_client->state.frames_at_last_ping; - rc_client_idle(g_client); - - assert_api_not_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - g_now += 120 * 1000; - - /* do_frame will increment frames_processed, and ping will get called */ - - rc_client_do_frame(g_client); - - assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF"); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - g_now += 120 * 1000; - - /* no frames processed. ping shouldn't be sent, but still rescheduled */ - - rc_client_idle(g_client); - - assert_api_call_count("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25&h=1&x=0123456789ABCDEF", 1); - - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_NUM_EQUALS(g_client->state.scheduled_callbacks->when, g_now + 120 * 1000); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); - } - - /* unloading game should unschedule ping */ - rc_client_unload_game(g_client); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); - - rc_client_destroy(g_client); -} - -/* ----- reset ----- */ - -static void test_reset_hides_widgets(void) -{ - const rc_client_leaderboard_t* leaderboard; - const rc_client_achievement_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - memory[0x01] = 1; /* challenge indicator for achievement 7 */ - memory[0x06] = 3; /* progress indicator for achievement 6 */ - memory[0x0A] = 2; /* tracker for leaderboard 48 */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 4); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - rc_client_reset(g_client); - - ASSERT_NUM_EQUALS(event_count, 3); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); - - /* non tracked achievements/leaderboards should also be reset to waiting */ - achievement = rc_client_get_achievement_info(g_client, 5); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); - - leaderboard = rc_client_get_leaderboard_info(g_client, 46); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); - - rc_client_destroy(g_client); -} - -static void test_reset_detaches_hide_progress_indicator_event(void) -{ - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - memory[0x06] = 3; /* progress indicator for achievement 6 */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - event_count = 0; - - /* should be two callbacks queued - hiding the progress indicator, and the rich presence update */ - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks, g_client->game->progress_tracker.hide_callback); - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks->next); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); - - /* advance time to hide the progress indicator */ - g_now = g_client->game->progress_tracker.hide_callback->when; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - event_count = 0; - - /* only the rich presence update should be scheduled */ - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next); - - /* advance time to just before the rich presence update */ - g_now = g_client->state.scheduled_callbacks->when - 10; - - /* reschedule the progress indicator */ - memory[0x06] = 4; /* 4/6 */ - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - event_count = 0; - - /* should be two callbacks queued - rich presence update, then hiding the progress indicator */ - ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); - ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->next, g_client->game->progress_tracker.hide_callback); - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next->next); - - rc_client_reset(g_client); - - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - - /* only the rich presence update should be scheduled */ - ASSERT_PTR_NULL(g_client->state.scheduled_callbacks->next); - - rc_client_destroy(g_client); -} - -/* ----- pause ----- */ - -static void test_can_pause(void) -{ - uint16_t frames_needed, frames_needed2, frames_needed3, frames_needed4; - uint32_t frames_remaining; - int i; - - /* pause without client should be allowed */ - ASSERT_NUM_EQUALS(rc_client_can_pause(NULL, NULL), 1); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - - rc_client_do_frame(g_client); - - /* first pause should always be allowed */ - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - frames_needed = g_client->state.unpaused_frame_decay; - - /* if no frames have been processed, the client is still paused, so pause is allowed */ - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed); - - /* do a few frames (not enough to allow pause) - pause should still not be allowed */ - for (i = 0; i < 10; i++) - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); - ASSERT_NUM_EQUALS(frames_remaining, 10); - ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed - 10); - - /* do enough frames to allow pause, but not clear out the decay value. - * pause should be allowed, and the decay value should be reset to a higher value. */ - for (i = 0; i < 20; i++) - rc_client_do_frame(g_client); - - ASSERT_NUM_GREATER(g_client->state.unpaused_frame_decay, 0); - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - frames_needed2 = g_client->state.unpaused_frame_decay; - ASSERT_NUM_GREATER(frames_needed2, frames_needed); - - /* do enough frames to allow pause before - should not allow pause now */ - for (i = 0; i < 25; i++) - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); - ASSERT_NUM_EQUALS(frames_remaining, 15); - ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed2 - 25); - - /* do enough frames to allow pause, but not clear out the decay value. - * pause should be allowed, and the decay value should be reset to an even higher value. */ - for (i = 0; i < 35; i++) - rc_client_do_frame(g_client); - - ASSERT_NUM_GREATER(g_client->state.unpaused_frame_decay, 0); - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - frames_needed3 = g_client->state.unpaused_frame_decay; - ASSERT_NUM_GREATER(frames_needed3, frames_needed2); - - /* completely clear out the decay. decay value should drop, but not all the way. */ - for (i = 0; i < frames_needed3; i++) - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - frames_needed4 = g_client->state.unpaused_frame_decay; - ASSERT_NUM_LESS(frames_needed4, frames_needed3); - ASSERT_NUM_GREATER(frames_needed4, frames_needed); - - /* completely clear out the decay. decay value should drop back to default - * have to do this twice to get through the decayed cycles */ - for (i = 0; i < frames_needed4 * 2; i++) - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - ASSERT_NUM_EQUALS(g_client->state.unpaused_frame_decay, frames_needed); - - /* disable hardcore. pause should be allowed immediately */ - rc_client_set_hardcore_enabled(g_client, 0); - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - - rc_client_destroy(g_client); -} - -/* ----- progress ----- */ - -static void test_deserialize_progress_updates_widgets(void) -{ - const rc_client_leaderboard_t* leaderboard; - const rc_client_achievement_t* achievement; - const rc_client_event_t* event; - uint8_t* serialized1; - uint8_t* serialized2; - size_t serialize_size; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - /* create an initial checkpoint */ - serialize_size = rc_client_progress_size(g_client); - serialized1 = (uint8_t*)malloc(serialize_size); - serialized2 = (uint8_t*)malloc(serialize_size); - ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized1), RC_OK); - - /* activate some widgets */ - memory[0x01] = 1; /* challenge indicator for achievement 7 */ - memory[0x06] = 4; /* progress indicator for achievement 6*/ - memory[0x0A] = 2; /* tracker for leaderboard 48 */ - memory[0x0E] = 25; /* leaderboard 48 value */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 4); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - /* capture the state with the widgets visible */ - ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized2), RC_OK); - - /* deserialize current state. expect progress tracker hide */ - ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - event_count = 0; - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - /* deserialize original state. expect challenge indicator hide, tracker hide */ - ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized1), RC_OK); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_ACTIVE); - - /* deserialize second state. expect challenge indicator show, tracker show */ - event_count = 0; - ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - /* update tracker value */ - memory[0x0E] = 30; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 1); - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000030"); - - /* deserialize second state. expect challenge tracker update to old value */ - event_count = 0; - ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); - ASSERT_NUM_EQUALS(event_count, 1); - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - free(serialized2); - free(serialized1); - rc_client_destroy(g_client); -} - -static void test_deserialize_progress_null(void) -{ - const rc_client_leaderboard_t* leaderboard; - const rc_client_achievement_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - /* activate some widgets */ - memory[0x01] = 1; /* challenge indicator for achievement 7 */ - memory[0x0A] = 2; /* tracker for leaderboard 48 */ - memory[0x0E] = 25; /* leaderboard 48 value */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ - ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, NULL), RC_OK); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); - - /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ - ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* advance frame, challenge indicator and leaderboard tracker should reappear */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - rc_client_destroy(g_client); -} - -static void test_deserialize_progress_invalid(void) -{ - const rc_client_leaderboard_t* leaderboard; - const rc_client_achievement_t* achievement; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - /* activate some widgets */ - memory[0x01] = 1; /* challenge indicator for achievement 7 */ - memory[0x0A] = 2; /* tracker for leaderboard 48 */ - memory[0x0E] = 25; /* leaderboard 48 value */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 0); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); - - /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ - ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, memory), RC_INVALID_STATE); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - - achievement = rc_client_get_achievement_info(g_client, 7); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); - ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); - - leaderboard = rc_client_get_leaderboard_info(g_client, 48); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); - - /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ - ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; - - /* advance frame, challenge indicator and leaderboard tracker should reappear */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - rc_client_destroy(g_client); -} - -static void test_deserialize_progress_sized(void) -{ - uint8_t* serialized1; - uint8_t* serialized2; - size_t serialize_size; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - /* create an initial checkpoint */ - serialize_size = rc_client_progress_size(g_client); - serialized1 = (uint8_t*)malloc(serialize_size); - serialized2 = (uint8_t*)malloc(serialize_size); - ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, serialized1, serialize_size - 1), RC_INSUFFICIENT_BUFFER); - ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, serialized1, serialize_size), RC_OK); - - /* activate some widgets */ - memory[0x01] = 1; /* challenge indicator for achievement 7 */ - memory[0x06] = 4; /* progress indicator for achievement 6*/ - memory[0x0A] = 2; /* tracker for leaderboard 48 */ - memory[0x0E] = 25; /* leaderboard 48 value */ - event_count = 0; - rc_client_do_frame(g_client); - - ASSERT_NUM_EQUALS(event_count, 4); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6)); - event_count = 0; - - /* capture the state with the widgets visible */ - ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, serialized2, serialize_size), RC_OK); - - /* deserialize current state. expect progress tracker hide */ - ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, serialized2, serialize_size), RC_OK); - ASSERT_NUM_EQUALS(event_count, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE, 0)); - event_count = 0; - - /* deserialize original state with incorrect size. expect challenge indicator hide, tracker hide */ - ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, serialized1, serialize_size - 1), RC_INSUFFICIENT_BUFFER); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); - - /* deserialize second state. expect challenge indicator show, tracker show */ - event_count = 0; - ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, serialized2, serialize_size), RC_OK); - ASSERT_NUM_EQUALS(event_count, 2); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); - - free(serialized2); - free(serialized1); - rc_client_destroy(g_client); -} - -static void test_deserialize_progress_unknown_game(void) -{ - uint8_t buffer[128]; - - g_client = mock_client_logged_in(); - - reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); - rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); - - ASSERT_NUM_EQUALS(rc_client_progress_size(g_client), 0); - ASSERT_NUM_EQUALS(rc_client_serialize_progress_sized(g_client, buffer, sizeof(buffer)), RC_NO_GAME_LOADED); - ASSERT_NUM_EQUALS(rc_client_deserialize_progress_sized(g_client, buffer, sizeof(buffer)), RC_NO_GAME_LOADED); - - rc_client_destroy(g_client); -} - -/* ----- processing required ----- */ - -static void test_processing_required(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - ASSERT_TRUE(rc_client_is_processing_required(g_client)); - - rc_client_destroy(g_client); -} - -static void test_processing_required_empty_game(void) -{ - g_client = mock_client_game_loaded(patchdata_empty, no_unlocks); - - ASSERT_FALSE(rc_client_is_processing_required(g_client)); - - rc_client_destroy(g_client); -} - -static void test_processing_required_rich_presence_only(void) -{ - g_client = mock_client_game_loaded(patchdata_rich_presence_only, no_unlocks); - - ASSERT_TRUE(rc_client_is_processing_required(g_client)); - - rc_client_destroy(g_client); -} - -static void test_processing_required_leaderboard_only(void) -{ - g_client = mock_client_game_loaded(patchdata_leaderboard_only, no_unlocks); - - ASSERT_TRUE(rc_client_is_processing_required(g_client)); - - rc_client_destroy(g_client); -} - -static void test_processing_required_after_mastery(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501_and_5502); - - ASSERT_TRUE(rc_client_is_processing_required(g_client)); - - rc_client_destroy(g_client); -} - -static void test_processing_required_after_mastery_no_leaderboards(void) -{ - g_client = mock_client_game_loaded(patchdata_2ach_0lbd, unlock_5501_and_5502); - - ASSERT_FALSE(rc_client_is_processing_required(g_client)); - - rc_client_destroy(g_client); -} - -/* ----- settings ----- */ - -static void test_set_hardcore_disable(void) -{ - const rc_client_achievement_t* achievement; - const rc_client_leaderboard_t* leaderboard; - const rc_trigger_t* trigger; - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - trigger = ((rc_client_achievement_info_t*)achievement)->trigger; - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); - - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - trigger = ((rc_client_achievement_info_t*)achievement)->trigger; - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_WAITING); - - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* only 5502 should be active*/ - - leaderboard = rc_client_get_leaderboard_info(g_client, 4401); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); - - rc_client_set_hardcore_enabled(g_client, 0); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); - ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); - - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - trigger = ((rc_client_achievement_info_t*)achievement)->trigger; - - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(trigger->state, RC_TRIGGER_STATE_TRIGGERED); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ - - leaderboard = rc_client_get_leaderboard_info(g_client, 4401); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); - - rc_client_destroy(g_client); -} - -static void test_set_hardcore_disable_active_tracker(void) -{ - const rc_client_leaderboard_t* leaderboard; - rc_client_event_t* event; - uint8_t memory[64]; - memset(memory, 0, sizeof(memory)); - - g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501h_and_5502); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - mock_memory(memory, sizeof(memory)); - - rc_client_do_frame(g_client); - - memory[0x0C] = 1; - memory[0x0E] = 25; - event_count = 0; - rc_client_do_frame(g_client); - ASSERT_NUM_EQUALS(event_count, 2); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 4401); - ASSERT_PTR_NOT_NULL(event); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); - - leaderboard = rc_client_get_leaderboard_info(g_client, 4401); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); - - event_count = 0; - rc_client_set_hardcore_enabled(g_client, 0); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); - ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); - ASSERT_NUM_EQUALS(event_count, 1); - - leaderboard = rc_client_get_leaderboard_info(g_client, 4401); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); - - event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); - ASSERT_PTR_NOT_NULL(event); - ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); - - rc_client_destroy(g_client); -} - -static void test_set_hardcore_enable(void) -{ - const rc_client_achievement_t* achievement; - const rc_client_leaderboard_t* leaderboard; - - g_client = mock_client_logged_in(); - rc_client_set_hardcore_enabled(g_client, 0); - mock_client_load_game_softcore(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); - - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ - } - - leaderboard = rc_client_get_leaderboard_info(g_client, 4401); - ASSERT_PTR_NOT_NULL(leaderboard); - if (leaderboard) { - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); - } - - /* when enabling hardcore, flag waiting_for_reset. this will prevent processing until rc_client_reset is called */ - event_count = 0; - rc_client_set_hardcore_enabled(g_client, 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 1); - ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RESET, 0)); - - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* 5502 should be active*/ - } - - leaderboard = rc_client_get_leaderboard_info(g_client, 4401); - ASSERT_PTR_NOT_NULL(leaderboard); - if (leaderboard) { - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); - } - - /* resetting clears waiting_for_reset */ - rc_client_reset(g_client); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); - - /* hardcore already enabled, attempting to set it again shouldn't flag waiting_for_reset */ - rc_client_set_hardcore_enabled(g_client, 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); - - rc_client_destroy(g_client); -} - -static void test_set_hardcore_enable_no_game_loaded(void) -{ - g_client = mock_client_logged_in(); - rc_client_set_hardcore_enabled(g_client, 0); - - /* enabling hardcore before a game is loaded just toggles the flag */ - event_count = 0; - rc_client_set_hardcore_enabled(g_client, 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(event_count, 0); - - rc_client_destroy(g_client); -} - -static void test_set_hardcore_enable_encore_mode(void) -{ - const rc_client_achievement_t* achievement; - rc_client_achievement_info_t* achievement_info; - - g_client = mock_client_logged_in(); - rc_client_set_encore_mode_enabled(g_client, 1); - mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); - - g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; - g_client->game->runtime.triggers[1].trigger->state = RC_TRIGGER_STATE_ACTIVE; - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ - ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - } - - /* toggle hardcore mode should retain active achievements */ - rc_client_set_hardcore_enabled(g_client, 0); - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); - } - - /* toggle hardcore mode should retain active achievements */ - rc_client_set_hardcore_enabled(g_client, 1); - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); - - /* trigger an achievement */ - achievement_info = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 5501); - achievement_info->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; - g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_TRIGGERED; - - /* toggle hardcore mode should retain active achievements */ - rc_client_set_hardcore_enabled(g_client, 0); - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* only one active now */ - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); - ASSERT_PTR_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger, g_client->game->runtime.triggers[0].trigger); - } - - /* toggle hardcore mode should retain active achievements */ - rc_client_set_hardcore_enabled(g_client, 1); - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); - - rc_client_destroy(g_client); -} - -static void test_set_encore_mode_enable(void) -{ - const rc_client_achievement_t* achievement; - - g_client = mock_client_logged_in(); - rc_client_set_encore_mode_enabled(g_client, 1); - mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - } - - /* toggle encore mode with a game loaded has no effect */ - rc_client_set_encore_mode_enabled(g_client, 0); - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - } - - rc_client_destroy(g_client); -} - -static void test_set_encore_mode_disable(void) -{ - const rc_client_achievement_t* achievement; - - g_client = mock_client_logged_in(); - rc_client_set_encore_mode_enabled(g_client, 1); - rc_client_set_encore_mode_enabled(g_client, 0); - mock_client_load_game(patchdata_2ach_1lbd, unlock_5501h_and_5502); - - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - } - - /* toggle encore mode with a game loaded has no effect */ - rc_client_set_encore_mode_enabled(g_client, 1); - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - - achievement = rc_client_get_achievement_info(g_client, 5501); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); - } - achievement = rc_client_get_achievement_info(g_client, 5502); - ASSERT_PTR_NOT_NULL(achievement); - if (achievement) { - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - } - - rc_client_destroy(g_client); -} - -static void test_get_user_agent_clause(void) -{ - char expected_clause[] = "rcheevos/" RCHEEVOS_VERSION_STRING; - char buffer[64]; - - g_client = mock_client_not_logged_in(); - ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, sizeof(buffer)), sizeof(expected_clause) - 1); - ASSERT_STR_EQUALS(buffer, expected_clause); - - /* snprintf will return the number of characters it wants, even if the buffer is too small, - * but will only fill as much of the buffer is available */ - ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, 8), sizeof(expected_clause) - 1); - ASSERT_STR_EQUALS(buffer, "rcheevo"); - - rc_client_destroy(g_client); -} - -static void test_version(void) -{ - ASSERT_STR_EQUALS(rc_version_string(), RCHEEVOS_VERSION_STRING); - ASSERT_NUM_EQUALS(rc_version(), RCHEEVOS_VERSION); -} - -/* ----- harness ----- */ - -void test_client(void) { - TEST_SUITE_BEGIN(); - - /* login */ - TEST(test_login_with_password); - TEST(test_login_with_token); - TEST(test_login_required_fields); - TEST(test_login_with_incorrect_password); - TEST(test_login_with_incorrect_token); - TEST(test_login_with_expired_token); - TEST(test_login_with_banned_account); - TEST(test_login_incomplete_response); - TEST(test_login_with_password_async); - TEST(test_login_with_password_async_aborted); - TEST(test_login_with_password_async_destroyed); - TEST(test_login_with_password_client_error); - - /* logout */ - TEST(test_logout); - TEST(test_logout_with_game_loaded); - TEST(test_logout_during_login); - TEST(test_logout_during_fetch_game); - - /* user */ - TEST(test_user_get_image_url); - - TEST(test_get_user_game_summary); - TEST(test_get_user_game_summary_softcore); - TEST(test_get_user_game_summary_encore_mode); - TEST(test_get_user_game_summary_with_unsupported_and_unofficial); - TEST(test_get_user_game_summary_with_unsupported_unlocks); - TEST(test_get_user_game_summary_with_unofficial_off); - TEST(test_get_user_game_summary_no_achievements); - TEST(test_get_user_game_summary_unknown_game); - TEST(test_get_user_game_summary_progress_incomplete); - TEST(test_get_user_game_summary_progress_progression_no_win); - TEST(test_get_user_game_summary_progress_win_only); - TEST(test_get_user_game_summary_beat); - TEST(test_get_user_game_summary_mastery); - - /* load game */ - TEST(test_load_game_required_fields); - TEST(test_load_game_unknown_hash); - TEST(test_load_game_unknown_hash_repeated); - TEST(test_load_game_unknown_hash_multiple); - TEST(test_load_game_not_logged_in); - TEST(test_load_game); - TEST(test_load_game_async_load_different_game); - TEST(test_load_game_async_login); - TEST(test_load_game_async_login_with_incorrect_password); - TEST(test_load_game_async_login_logout); - TEST(test_load_game_async_login_aborted); - TEST(test_load_game_patch_failure); - TEST(test_load_game_startsession_failure); - TEST(test_load_game_startsession_timeout); - TEST(test_load_game_startsession_custom_timeout); - TEST(test_load_game_patch_aborted); - TEST(test_load_game_startsession_aborted); - TEST(test_load_game_while_spectating); - TEST(test_load_game_process_game_sets); - TEST(test_load_game_destroy_while_fetching_game_data); - TEST(test_load_unknown_game); - TEST(test_load_unknown_game_multihash); - TEST(test_load_game_dispatched_read_memory); - - /* unload game */ - TEST(test_unload_game); - TEST(test_unload_game_hides_ui); - TEST(test_unload_game_while_fetching_game_data); - TEST(test_unload_game_while_starting_session); - -#ifdef RC_CLIENT_SUPPORTS_HASH - /* identify and load game */ - TEST(test_identify_and_load_game_required_fields); - TEST(test_identify_and_load_game_console_specified); - TEST(test_identify_and_load_game_console_not_specified); - #ifndef RC_HASH_NO_ROM - TEST(test_identify_and_load_game_multiconsole_first); - TEST(test_identify_and_load_game_multiconsole_second); - #endif - #ifndef RC_HASH_NO_DISC - TEST(test_identify_and_load_game_from_disc); - #endif - TEST(test_identify_and_load_game_unknown_hash); - TEST(test_identify_and_load_game_unknown_hash_repeated); - TEST(test_identify_and_load_game_unknown_hash_multiple); - #ifndef RC_HASH_NO_ROM - TEST(test_identify_and_load_game_unknown_hash_multiconsole); - #endif - TEST(test_identify_and_load_game_unknown_hash_console_specified); - TEST(test_identify_and_load_game_unknown_hash_client_provided); - TEST(test_identify_and_load_game_multihash); - TEST(test_identify_and_load_game_multihash_unknown_game); - TEST(test_identify_and_load_game_multihash_differ); - - /* change media */ - TEST(test_change_media_required_fields); - TEST(test_change_media_no_game_loaded); - TEST(test_change_media_same_game); - TEST(test_change_media_known_game); - TEST(test_change_media_unknown_game); - TEST(test_change_media_unhashable); - TEST(test_change_media_unhashable_without_generation); - TEST(test_change_media_back_and_forth); - TEST(test_change_media_while_loading); - TEST(test_change_media_while_loading_later); - TEST(test_change_media_async_aborted); - TEST(test_change_media_client_error); -#endif - - TEST(test_change_media_from_hash_required_fields); - TEST(test_change_media_from_hash_no_game_loaded); - TEST(test_change_media_from_hash_same_game); - TEST(test_change_media_from_hash_known_game); - TEST(test_change_media_from_hash_unknown_game); - TEST(test_change_media_from_hash_back_and_forth); - TEST(test_change_media_from_hash_while_loading); - TEST(test_change_media_from_hash_while_loading_later); - TEST(test_change_media_from_hash_async_aborted); - TEST(test_change_media_from_hash_client_error); - - /* game */ - TEST(test_game_get_image_url); - - /* hash library */ - TEST(test_fetch_hash_library); - - /* game titles */ - TEST(test_fetch_game_titles); - - /* all user progress */ - TEST(test_fetch_all_user_progress); - - /* subset */ - TEST(test_load_subset); - TEST(test_subset_list); - - /* achievements */ - TEST(test_achievement_list_simple); - TEST(test_achievement_list_simple_with_unlocks); - TEST(test_achievement_list_simple_with_unlocks_encore_mode); - TEST(test_achievement_list_simple_with_unofficial_and_unsupported); - TEST(test_achievement_list_simple_with_unofficial_off); - TEST(test_achievement_list_buckets); - TEST(test_achievement_list_buckets_progress_sort); - TEST(test_achievement_list_buckets_progress_sort_big_ids); - TEST(test_achievement_list_buckets_with_unsynced); - TEST(test_achievement_list_subset_with_unofficial_and_unsupported); - TEST(test_achievement_list_subset_buckets); - - TEST(test_achievement_get_image_url); - - /* leaderboards */ - TEST(test_leaderboard_list_simple); - TEST(test_leaderboard_list_simple_with_unsupported); - TEST(test_leaderboard_list_buckets); - TEST(test_leaderboard_list_buckets_with_unsupported); - TEST(test_leaderboard_list_subset); - TEST(test_leaderboard_list_hidden); - - TEST(test_fetch_leaderboard_entries); - TEST(test_fetch_leaderboard_entries_no_user); - TEST(test_fetch_leaderboard_entries_around_user); - TEST(test_fetch_leaderboard_entries_around_user_not_logged_in); - TEST(test_fetch_leaderboard_entries_async_aborted); - TEST(test_fetch_leaderboard_entries_client_error); - - TEST(test_map_leaderboard_format); - - /* do frame */ - TEST(test_do_frame_bounds_check_system); - TEST(test_do_frame_bounds_check_available); - TEST(test_do_frame_achievement_trigger); - TEST(test_do_frame_achievement_trigger_already_awarded); - TEST(test_do_frame_achievement_trigger_server_error); - TEST(test_do_frame_achievement_trigger_while_spectating); - TEST(test_do_frame_achievement_trigger_blocked); - TEST(test_do_frame_achievement_trigger_automatic_retry_empty); - TEST(test_do_frame_achievement_trigger_automatic_retry_429); - TEST(test_do_frame_achievement_trigger_automatic_retry_502); - TEST(test_do_frame_achievement_trigger_automatic_retry_503); - TEST(test_do_frame_achievement_trigger_automatic_retry_custom_timeout); - TEST(test_do_frame_achievement_trigger_automatic_retry_generic_empty_response); - TEST(test_do_frame_achievement_trigger_subset); - TEST(test_do_frame_achievement_trigger_rarity); - TEST(test_do_frame_achievement_measured); - TEST(test_do_frame_achievement_measured_progress_event); - TEST(test_do_frame_achievement_measured_progress_reshown); - TEST(test_do_frame_achievement_challenge_indicator); - TEST(test_do_frame_achievement_challenge_indicator_primed_while_reset); - TEST(test_do_frame_achievement_challenge_indicator_primed_while_reset_next); - TEST(test_do_frame_mastery); - TEST(test_do_frame_mastery_encore); - TEST(test_do_frame_mastery_subset); - TEST(test_do_frame_leaderboard_started); - TEST(test_do_frame_leaderboard_update); - TEST(test_do_frame_leaderboard_failed); - TEST(test_do_frame_leaderboard_submit); - TEST(test_do_frame_leaderboard_submit_server_error); - TEST(test_do_frame_leaderboard_submit_while_spectating); - TEST(test_do_frame_leaderboard_submit_immediate); - TEST(test_do_frame_leaderboard_submit_hidden); - TEST(test_do_frame_leaderboard_submit_blocked); - TEST(test_do_frame_leaderboard_submit_softcore); - TEST(test_do_frame_leaderboard_tracker_sharing); - TEST(test_do_frame_leaderboard_tracker_sharing_hits); - TEST(test_do_frame_leaderboard_submit_automatic_retry); - TEST(test_do_frame_multiple_automatic_retry); - TEST(test_do_frame_rich_presence_hitcount); - - TEST(test_clock_get_now_millisecs); - - /* ping */ - TEST(test_idle_ping); - TEST(test_do_frame_ping_rich_presence); - TEST(test_do_frame_ping_rich_presence_override_allowed); - TEST(test_do_frame_ping_rich_presence_override_replaced); - TEST(test_idle_ping_while_not_running); - - /* reset */ - TEST(test_reset_hides_widgets); - TEST(test_reset_detaches_hide_progress_indicator_event); - - /* pause */ - TEST(test_can_pause); - - /* deserialize_progress */ - TEST(test_deserialize_progress_updates_widgets); - TEST(test_deserialize_progress_null); - TEST(test_deserialize_progress_invalid); - TEST(test_deserialize_progress_sized); - TEST(test_deserialize_progress_unknown_game); - - /* processing required */ - TEST(test_processing_required); - TEST(test_processing_required_empty_game); - TEST(test_processing_required_rich_presence_only); - TEST(test_processing_required_leaderboard_only); - TEST(test_processing_required_after_mastery); - TEST(test_processing_required_after_mastery_no_leaderboards); - - /* settings */ - TEST(test_set_hardcore_disable); - TEST(test_set_hardcore_disable_active_tracker); - TEST(test_set_hardcore_enable); - TEST(test_set_hardcore_enable_no_game_loaded); - TEST(test_set_hardcore_enable_encore_mode); - TEST(test_set_encore_mode_enable); - TEST(test_set_encore_mode_disable); - - TEST(test_get_user_agent_clause); - TEST(test_version); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/test_rc_client_external.c b/src/rcheevos/test/test_rc_client_external.c deleted file mode 100644 index 497b22fc0d..0000000000 --- a/src/rcheevos/test/test_rc_client_external.c +++ /dev/null @@ -1,2170 +0,0 @@ -#include "rc_client.h" - -#include "../src/rc_client_external_versions.h" -#include "../src/rc_client_internal.h" -#include "../src/rc_version.h" -#include "rc_consoles.h" -#include "rhash/data.h" - -#include "test_framework.h" - -#ifdef RC_CLIENT_SUPPORTS_EXTERNAL - -static rc_client_t* g_client; -static const char* g_external_event; -static int g_external_int = 0; -static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ - -/* begin from test_rc_client.c */ - -extern void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); -extern void reset_mock_api_handlers(void); -extern void mock_api_response(const char* request_params, const char* response_body); -extern void mock_api_error(const char* request_params, const char* response_body, int http_status_code); - -/* end from test_rc_client.c */ - -#define RC_OFFSETOF(s,f) ((uint32_t)((uint8_t*)&(((s*)(0))->f) - ((uint8_t*)0))) - -#define ASSERT_FIELD_OFFSET(struct1, struct2, field) { \ - const uint32_t __o1 = RC_OFFSETOF(struct1, field); \ - const uint32_t __o2 = RC_OFFSETOF(struct2, field); \ - if (__o1 != __o2) { \ - ASSERT_FAIL("Expected: " #struct1 "." #field " at offset %u (%s:%d)\n Found: " #struct2 "." #field " at offset %u", \ - __o1, test_framework_basename(__FILE__), __LINE__, __o2); \ - } \ -} - -static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) -{ - return 0; -} - -static rc_client_t* mock_client_with_external() -{ - rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); - client->state.external_client = (rc_client_external_t*) - rc_buffer_alloc(&client->state.buffer, sizeof(*client->state.external_client)); - memset(client->state.external_client, 0, sizeof(*client->state.external_client)); - - rc_api_set_host(NULL); - reset_mock_api_handlers(); - g_external_event = "none"; - g_external_int = 0; - - return client; -} - -static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_OK); - ASSERT_PTR_NULL(error_message); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_external_unload_game(void) -{ - g_external_event = "unload_game"; -} - -/* ----- settings ----- */ - -static int rc_client_external_get_int(void) -{ - return g_external_int; -} - -static void rc_client_external_set_int(int value) -{ - g_external_int = value; -} - -static void test_hardcore_enabled(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->get_hardcore_enabled = rc_client_external_get_int; - g_client->state.external_client->set_hardcore_enabled = rc_client_external_set_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); - - rc_client_set_hardcore_enabled(g_client, 0); - ASSERT_NUM_EQUALS(g_external_int, 0); - - rc_client_set_hardcore_enabled(g_client, 1); - ASSERT_NUM_EQUALS(g_external_int, 1); - - rc_client_destroy(g_client); -} - -static void test_unofficial_enabled(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->get_unofficial_enabled = rc_client_external_get_int; - g_client->state.external_client->set_unofficial_enabled = rc_client_external_set_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_get_unofficial_enabled(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_get_unofficial_enabled(g_client), 1); - - rc_client_set_unofficial_enabled(g_client, 0); - ASSERT_NUM_EQUALS(g_external_int, 0); - - rc_client_set_unofficial_enabled(g_client, 1); - ASSERT_NUM_EQUALS(g_external_int, 1); - - rc_client_destroy(g_client); -} - -static void test_encore_mode_enabled(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->get_encore_mode_enabled = rc_client_external_get_int; - g_client->state.external_client->set_encore_mode_enabled = rc_client_external_set_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); - - rc_client_set_encore_mode_enabled(g_client, 0); - ASSERT_NUM_EQUALS(g_external_int, 0); - - rc_client_set_encore_mode_enabled(g_client, 1); - ASSERT_NUM_EQUALS(g_external_int, 1); - - rc_client_destroy(g_client); -} - -static void test_spectator_mode_enabled(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->get_spectator_mode_enabled = rc_client_external_get_int; - g_client->state.external_client->set_spectator_mode_enabled = rc_client_external_set_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_get_spectator_mode_enabled(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_get_spectator_mode_enabled(g_client), 1); - - rc_client_set_spectator_mode_enabled(g_client, 0); - ASSERT_NUM_EQUALS(g_external_int, 0); - - rc_client_set_spectator_mode_enabled(g_client, 1); - ASSERT_NUM_EQUALS(g_external_int, 1); - - rc_client_destroy(g_client); -} - -static void rc_client_external_log_message(const char* message, const rc_client_t* client) -{ -} - -static void rc_client_external_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) -{ - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_NUM_EQUALS(level, RC_CLIENT_LOG_LEVEL_INFO); - ASSERT_PTR_EQUALS(callback, rc_client_external_log_message); - - g_external_event = "enable_logging"; -} - -static void test_enable_logging(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->enable_logging = rc_client_external_enable_logging; - - rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_INFO, rc_client_external_log_message); - - ASSERT_STR_EQUALS(g_external_event, "enable_logging"); - - rc_client_destroy(g_client); -} - -static void rc_client_external_event_handler(const rc_client_event_t* event, rc_client_t* client) -{ -} - -static void rc_client_external_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) -{ - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(handler, rc_client_external_event_handler); - - g_external_event = "event_handler"; -} - -static void test_event_handler(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->set_event_handler = rc_client_external_set_event_handler; - - rc_client_set_event_handler(g_client, rc_client_external_event_handler); - - ASSERT_STR_EQUALS(g_external_event, "event_handler"); - - rc_client_destroy(g_client); -} - -static uint32_t rc_client_external_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) -{ - return 0; -} - -static void rc_client_external_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) -{ - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(handler, rc_client_external_read_memory); - - g_external_event = "read_memory"; -} - -static void test_read_memory(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->set_read_memory = rc_client_external_set_read_memory_function; - - rc_client_set_read_memory_function(g_client, rc_client_external_read_memory); - - ASSERT_STR_EQUALS(g_external_event, "read_memory"); - - rc_client_destroy(g_client); -} - -static rc_clock_t rc_client_external_now_millisecs(const rc_client_t* client) -{ - return (rc_clock_t)12345678; -} - -static void rc_client_external_set_get_time_millisecs(rc_client_t* client, rc_get_time_millisecs_func_t handler) -{ - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(handler, rc_client_external_now_millisecs); - - g_external_event = "set_milli"; -} - -static void test_get_time_millisecs(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->set_get_time_millisecs = rc_client_external_set_get_time_millisecs; - - rc_client_set_get_time_millisecs_function(g_client, rc_client_external_now_millisecs); - - ASSERT_STR_EQUALS(g_external_event, "set_milli"); - - rc_client_destroy(g_client); -} - -static void rc_client_external_set_host(const char* hostname) -{ - ASSERT_STR_EQUALS(hostname, "localhost"); - - g_external_event = "set_host"; -} - -static void test_set_host(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->set_host = rc_client_external_set_host; - - rc_client_set_host(g_client, "localhost"); - - ASSERT_STR_EQUALS(g_external_event, "set_host"); - - rc_api_set_host(NULL); - rc_api_set_image_host(NULL); - - rc_client_destroy(g_client); -} - -static size_t rc_client_external_get_user_agent_clause(char buffer[], size_t buffer_size) -{ - return snprintf(buffer, buffer_size, "external/2.1"); -} - -static void test_get_user_agent_clause(void) -{ - char expected_clause[] = "external/2.1 rc_client/" RCHEEVOS_VERSION_STRING; - char buffer[64]; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_user_agent_clause = rc_client_external_get_user_agent_clause; - - ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, sizeof(buffer)), sizeof(expected_clause) - 1); - ASSERT_STR_EQUALS(buffer, expected_clause); - - /* snprintf will return the number of characters it wants, even if the buffer is too small, - * but will only fill as much of the buffer is available */ - ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, 8), sizeof(expected_clause) - 1); - ASSERT_STR_EQUALS(buffer, "externa"); - - ASSERT_NUM_EQUALS(rc_client_get_user_agent_clause(g_client, buffer, 20), sizeof(expected_clause) - 1); - ASSERT_STR_EQUALS(buffer, "external/2.1 rc_cli"); - - rc_client_destroy(g_client); -} - -static void rc_client_external_add_game_hash(const char* hash, uint32_t game_id) -{ - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_NUM_EQUALS(game_id, 1234); - - g_external_event = "add_game_hash"; - g_external_int = game_id; -} - -static void test_add_game_hash(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->add_game_hash = rc_client_external_add_game_hash; - - rc_client_add_game_hash(g_client, "6a2305a2b6675a97ff792709be1ca857", 1234); - - /* hash should be loaded in both local client and external client */ - ASSERT_PTR_NOT_NULL(g_client->hashes); - ASSERT_STR_EQUALS(g_client->hashes->hash, "6a2305a2b6675a97ff792709be1ca857"); - ASSERT_NUM_EQUALS(g_client->hashes->game_id, 1234); - - ASSERT_STR_EQUALS(g_external_event, "add_game_hash"); - - rc_client_destroy(g_client); -} - -static void test_set_allow_background_memory_reads(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->set_allow_background_memory_reads = rc_client_external_set_int; - - rc_client_set_allow_background_memory_reads(g_client, 0); - ASSERT_NUM_EQUALS(g_external_int, 0); - - rc_client_set_allow_background_memory_reads(g_client, 1); - ASSERT_NUM_EQUALS(g_external_int, 1); - - rc_client_destroy(g_client); -} - -/* ----- login ----- */ - -static void test_v1_user_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, display_name); - ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, username); - ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, token); - ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, score); - ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, score_softcore); - ASSERT_FIELD_OFFSET(rc_client_user_t, v1_rc_client_user_t, num_unread_messages); -} - -static void test_v3_user_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, display_name); - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, username); - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, token); - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, score); - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, score_softcore); - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, num_unread_messages); - ASSERT_FIELD_OFFSET(rc_client_user_t, v3_rc_client_user_t, avatar_url); -} - -static void assert_login_with_password(rc_client_t* client, const char* username, const char* password) -{ - ASSERT_PTR_EQUALS(client, g_client); - - ASSERT_STR_EQUALS(username, "User"); - ASSERT_STR_EQUALS(password, "Pa$$word"); -} - -static rc_client_async_handle_t* rc_client_external_login_with_password(rc_client_t* client, - const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) -{ - assert_login_with_password(client, username, password); - - g_external_event = "login"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static const rc_client_user_t* rc_client_external_get_user_info_v1(void) -{ - v1_rc_client_user_t* user = (v1_rc_client_user_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_user_t)); - - memset(user, 0, sizeof(*user)); - user->display_name = "User"; - user->username = "User"; - user->token = "ApiToken"; - user->score = 12345; - user->score_softcore = 123; - user->num_unread_messages = 2; - - return (rc_client_user_t*)user; -} - -static const rc_client_user_t* rc_client_external_get_user_info_v3(void) -{ - v3_rc_client_user_t* user = (v3_rc_client_user_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_user_t)); - - memset(user, 0, sizeof(*user)); - user->display_name = "User"; - user->username = "User"; - user->token = "ApiToken"; - user->score = 12345; - user->score_softcore = 123; - user->num_unread_messages = 2; - user->avatar_url = "/UserPic/User.png"; - - return (rc_client_user_t*)user; -} - -static void test_login_with_password(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_login_with_password = rc_client_external_login_with_password; - g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1; - g_client->state.external_client->get_user_info_v3 = rc_client_external_get_user_info_v3; - - rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "login"); - - /* user data should come from external client. validate structure */ - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "User"); - ASSERT_STR_EQUALS(user->display_name, "User"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->score_softcore, 123); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - ASSERT_STR_EQUALS(user->avatar_url, "/UserPic/User.png"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->user.username); - - rc_client_destroy(g_client); -} - -static void assert_login_with_token(rc_client_t* client, const char* username, const char* token) -{ - ASSERT_PTR_EQUALS(client, g_client); - - ASSERT_STR_EQUALS(username, "User"); - ASSERT_STR_EQUALS(token, "ApiToken"); -} - -static rc_client_async_handle_t* rc_client_external_login_with_token(rc_client_t* client, - const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) -{ - assert_login_with_token(client, username, token); - - g_external_event = "login"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static void test_login_with_token_v1(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token; - g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1; - - rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "login"); - - /* user data should come from external client. validate structure */ - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "User"); - ASSERT_STR_EQUALS(user->display_name, "User"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->score_softcore, 123); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - ASSERT_STR_EQUALS(user->avatar_url, "https://media.retroachievements.org/UserPic/User.png"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->user.username); - - rc_client_destroy(g_client); -} - -static const rc_client_user_t* rc_client_external_get_user_info_v1_long_name(void) -{ - v1_rc_client_user_t* user = (v1_rc_client_user_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_user_t)); - - memset(user, 0, sizeof(*user)); - user->display_name = "TwentyCharUserNameXX"; - user->username = "TwentyCharUserNameXX"; - user->token = "ApiToken"; - user->score = 12345; - user->score_softcore = 123; - user->num_unread_messages = 2; - - return (rc_client_user_t*)user; -} - -static rc_client_async_handle_t* rc_client_external_login_with_token_long_name(rc_client_t* client, - const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) -{ - g_external_event = "login"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static void test_login_with_token_v1_long_username(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token_long_name; - g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1_long_name; - - rc_client_begin_login_with_token(g_client, "TwentyCharUserNameXX", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "login"); - - /* user data should come from external client. validate structure */ - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "TwentyCharUserNameXX"); - ASSERT_STR_EQUALS(user->display_name, "TwentyCharUserNameXX"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->score_softcore, 123); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - ASSERT_STR_EQUALS(user->avatar_url, "https://media.retroachievements.org/UserPic/TwentyCharUserNameXX.png"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->user.username); - - rc_client_destroy(g_client); -} - -static const rc_client_user_t* rc_client_external_get_user_info_v1_too_long_name(void) -{ - v1_rc_client_user_t* user = (v1_rc_client_user_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_user_t)); - - memset(user, 0, sizeof(*user)); - user->display_name = "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"; - user->username = "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"; - user->token = "ApiToken"; - user->score = 12345; - user->score_softcore = 123; - user->num_unread_messages = 2; - - return (rc_client_user_t*)user; -} - -static void test_login_with_token_v1_too_long_username(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token_long_name; - g_client->state.external_client->get_user_info = rc_client_external_get_user_info_v1_too_long_name; - - rc_client_begin_login_with_token(g_client, "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "login"); - - /* user data should come from external client. validate structure */ - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"); - ASSERT_STR_EQUALS(user->display_name, "ThisUserNameIsTooLongToFitIntoTheUserAvatarBufferWithoutOverflowing"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->score_softcore, 123); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - /* overly long URL will be truncated, but should not cause an exception. - * test_login_with_token_v1_long_username validates the longest allowed username, so this shouldn't occur anyway */ - ASSERT_STR_EQUALS(user->avatar_url, "https://media.retroachievements.org/UserPic/ThisUserNameIsTooLongToFitIntoTheUs"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->user.username); - - rc_client_destroy(g_client); -} - -static void test_login_with_token(void) -{ - const rc_client_user_t* user; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_login_with_token = rc_client_external_login_with_token; - g_client->state.external_client->get_user_info_v3 = rc_client_external_get_user_info_v3; - - rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "login"); - - /* user data should come from external client. validate structure */ - user = rc_client_get_user_info(g_client); - ASSERT_PTR_NOT_NULL(user); - ASSERT_STR_EQUALS(user->username, "User"); - ASSERT_STR_EQUALS(user->display_name, "User"); - ASSERT_STR_EQUALS(user->token, "ApiToken"); - ASSERT_NUM_EQUALS(user->score, 12345); - ASSERT_NUM_EQUALS(user->score_softcore, 123); - ASSERT_NUM_EQUALS(user->num_unread_messages, 2); - ASSERT_STR_EQUALS(user->avatar_url, "/UserPic/User.png"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->user.username); - - rc_client_destroy(g_client); -} - -static void rc_client_external_logout(void) -{ - g_external_event = "logout"; -} - -static void test_logout(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->logout = rc_client_external_logout; - - /* external client should maintain its own state, but use the singular instance*/ - g_client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; - - rc_client_logout(g_client); - ASSERT_STR_EQUALS(g_external_event, "logout"); - - /* ensure non-external client user was not modified */ - ASSERT_NUM_EQUALS(g_client->state.user, RC_CLIENT_USER_STATE_LOGGED_IN); - - rc_client_destroy(g_client); -} - -/* ----- load game ----- */ - -static void test_v1_game_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, id); - ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, console_id); - ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, title); - ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, hash); - ASSERT_FIELD_OFFSET(rc_client_game_t, v1_rc_client_game_t, badge_name); -} - -static void test_v3_game_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, id); - ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, console_id); - ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, title); - ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, hash); - ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, badge_name); - ASSERT_FIELD_OFFSET(rc_client_game_t, v3_rc_client_game_t, badge_url); -} - -static const rc_client_game_t* rc_client_external_get_game_info_v1(void) -{ - v1_rc_client_game_t* game = (v1_rc_client_game_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_game_t)); - - memset(game, 0, sizeof(*game)); - game->id = 1234; - game->console_id = RC_CONSOLE_PLAYSTATION; - game->title = "Game Title"; - game->hash = "GAME_HASH"; - game->badge_name = "BDG001"; - - return (const rc_client_game_t*)game; -} - -static const rc_client_game_t* rc_client_external_get_game_info_v1_not_found(void) -{ - return NULL; -} - -static const rc_client_game_t* rc_client_external_get_game_info_v3(void) -{ - v3_rc_client_game_t* game = (v3_rc_client_game_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_game_t)); - - memset(game, 0, sizeof(*game)); - game->id = 1234; - game->console_id = RC_CONSOLE_PLAYSTATION; - game->title = "Game Title"; - game->hash = "GAME_HASH"; - game->badge_name = "BDG001"; - game->badge_url = "/Badge/BDG001.png"; - - return (const rc_client_game_t*)game; -} - -#ifdef RC_CLIENT_SUPPORTS_HASH - -static void assert_identify_and_load_game(rc_client_t* client, - uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size) -{ - ASSERT_PTR_EQUALS(client, g_client); - - ASSERT_NUM_EQUALS(console_id, RC_CONSOLE_GAMEBOY); - ASSERT_STR_EQUALS(file_path, "foo.zip#foo.gb"); - ASSERT_PTR_NOT_NULL(data); - ASSERT_NUM_EQUALS(32768, data_size); -} - -static rc_client_async_handle_t* rc_client_external_identify_and_load_game(rc_client_t* client, - uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size, - rc_client_callback_t callback, void* callback_userdata) -{ - assert_identify_and_load_game(client, console_id, file_path, data, data_size); - - g_external_event = "load_game"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static void test_identify_and_load_game_v1(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_identify_and_load_game = rc_client_external_identify_and_load_game; - g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "https://media.retroachievements.org/Images/BDG001.png"); - - /* ensure non-external client game was not initialized */ - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_load_game(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_identify_and_load_game = rc_client_external_identify_and_load_game; - g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; - g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - /* ensure non-external client game was not initialized */ - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_reload_game(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_identify_and_load_game = rc_client_external_identify_and_load_game; - g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; - g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; - g_client->state.external_client->unload_game = rc_client_external_unload_game; - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - /* ensure non-external client game was not initialized */ - ASSERT_PTR_NULL(g_client->game); - - rc_client_unload_game(g_client); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - /* ensure non-external client game was not initialized */ - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -#endif /* RC_CLIENT_SUPPORTS_HASH */ - -static void assert_load_game(rc_client_t* client, const char* hash) -{ - ASSERT_PTR_EQUALS(client, g_client); - - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); -} - -static rc_client_async_handle_t* rc_client_external_load_game(rc_client_t* client, - const char* hash, rc_client_callback_t callback, void* callback_userdata) -{ - assert_load_game(client, hash); - - g_external_event = "load_game"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static void test_load_game_v1(void) -{ - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_load_game = rc_client_external_load_game; - g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; - - rc_client_begin_load_game(g_client, "6a2305a2b6675a97ff792709be1ca857", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); - - /* game data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "https://media.retroachievements.org/Images/BDG001.png"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_load_game(void) -{ - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_load_game = rc_client_external_load_game; - g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1; - g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; - - rc_client_begin_load_game(g_client, "6a2305a2b6675a97ff792709be1ca857", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); - - /* game data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - - /* ensure non-external client user was not initialized */ - ASSERT_PTR_NULL(g_client->game); - - rc_client_destroy(g_client); -} - -static void test_get_game_info_v1_no_game_loaded(void) -{ - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_game_info = rc_client_external_get_game_info_v1_not_found; - - /* game data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NULL(game); - - rc_client_destroy(g_client); -} - -static void test_v1_user_game_summary_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_core_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_unofficial_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_unlocked_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, num_unsupported_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, points_core); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v1_rc_client_user_game_summary_t, points_unlocked); -} - -static void test_v5_user_game_summary_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_core_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_unofficial_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_unlocked_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, num_unsupported_achievements); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, points_core); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, points_unlocked); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, beaten_time); - ASSERT_FIELD_OFFSET(rc_client_user_game_summary_t, v5_rc_client_user_game_summary_t, completed_time); -} - -static void rc_client_external_get_user_game_summary(rc_client_user_game_summary_t* summary) -{ - summary->num_core_achievements = 20; - summary->num_unlocked_achievements = 6; - summary->num_unofficial_achievements = 3; - summary->num_unsupported_achievements = 1; - summary->points_core = 100; - summary->points_unlocked = 23; -} - -static void rc_client_external_get_user_game_summary_v5(rc_client_user_game_summary_t* summary) -{ - summary->num_core_achievements = 20; - summary->num_unlocked_achievements = 6; - summary->num_unofficial_achievements = 3; - summary->num_unsupported_achievements = 1; - summary->points_core = 100; - summary->points_unlocked = 23; - summary->beaten_time = 1234567890; - summary->completed_time = 1234598760; -} - -static void test_get_user_game_summary(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_user_game_summary = rc_client_external_get_user_game_summary; - - rc_client_get_user_game_summary(g_client, &summary); - - ASSERT_NUM_EQUALS(summary.num_core_achievements, 20); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 6); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 3); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); - ASSERT_NUM_EQUALS(summary.points_core, 100); - ASSERT_NUM_EQUALS(summary.points_unlocked, 23); - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_destroy(g_client); -} - -static void test_get_user_game_summary_v5(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_user_game_summary_v5 = rc_client_external_get_user_game_summary_v5; - - rc_client_get_user_game_summary(g_client, &summary); - - ASSERT_NUM_EQUALS(summary.num_core_achievements, 20); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 6); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 3); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); - ASSERT_NUM_EQUALS(summary.points_core, 100); - ASSERT_NUM_EQUALS(summary.points_unlocked, 23); - ASSERT_NUM_EQUALS(summary.beaten_time, 1234567890); - ASSERT_NUM_EQUALS(summary.completed_time, 1234598760); - - rc_client_destroy(g_client); -} - -static void rc_client_external_get_user_subset_summary(uint32_t subset_id, rc_client_user_game_summary_t* summary) -{ - if (subset_id == 6) { - summary->num_core_achievements = 20; - summary->num_unlocked_achievements = 6; - summary->num_unofficial_achievements = 3; - summary->num_unsupported_achievements = 1; - summary->points_core = 100; - summary->points_unlocked = 23; - summary->beaten_time = 1234567890; - summary->completed_time = 1234598760; - } -} - -static void test_get_user_subset_summary(void) -{ - rc_client_user_game_summary_t summary; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_user_subset_summary = rc_client_external_get_user_subset_summary; - - rc_client_get_user_subset_summary(g_client, 1, &summary); - - ASSERT_NUM_EQUALS(summary.num_core_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); - ASSERT_NUM_EQUALS(summary.points_core, 0); - ASSERT_NUM_EQUALS(summary.points_unlocked, 0); - ASSERT_NUM_EQUALS(summary.beaten_time, 0); - ASSERT_NUM_EQUALS(summary.completed_time, 0); - - rc_client_get_user_subset_summary(g_client, 6, &summary); - - ASSERT_NUM_EQUALS(summary.num_core_achievements, 20); - ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 6); - ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 3); - ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); - ASSERT_NUM_EQUALS(summary.points_core, 100); - ASSERT_NUM_EQUALS(summary.points_unlocked, 23); - ASSERT_NUM_EQUALS(summary.beaten_time, 1234567890); - ASSERT_NUM_EQUALS(summary.completed_time, 1234598760); - - rc_client_destroy(g_client); -} - -#ifdef RC_CLIENT_SUPPORTS_HASH - -static void test_identify_and_load_game_external_hash(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->add_game_hash = rc_client_external_add_game_hash; - g_client->state.external_client->begin_load_game = rc_client_external_load_game; - g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - g_external_int = 0; - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); /* begin_load_game called */ - ASSERT_NUM_EQUALS(g_external_int, 1234); /* add_game_hash called */ - ASSERT_PTR_NULL(g_client->state.load); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - - /* ensure internal client game was initialized to hold media hashes */ - ASSERT_PTR_NOT_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void test_identify_and_reload_game_external_hash(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - const rc_client_game_t* game; - - g_client = mock_client_with_external(); - g_client->state.external_client->add_game_hash = rc_client_external_add_game_hash; - g_client->state.external_client->begin_load_game = rc_client_external_load_game; - g_client->state.external_client->get_game_info_v3 = rc_client_external_get_game_info_v3; - g_client->state.external_client->unload_game = rc_client_external_unload_game; - - mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); - g_external_int = 0; - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); /* begin_load_game called */ - ASSERT_NUM_EQUALS(g_external_int, 1234); /* add_game_hash called */ - ASSERT_PTR_NULL(g_client->state.load); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - - /* ensure internal client game was initialized to hold media hashes */ - ASSERT_PTR_NOT_NULL(g_client->game); - - rc_client_unload_game(g_client); - - rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", - image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "load_game"); /* begin_load_game called */ - ASSERT_NUM_EQUALS(g_external_int, 1234); /* add_game_hash called */ - ASSERT_PTR_NULL(g_client->state.load); - - /* user data should come from external client. validate structure */ - game = rc_client_get_game_info(g_client); - ASSERT_PTR_NOT_NULL(game); - ASSERT_NUM_EQUALS(game->id, 1234); - ASSERT_NUM_EQUALS(game->console_id, RC_CONSOLE_PLAYSTATION); - ASSERT_STR_EQUALS(game->title, "Game Title"); - ASSERT_STR_EQUALS(game->hash, "GAME_HASH"); - ASSERT_STR_EQUALS(game->badge_name, "BDG001"); - ASSERT_STR_EQUALS(game->badge_url, "/Badge/BDG001.png"); - - /* ensure internal client game was initialized to hold media hashes */ - ASSERT_PTR_NOT_NULL(g_client->game); - - rc_client_destroy(g_client); - free(image); -} - -static void assert_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size) -{ - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_STR_EQUALS(file_path, "foo.zip#foo.gb"); - ASSERT_PTR_NOT_NULL(data); - ASSERT_NUM_EQUALS(data_size, 32768); -} - -static rc_client_async_handle_t* rc_client_external_begin_identify_and_change_media(rc_client_t* client, const char* file_path, - const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) -{ - assert_change_media(client, file_path, data, data_size); - - g_external_event = "change_media"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static void test_change_media(void) -{ - const size_t image_size = 32768; - uint8_t* image = generate_generic_file(image_size); - - g_client = mock_client_with_external(); - g_client->state.external_client->begin_identify_and_change_media = rc_client_external_begin_identify_and_change_media; - - rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "change_media"); - - rc_client_destroy(g_client); - free(image); -} - -#endif - -static void assert_change_media_from_hash(rc_client_t* client, const char* hash) -{ - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_STR_EQUALS(hash, "6a2305a2b6675a97ff792709be1ca857"); -} - -static rc_client_async_handle_t* rc_client_external_begin_change_media(rc_client_t* client, const char* hash, - rc_client_callback_t callback, void* callback_userdata) -{ - assert_change_media_from_hash(client, hash); - - g_external_event = "change_media_from_hash"; - - callback(RC_OK, NULL, client, callback_userdata); - return NULL; -} - -static void test_change_media_from_hash(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->begin_change_media = rc_client_external_begin_change_media; - - rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_external_event, "change_media_from_hash"); - - rc_client_destroy(g_client); -} - -static const rc_client_subset_t* rc_client_external_get_subset_info_v1(uint32_t subset_id) -{ - v1_rc_client_subset_t* subset = (v1_rc_client_subset_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_subset_t)); - - memset(subset, 0, sizeof(*subset)); - subset->id = subset_id; - subset->title = "Subset Title"; - snprintf(subset->badge_name, sizeof(subset->badge_name), "%s", "BDG001"); - subset->num_achievements = 2; - subset->num_leaderboards = 1; - - return (const rc_client_subset_t*)subset; -} - -static const rc_client_subset_t* rc_client_external_get_subset_info_v3(uint32_t subset_id) -{ - v3_rc_client_subset_t* subset = (v3_rc_client_subset_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_subset_t)); - - memset(subset, 0, sizeof(*subset)); - subset->id = subset_id; - subset->title = "Subset Title"; - snprintf(subset->badge_name, sizeof(subset->badge_name), "%s", "BDG001"); - subset->num_achievements = 2; - subset->num_leaderboards = 1; - subset->badge_url = "/Badge/BDG001.png"; - - return (const rc_client_subset_t*)subset; -} - -static void test_v1_subset_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, id); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, title); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, badge_name); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, num_achievements); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v1_rc_client_subset_t, num_leaderboards); -} - -static void test_v3_subset_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, id); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, title); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, badge_name); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, num_achievements); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, num_leaderboards); - ASSERT_FIELD_OFFSET(rc_client_subset_t, v3_rc_client_subset_t, badge_url); -} - -static void test_get_subset_info_v1(void) -{ - const rc_client_subset_t* subset; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_subset_info = rc_client_external_get_subset_info_v1; - - /* subset data should come from external client. validate structure */ - subset = rc_client_get_subset_info(g_client, 1234); - ASSERT_PTR_NOT_NULL(subset); - ASSERT_NUM_EQUALS(subset->id, 1234); - ASSERT_STR_EQUALS(subset->title, "Subset Title"); - ASSERT_STR_EQUALS(subset->badge_name, "BDG001"); - ASSERT_NUM_EQUALS(subset->num_achievements, 2); - ASSERT_NUM_EQUALS(subset->num_leaderboards, 1); - ASSERT_STR_EQUALS(subset->badge_url, "https://media.retroachievements.org/Images/BDG001.png"); - - rc_client_destroy(g_client); -} - -static void test_get_subset_info(void) -{ - const rc_client_subset_t* subset; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_subset_info_v3 = rc_client_external_get_subset_info_v3; - - /* subset data should come from external client. validate structure */ - subset = rc_client_get_subset_info(g_client, 1234); - ASSERT_PTR_NOT_NULL(subset); - ASSERT_NUM_EQUALS(subset->id, 1234); - ASSERT_STR_EQUALS(subset->title, "Subset Title"); - ASSERT_STR_EQUALS(subset->badge_name, "BDG001"); - ASSERT_NUM_EQUALS(subset->num_achievements, 2); - ASSERT_NUM_EQUALS(subset->num_leaderboards, 1); - ASSERT_STR_EQUALS(subset->badge_url, "/Badge/BDG001.png"); - - rc_client_destroy(g_client); -} - -static void rc_client_external_destroy_subset_list(rc_client_subset_list_info_t* list) -{ - g_external_event = "destroyed"; - free(list); -} - -static rc_client_subset_list_info_t* rc_client_external_create_subset_list_v6() -{ - rc_client_subset_list_info_t* list; - - list = (rc_client_subset_list_info_t*)calloc(1, sizeof(*list) + sizeof(rc_client_subset_t*) * 2); - if (list) { - const rc_client_subset_t** subset; - rc_client_subset_t* mutable_subset; - list->public_.num_subsets = 2; - list->public_.subsets = subset = (const rc_client_subset_t**)((uint8_t*)list + sizeof(*list)); - *subset++ = rc_client_external_get_subset_info_v3(1111); - *subset = rc_client_external_get_subset_info_v3(2345); - mutable_subset = (rc_client_subset_t*)*subset; - mutable_subset->title = "Bonus"; - mutable_subset->num_achievements = 1; - mutable_subset->num_leaderboards = 0; - - list->destroy_func = rc_client_external_destroy_subset_list; - } - - return list; -} - -static void test_create_subset_list(void) -{ - rc_client_subset_list_t* list; - - g_client = mock_client_with_external(); - g_client->state.external_client->create_subset_list = rc_client_external_create_subset_list_v6; - - list = rc_client_create_subset_list(g_client); - ASSERT_PTR_NOT_NULL(list); - ASSERT_NUM_EQUALS(list->num_subsets, 2); - ASSERT_PTR_NOT_NULL(list->subsets); - ASSERT_NUM_EQUALS(list->subsets[0]->id, 1111); - ASSERT_STR_EQUALS(list->subsets[0]->title, "Subset Title"); - ASSERT_STR_EQUALS(list->subsets[0]->badge_name, "BDG001"); - ASSERT_NUM_EQUALS(list->subsets[0]->num_achievements, 2); - ASSERT_NUM_EQUALS(list->subsets[0]->num_leaderboards, 1); - ASSERT_STR_EQUALS(list->subsets[0]->badge_url, "/Badge/BDG001.png"); - ASSERT_NUM_EQUALS(list->subsets[1]->id, 2345); - ASSERT_STR_EQUALS(list->subsets[1]->title, "Bonus"); - ASSERT_STR_EQUALS(list->subsets[1]->badge_name, "BDG001"); - ASSERT_NUM_EQUALS(list->subsets[1]->num_achievements, 1); - ASSERT_NUM_EQUALS(list->subsets[1]->num_leaderboards, 0); - ASSERT_STR_EQUALS(list->subsets[1]->badge_url, "/Badge/BDG001.png"); - - rc_client_destroy_subset_list(list); - - ASSERT_STR_EQUALS(g_external_event, "destroyed"); - - rc_client_destroy(g_client); -} - -static void test_unload_game(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->unload_game = rc_client_external_unload_game; - - rc_client_unload_game(g_client); - - ASSERT_STR_EQUALS(g_external_event, "unload_game"); - - rc_client_destroy(g_client); -} - -/* ----- achievements ----- */ - -static void test_v1_achievement_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, id); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, description); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, badge_name); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, measured_progress); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, measured_percent); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, id); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, points); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, unlock_time); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, state); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, category); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, bucket); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, unlocked); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, rarity); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, rarity_hardcore); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, type); -} - -static void test_v3_achievement_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, id); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, description); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_name); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, measured_progress); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, measured_percent); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, id); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, points); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, unlock_time); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, state); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, category); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, bucket); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, unlocked); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, rarity); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, rarity_hardcore); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, type); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_url); - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_locked_url); -} - -static const rc_client_achievement_t* rc_client_external_get_achievement_info_v1(uint32_t id) -{ - v1_rc_client_achievement_t* achievement = (v1_rc_client_achievement_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_achievement_t)); - - memset(achievement, 0, sizeof(*achievement)); - achievement->id = id; - achievement->title = "Achievement Title"; - achievement->description = "Do something cool"; - memcpy(achievement->badge_name, "BDG1234", 8); - achievement->measured_percent = 33.5; - achievement->points = 5; - achievement->state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - achievement->category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; - achievement->bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; - achievement->unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; - achievement->rarity = 75.0f; - achievement->rarity_hardcore = 66.66f; - achievement->type = RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE; - - return (const rc_client_achievement_t*)achievement; -} - -static const rc_client_achievement_t* rc_client_external_get_achievement_info_v1_not_found(uint32_t id) -{ - return NULL; -} - -static const rc_client_achievement_t* rc_client_external_get_achievement_info_v3(uint32_t id) -{ - v3_rc_client_achievement_t* achievement = (v3_rc_client_achievement_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v3_rc_client_achievement_t)); - - memset(achievement, 0, sizeof(*achievement)); - achievement->id = id; - achievement->title = "Achievement Title"; - achievement->description = "Do something cool"; - memcpy(achievement->badge_name, "BDG1234", 8); - achievement->measured_percent = 33.5; - achievement->points = 5; - achievement->state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - achievement->category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; - achievement->bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; - achievement->unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; - achievement->rarity = 75.0f; - achievement->rarity_hardcore = 66.66f; - achievement->type = RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE; - achievement->badge_url = "/Badge/000234.png"; - achievement->badge_locked_url = "/Badge/000234_locked.png"; - return (const rc_client_achievement_t*)achievement; -} - -static void test_has_achievements(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->has_achievements = rc_client_external_get_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_has_achievements(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_has_achievements(g_client), 1); - - rc_client_destroy(g_client); -} - -static void test_get_achievement_info_v1(void) -{ - const rc_client_achievement_t* achievement; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_achievement_info = rc_client_external_get_achievement_info_v1; - - achievement = rc_client_get_achievement_info(g_client, 4); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->id, 4); - ASSERT_STR_EQUALS(achievement->title, "Achievement Title"); - ASSERT_STR_EQUALS(achievement->description, "Do something cool"); - ASSERT_STR_EQUALS(achievement->badge_name, "BDG1234"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.5); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 75.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.66f); - ASSERT_NUM_EQUALS(achievement->type, RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_STR_EQUALS(achievement->badge_url, "https://media.retroachievements.org/Badge/BDG1234.png"); - ASSERT_STR_EQUALS(achievement->badge_locked_url, "https://media.retroachievements.org/Badge/BDG1234_lock.png"); - - rc_client_destroy(g_client); -} - -static void test_get_achievement_info_v1_not_found(void) -{ - const rc_client_achievement_t* achievement; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_achievement_info = rc_client_external_get_achievement_info_v1_not_found; - - achievement = rc_client_get_achievement_info(g_client, 4); - ASSERT_PTR_NULL(achievement); - - rc_client_destroy(g_client); -} - -static void test_get_achievement_info(void) -{ - const rc_client_achievement_t* achievement; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_achievement_info = rc_client_external_get_achievement_info_v1; - g_client->state.external_client->get_achievement_info_v3 = rc_client_external_get_achievement_info_v3; - - achievement = rc_client_get_achievement_info(g_client, 4); - ASSERT_PTR_NOT_NULL(achievement); - ASSERT_NUM_EQUALS(achievement->id, 4); - ASSERT_STR_EQUALS(achievement->title, "Achievement Title"); - ASSERT_STR_EQUALS(achievement->description, "Do something cool"); - ASSERT_STR_EQUALS(achievement->badge_name, "BDG1234"); - ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.5); - ASSERT_NUM_EQUALS(achievement->points, 5); - ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); - ASSERT_NUM_EQUALS(achievement->category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); - ASSERT_FLOAT_EQUALS(achievement->rarity, 75.0f); - ASSERT_FLOAT_EQUALS(achievement->rarity_hardcore, 66.66f); - ASSERT_NUM_EQUALS(achievement->type, RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE); - ASSERT_STR_EQUALS(achievement->badge_url, "/Badge/000234.png"); - ASSERT_STR_EQUALS(achievement->badge_locked_url, "/Badge/000234_locked.png"); - - rc_client_destroy(g_client); -} - -static void test_v1_achievement_list_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v1_rc_client_achievement_list_info_t, public_); - ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v1_rc_client_achievement_list_info_t, destroy_func); - - ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v1_rc_client_achievement_list_t, buckets); - ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v1_rc_client_achievement_list_t, num_buckets); - - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, achievements); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, num_achievements); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, label); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, subset_id); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v1_rc_client_achievement_bucket_t, bucket_type); -} - -static void test_v3_achievement_list_field_offsets(void) -{ - ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v3_rc_client_achievement_list_info_t, public_); - ASSERT_FIELD_OFFSET(rc_client_achievement_list_info_t, v3_rc_client_achievement_list_info_t, destroy_func); - - ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v3_rc_client_achievement_list_t, buckets); - ASSERT_FIELD_OFFSET(rc_client_achievement_list_t, v3_rc_client_achievement_list_t, num_buckets); - - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, achievements); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, num_achievements); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, label); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, subset_id); - ASSERT_FIELD_OFFSET(rc_client_achievement_bucket_t, v3_rc_client_achievement_bucket_t, bucket_type); -} - -static void assert_achievement_list_category_grouping(int category, int grouping) -{ - ASSERT_NUM_EQUALS(category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); - ASSERT_NUM_EQUALS(grouping, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); -} - -static void rc_client_external_destroy_achievement_list(rc_client_achievement_list_info_t* list) -{ - g_external_event = "destroyed"; - free(list); -} - -static rc_client_achievement_list_info_t* rc_client_external_create_achievement_list_v1(int category, int grouping) -{ - v1_rc_client_achievement_list_info_t* list; - - assert_achievement_list_category_grouping(category, grouping); - - list = (v1_rc_client_achievement_list_info_t*)calloc(1, sizeof(*list) + sizeof(v1_rc_client_achievement_bucket_t) + sizeof(v1_rc_client_achievement_t*) * 2); - if (list) { - list->public_.num_buckets = 1; - list->public_.buckets = (v1_rc_client_achievement_bucket_t*)((uint8_t*)list + sizeof(*list)); - list->public_.buckets[0].achievements = (v1_rc_client_achievement_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); - list->public_.buckets[0].achievements[0] = (v1_rc_client_achievement_t*)rc_client_external_get_achievement_info_v1(1234); - list->public_.buckets[0].achievements[1] = (v1_rc_client_achievement_t*)rc_client_external_get_achievement_info_v1(1235); - list->public_.buckets[0].num_achievements = 2; - list->public_.buckets[0].bucket_type = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; - list->public_.buckets[0].label = "Locked"; - list->public_.buckets[0].subset_id = 1234; - - list->destroy_func = rc_client_external_destroy_achievement_list; - } - - return (rc_client_achievement_list_info_t*)list; -} - -static rc_client_achievement_list_info_t* rc_client_external_create_achievement_list_v3(int category, int grouping) -{ - v3_rc_client_achievement_list_info_t* list; - - assert_achievement_list_category_grouping(category, grouping); - - list = (v3_rc_client_achievement_list_info_t*)calloc(1, sizeof(*list) + sizeof(v3_rc_client_achievement_bucket_t) + sizeof(v3_rc_client_achievement_t*) * 2); - if (list) { - v3_rc_client_achievement_bucket_t* bucket; - list->public_.num_buckets = 1; - list->public_.buckets = bucket = (v3_rc_client_achievement_bucket_t*)((uint8_t*)list + sizeof(*list)); - bucket->achievements = (const v3_rc_client_achievement_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); - bucket->achievements[0] = (const v3_rc_client_achievement_t*)rc_client_external_get_achievement_info_v3(1234); - bucket->achievements[1] = (const v3_rc_client_achievement_t*)rc_client_external_get_achievement_info_v3(1235); - bucket->num_achievements = 2; - bucket->bucket_type = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; - bucket->label = "Locked"; - bucket->subset_id = 1234; - - list->destroy_func = rc_client_external_destroy_achievement_list; - } - - return (rc_client_achievement_list_info_t*)list; -} - -static void test_create_achievement_list_v1(void) -{ - rc_client_achievement_list_t* list; - - g_client = mock_client_with_external(); - g_client->state.external_client->create_achievement_list = rc_client_external_create_achievement_list_v1; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_PTR_NOT_NULL(list->buckets); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 1234); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 1235); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - - ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_url, "https://media.retroachievements.org/Badge/BDG1234.png"); - ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_locked_url, "https://media.retroachievements.org/Badge/BDG1234_lock.png"); - - rc_client_destroy_achievement_list(list); - - ASSERT_STR_EQUALS(g_external_event, "destroyed"); - - rc_client_destroy(g_client); -} - -static void test_create_achievement_list(void) -{ - rc_client_achievement_list_t* list; - - g_client = mock_client_with_external(); - g_client->state.external_client->create_achievement_list = rc_client_external_create_achievement_list_v1; - g_client->state.external_client->create_achievement_list_v3 = rc_client_external_create_achievement_list_v3; - - list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - ASSERT_PTR_NOT_NULL(list); - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_PTR_NOT_NULL(list->buckets); - ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 1234); - ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 1235); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); - ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); - - // only difference between v1 and v3 create_achievement_list is the badge_url/badge_unlocked_url fields on each achievement - ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_url, "/Badge/000234.png"); - ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->badge_locked_url, "/Badge/000234_locked.png"); - - rc_client_destroy_achievement_list(list); - - ASSERT_STR_EQUALS(g_external_event, "destroyed"); - - rc_client_destroy(g_client); -} - -/* ----- leaderboards ----- */ - -typedef struct v1_rc_client_leaderboard_t { - const char* title; - const char* description; - const char* tracker_value; - uint32_t id; - uint8_t state; - uint8_t format; - uint8_t lower_is_better; -} v1_rc_client_leaderboard_t; - -static const rc_client_leaderboard_t* rc_client_external_get_leaderboard_info(uint32_t id) -{ - v1_rc_client_leaderboard_t* leaderboard = (v1_rc_client_leaderboard_t*) - rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_leaderboard_t)); - - memset(leaderboard, 0, sizeof(*leaderboard)); - leaderboard->id = 1234; - leaderboard->title = "Leaderboard Title"; - leaderboard->description = "Do something cool"; - leaderboard->tracker_value = "000250"; - leaderboard->state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; - leaderboard->format = RC_CLIENT_LEADERBOARD_FORMAT_SCORE; - leaderboard->lower_is_better = 1; - - return (const rc_client_leaderboard_t*)leaderboard; -} - -typedef struct v1_rc_client_leaderboard_bucket_t { - rc_client_leaderboard_t** leaderboards; - uint32_t num_leaderboards; - - const char* label; - uint32_t subset_id; - uint8_t bucket_type; -} v1_rc_client_leaderboard_bucket_t; - -typedef struct v1_rc_client_leaderboard_list_t { - v1_rc_client_leaderboard_bucket_t* buckets; - uint32_t num_buckets; -} v1_rc_client_leaderboard_list_t; - -typedef struct v1_rc_client_leaderboard_list_info_t { - v1_rc_client_leaderboard_list_t public_; - rc_client_destroy_leaderboard_list_func_t destroy_func; -} v1_rc_client_leaderboard_list_info_t; - -static void assert_leaderboard_list_grouping(int grouping) -{ - ASSERT_NUM_EQUALS(grouping, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); -} - -static void rc_client_external_destroy_leaderboard_list(rc_client_leaderboard_list_info_t* list) -{ - g_external_event = "destroyed"; - free(list); -} - -static rc_client_leaderboard_list_info_t* rc_client_external_create_leaderboard_list(int grouping) -{ - v1_rc_client_leaderboard_list_info_t* list; - - assert_leaderboard_list_grouping(grouping); - - list = (v1_rc_client_leaderboard_list_info_t*)calloc(1, sizeof(*list) + sizeof(v1_rc_client_leaderboard_bucket_t)); - if (list) { - list->public_.num_buckets = 1; - list->public_.buckets = (v1_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); - list->public_.buckets[0].num_leaderboards = 2; /* didn't actually allocate these */ - list->public_.buckets[0].bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; - list->public_.buckets[0].label = "Inactive"; - list->public_.buckets[0].subset_id = 1234; - - list->destroy_func = rc_client_external_destroy_leaderboard_list; - } - - return (rc_client_leaderboard_list_info_t*)list; -} - -static void test_create_leaderboard_list(void) -{ - rc_client_leaderboard_list_t* list; - - g_client = mock_client_with_external(); - g_client->state.external_client->create_leaderboard_list = rc_client_external_create_leaderboard_list; - - list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); - ASSERT_PTR_NOT_NULL(list); - ASSERT_NUM_EQUALS(list->num_buckets, 1); - ASSERT_PTR_NOT_NULL(list->buckets); - ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); - ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); - ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); - ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); - - rc_client_destroy_leaderboard_list(list); - - rc_client_destroy(g_client); -} - -static void test_has_leaderboards(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->has_leaderboards = rc_client_external_get_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 1); - - rc_client_destroy(g_client); -} - -static void test_get_leaderboard_info(void) -{ - const rc_client_leaderboard_t* leaderboard; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info; - - leaderboard = rc_client_get_leaderboard_info(g_client, 4); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->id, 1234); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); - ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); - ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); - - rc_client_destroy(g_client); -} - -/* ----- rich presence ----- */ - -static size_t rc_client_external_get_rich_presence_message(char buffer[], size_t buffer_size) -{ - size_t result = snprintf(buffer, buffer_size, "Playing Game Title"); - if (result >= buffer_size) - return (buffer_size - 1); - return result; -} - -static void test_get_rich_presence_message(void) -{ - char buffer[16]; - size_t result; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_rich_presence_message = rc_client_external_get_rich_presence_message; - - result = rc_client_get_rich_presence_message(g_client, buffer, sizeof(buffer)); - - ASSERT_STR_EQUALS(buffer, "Playing Game Ti"); - ASSERT_NUM_EQUALS(result, 15); - - rc_client_destroy(g_client); -} - -static void test_has_rich_presence(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->has_rich_presence = rc_client_external_get_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_has_rich_presence(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_has_rich_presence(g_client), 1); - - rc_client_destroy(g_client); -} - -/* ----- processing ----- */ - -static void test_is_processing_required(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->is_processing_required = rc_client_external_get_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_is_processing_required(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_is_processing_required(g_client), 1); - - rc_client_destroy(g_client); -} - -static void rc_client_external_do_frame(void) -{ - g_external_event = "do_frame"; -} - -static void test_do_frame(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->do_frame = rc_client_external_do_frame; - - rc_client_do_frame(g_client); - - ASSERT_STR_EQUALS(g_external_event, "do_frame"); - - rc_client_destroy(g_client); -} - -static void rc_client_external_idle(void) -{ - g_external_event = "idle"; -} - -static void test_idle(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->idle = rc_client_external_idle; - - rc_client_idle(g_client); - - ASSERT_STR_EQUALS(g_external_event, "idle"); - - rc_client_destroy(g_client); -} - -static void rc_client_external_reset(void) -{ - g_external_event = "reset"; -} - -static int rc_client_external_can_pause(uint32_t* frames_remaining) -{ - *frames_remaining = g_external_int ? 0 : 10; - - return g_external_int; -} - -static void test_can_pause(void) -{ - uint32_t frames_remaining; - g_client = mock_client_with_external(); - g_client->state.external_client->can_pause = rc_client_external_can_pause; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 0); - ASSERT_NUM_EQUALS(frames_remaining, 10); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_can_pause(g_client, &frames_remaining), 1); - ASSERT_NUM_EQUALS(frames_remaining, 0); - - rc_client_destroy(g_client); -} - -static void test_reset(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->reset = rc_client_external_reset; - - rc_client_reset(g_client); - - ASSERT_STR_EQUALS(g_external_event, "reset"); - - rc_client_destroy(g_client); -} - -/* ----- progress ----- */ - -static size_t rc_client_external_progress_size(void) -{ - return 12345678; -} - -static void test_progress_size(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->progress_size = rc_client_external_progress_size; - - ASSERT_NUM_EQUALS(rc_client_progress_size(g_client), 12345678); - - rc_client_destroy(g_client); -} - -static int rc_client_external_serialize_progress(uint8_t* buffer, size_t size) -{ - memcpy(buffer, "SAVED", 6); - - g_external_event = "serialize_progress"; - - return RC_OK; -} - -static void test_serialize_progress(void) -{ - int result; - uint8_t buffer[8] = { 0 }; - - g_client = mock_client_with_external(); - g_client->state.external_client->serialize_progress = rc_client_external_serialize_progress; - - result = rc_client_serialize_progress(g_client, buffer); - - ASSERT_STR_EQUALS(g_external_event, "serialize_progress"); - ASSERT_STR_EQUALS(buffer, "SAVED"); - ASSERT_NUM_EQUALS(result, RC_OK); - - rc_client_destroy(g_client); -} - -static int rc_client_external_deserialize_progress(const uint8_t* buffer, size_t size) -{ - if (memcmp(buffer, "SAVE", 5) == 0) - g_external_event = "deserialize_progress"; - - return RC_OK; -} - -static void test_deserialize_progress(void) -{ - int result; - uint8_t buffer[8] = {'S', 'A', 'V', 'E'}; - - g_client = mock_client_with_external(); - g_client->state.external_client->deserialize_progress = rc_client_external_deserialize_progress; - - result = rc_client_deserialize_progress(g_client, buffer); - - ASSERT_STR_EQUALS(g_external_event, "deserialize_progress"); - ASSERT_NUM_EQUALS(result, RC_OK); - - rc_client_destroy(g_client); -} - -/* ----- harness ----- */ - -void test_client_external(void) { - TEST_SUITE_BEGIN(); - - /* settings */ - TEST(test_hardcore_enabled); - TEST(test_unofficial_enabled); - TEST(test_encore_mode_enabled); - TEST(test_spectator_mode_enabled); - TEST(test_enable_logging); - TEST(test_event_handler); - TEST(test_read_memory); - TEST(test_get_time_millisecs); - TEST(test_set_host); - TEST(test_get_user_agent_clause); - TEST(test_set_allow_background_memory_reads); - - /* login */ - TEST(test_v1_user_field_offsets); - TEST(test_v3_user_field_offsets); - TEST(test_login_with_password); - TEST(test_login_with_token_v1); - TEST(test_login_with_token_v1_long_username); - TEST(test_login_with_token_v1_too_long_username); - TEST(test_login_with_token); - - TEST(test_logout); - - /* load game */ - TEST(test_v1_game_field_offsets); - TEST(test_v3_game_field_offsets); - -#ifdef RC_CLIENT_SUPPORTS_HASH - TEST(test_identify_and_load_game_v1); - TEST(test_identify_and_load_game); - TEST(test_identify_and_reload_game); -#endif - TEST(test_load_game_v1); - TEST(test_load_game); - TEST(test_get_game_info_v1_no_game_loaded); - TEST(test_v1_user_game_summary_field_offsets); - TEST(test_v5_user_game_summary_field_offsets); - TEST(test_get_user_game_summary); - TEST(test_get_user_game_summary_v5); - TEST(test_get_user_subset_summary); -#ifdef RC_CLIENT_SUPPORTS_HASH - TEST(test_identify_and_load_game_external_hash); - TEST(test_identify_and_reload_game_external_hash); - TEST(test_change_media); -#endif - TEST(test_change_media_from_hash); - TEST(test_add_game_hash); - - TEST(test_unload_game); - - /* subsets */ - TEST(test_v1_subset_field_offsets); - TEST(test_v3_subset_field_offsets); - TEST(test_get_subset_info_v1); - TEST(test_get_subset_info); - TEST(test_create_subset_list); - - /* achievements */ - TEST(test_v1_achievement_field_offsets); - TEST(test_v3_achievement_field_offsets); - TEST(test_has_achievements); - TEST(test_get_achievement_info_v1); - TEST(test_get_achievement_info_v1_not_found); - TEST(test_get_achievement_info); - - TEST(test_v1_achievement_list_field_offsets); - TEST(test_v3_achievement_list_field_offsets); - TEST(test_create_achievement_list_v1); - TEST(test_create_achievement_list); - - /* leaderboards */ - TEST(test_create_leaderboard_list); - TEST(test_has_leaderboards); - TEST(test_get_leaderboard_info); - - /* rich presence */ - TEST(test_get_rich_presence_message); - TEST(test_has_rich_presence); - - /* processing */ - TEST(test_is_processing_required); - TEST(test_do_frame); - TEST(test_idle); - TEST(test_can_pause); - TEST(test_reset); - - /* progress */ - TEST(test_progress_size); - TEST(test_serialize_progress); - TEST(test_deserialize_progress); - - TEST_SUITE_END(); -} - -#endif /* RC_CLIENT_SUPPORTS_EXTERNAL */ diff --git a/src/rcheevos/test/test_rc_client_raintegration.c b/src/rcheevos/test/test_rc_client_raintegration.c deleted file mode 100644 index 8308c32a36..0000000000 --- a/src/rcheevos/test/test_rc_client_raintegration.c +++ /dev/null @@ -1,441 +0,0 @@ -#include "rc_client.h" - -#include "rc_consoles.h" -#include "rc_hash.h" -#include "rc_internal.h" -#include "rc_api_runtime.h" - -#include "../src/rc_client_internal.h" -#include "../src/rc_version.h" - -#include "rhash/data.h" -#include "test_framework.h" - -#ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION - -static rc_client_t* g_client; -static const char* g_integration_event; -static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ - -/* begin from test_rc_client.c */ - -extern void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); -extern void rc_client_server_call_async(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); -extern void reset_mock_api_handlers(void); -extern void mock_api_response(const char* request_params, const char* response_body); -extern void mock_api_error(const char* request_params, const char* response_body, int http_status_code); -extern void async_api_response(const char* request_params, const char* response_body); -extern void async_api_error(const char* request_params, const char* response_body, int http_status_code); - -/* end from test_rc_client.c */ - -static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) -{ - return 0; -} - -static rc_client_t* mock_client_with_integration() -{ - rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); - client->state.raintegration = (rc_client_raintegration_t*) - rc_buffer_alloc(&client->state.buffer, sizeof(*client->state.raintegration)); - memset(client->state.raintegration, 0, sizeof(*client->state.raintegration)); - - rc_api_set_host(NULL); - reset_mock_api_handlers(); - g_integration_event = "none"; - - return client; -} - -static rc_client_t* mock_client_with_integration_async() -{ - rc_client_t* client = mock_client_with_integration(); - client->callbacks.server_call = rc_client_server_call_async; - return client; -} - -static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_OK); - ASSERT_PTR_NULL(error_message); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_FAIL("Callback should not have been called."); -} - -static uint32_t g_uint32; - -static void rc_client_integration_set_uint(uint32_t value) -{ - g_uint32 = value; -} - -/* ----- version ----- */ - -extern int rc_client_version_less(const char* left, const char* right); - -static void test_version_less(const char* left, const char* right, int expected) -{ - if (expected == 0) { - ASSERT_FALSE(rc_client_version_less(left, right)); - } - else { - ASSERT_TRUE(rc_client_version_less(left, right)); - } -} - -/* ----- login ----- */ - -static void assert_init_params(HWND hWnd, const char* client_name, const char* client_version) -{ - ASSERT_PTR_NOT_NULL((void*)hWnd); - ASSERT_STR_EQUALS(client_name, "TestClient"); - ASSERT_STR_EQUALS(client_version, "1.0.1"); -} - -static int rc_client_integration_init(HWND hWnd, const char* client_name, const char* client_version) -{ - assert_init_params(hWnd, client_name, client_version); - - g_integration_event = "init"; - return 1; -} - -static int rc_client_get_external_client(rc_client_external_t* client, int nVersion) -{ - if (strcmp(g_integration_event, "init") == 0) - g_integration_event = "init2"; - - return 1; -} - -static const char* rc_client_integration_get_version(void) -{ - return "1.3.0"; -} - -static const char* rc_client_integration_get_host_url_offline(void) -{ - return "OFFLINE"; -} - -static void test_load_raintegration(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->get_version = rc_client_integration_get_version; - g_client->state.raintegration->init_client = rc_client_integration_init; - g_client->state.raintegration->get_external_client = rc_client_get_external_client; - - mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.0\"}"); - - rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_integration_event, "init2"); - - rc_client_destroy(g_client); -} - -static void test_load_raintegration_aborted(void) -{ - rc_client_async_handle_t* handle; - - g_client = mock_client_with_integration_async(); - g_client->state.raintegration->get_version = rc_client_integration_get_version; - g_client->state.raintegration->init_client = rc_client_integration_init; - g_client->state.raintegration->get_external_client = rc_client_get_external_client; - - handle = rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", - rc_client_callback_expect_uncalled, g_callback_userdata); - - rc_client_abort_async(g_client, handle); - - async_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.0\"}"); - - ASSERT_STR_EQUALS(g_integration_event, "none"); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_outdated_version(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_ABORTED); - ASSERT_STR_EQUALS(error_message, "RA_Integration version 1.3.0 is lower than minimum version 1.3.1"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_load_raintegration_outdated_version(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->get_version = rc_client_integration_get_version; - g_client->state.raintegration->init_client = rc_client_integration_init; - g_client->state.raintegration->get_external_client = rc_client_get_external_client; - - mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.1\"}"); - - rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", - rc_client_callback_expect_outdated_version, g_callback_userdata); - - ASSERT_STR_EQUALS(g_integration_event, "none"); - - rc_client_destroy(g_client); -} - -static void test_load_raintegration_supported_version(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->get_version = rc_client_integration_get_version; - g_client->state.raintegration->init_client = rc_client_integration_init; - g_client->state.raintegration->get_external_client = rc_client_get_external_client; - - mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.2.1\"}"); - - rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_integration_event, "init2"); - - rc_client_destroy(g_client); -} - -static void test_load_raintegration_offline(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->get_host_url = rc_client_integration_get_host_url_offline; - g_client->state.raintegration->get_version = rc_client_integration_get_version; - g_client->state.raintegration->init_client_offline = rc_client_integration_init; - g_client->state.raintegration->get_external_client = rc_client_get_external_client; - - mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.2.1\"}"); - - rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", - rc_client_callback_expect_success, g_callback_userdata); - - ASSERT_STR_EQUALS(g_integration_event, "init2"); - - rc_client_destroy(g_client); -} - -static void rc_client_callback_expect_after_login_failure(int result, const char* error_message, rc_client_t* client, void* callback_userdata) -{ - ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); - ASSERT_STR_EQUALS(error_message, "Cannot initialize RAIntegration after login"); - ASSERT_PTR_EQUALS(client, g_client); - ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); -} - -static void test_load_raintegration_after_login(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->get_version = rc_client_integration_get_version; - g_client->state.raintegration->init_client = rc_client_integration_init; - g_client->state.raintegration->get_external_client = rc_client_get_external_client; - g_client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; - - mock_api_response("r=latestintegration", "{\"Success\":true,\"MinimumVersion\":\"1.3.0\"}"); - - rc_client_begin_load_raintegration(g_client, L"C:\\Client", (HWND)0x1234, "TestClient", "1.0.1", - rc_client_callback_expect_after_login_failure, g_callback_userdata); - - ASSERT_STR_EQUALS(g_integration_event, "none"); - - rc_client_destroy(g_client); -} - -/* ----- windows ----- */ - -static HWND g_hWnd; - -static void rc_client_integration_update_main_window_handle(HWND hWnd) -{ - g_hWnd = hWnd; -} - -static void test_update_main_window_handle(void) -{ - HWND hWnd = (HWND)0x12345678; - g_hWnd = NULL; - - g_client = mock_client_with_integration(); - g_client->state.raintegration->update_main_window_handle = rc_client_integration_update_main_window_handle; - - /* does nothing if raintegration hasn't been initialized */ - ASSERT_NUM_EQUALS(g_client->state.raintegration->bIsInited, 0); - rc_client_raintegration_update_main_window_handle(g_client, hWnd); - ASSERT_PTR_NULL(g_hWnd); - - g_client->state.raintegration->bIsInited = 1; - rc_client_raintegration_update_main_window_handle(g_client, hWnd); - ASSERT_PTR_EQUALS(g_hWnd, hWnd); - - g_hWnd = NULL; -} - -/* ----- menu ----- */ - -static rc_client_raintegration_menu_t* g_menu; - -static const rc_client_raintegration_menu_t* rc_client_integration_get_menu(void) -{ - return g_menu; -} - -static void test_get_menu(void) -{ - const rc_client_raintegration_menu_t* menu; - rc_client_raintegration_menu_t local_menu; - rc_client_raintegration_menu_item_t local_menu_items[3]; - - memset(&local_menu_items, 0, sizeof(local_menu_items)); - local_menu_items[0].id = 1234; - local_menu_items[0].label = "Label 1"; - local_menu_items[0].checked = 1; - local_menu_items[0].enabled = 1; - local_menu_items[2].id = 2345; - local_menu_items[2].label = "Label 2"; - - local_menu.num_items = sizeof(local_menu_items) / sizeof(local_menu_items[0]); - local_menu.items = local_menu_items; - g_menu = &local_menu; - - g_client = mock_client_with_integration(); - g_client->state.raintegration->get_menu = rc_client_integration_get_menu; - - /* returns null if raintegration hasn't been initialized */ - ASSERT_NUM_EQUALS(g_client->state.raintegration->bIsInited, 0); - menu = rc_client_raintegration_get_menu(g_client); - ASSERT_PTR_NULL(menu); - - g_client->state.raintegration->bIsInited = 1; - menu = rc_client_raintegration_get_menu(g_client); - ASSERT_PTR_NOT_NULL(menu); - - ASSERT_NUM_EQUALS(menu->num_items, 3); - ASSERT_NUM_EQUALS(menu->items[0].id, 1234); - ASSERT_STR_EQUALS(menu->items[0].label, "Label 1"); - ASSERT_NUM_EQUALS(menu->items[0].checked, 1); - ASSERT_NUM_EQUALS(menu->items[0].enabled, 1); - ASSERT_NUM_EQUALS(menu->items[1].id, 0); - ASSERT_PTR_NULL(menu->items[1].label); - ASSERT_NUM_EQUALS(menu->items[1].checked, 0); - ASSERT_NUM_EQUALS(menu->items[1].enabled, 0); - ASSERT_NUM_EQUALS(menu->items[2].id, 2345); - ASSERT_STR_EQUALS(menu->items[2].label, "Label 2"); - ASSERT_NUM_EQUALS(menu->items[2].checked, 0); - ASSERT_NUM_EQUALS(menu->items[2].enabled, 0); - - g_menu = NULL; -} - -static int rc_client_integration_activate_menu_item(uint32_t id) -{ - if (id < 1700) - return 0; - - g_uint32 = id; - return 1; -} - -static void test_activate_menu_item(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->activate_menu_item = rc_client_integration_activate_menu_item; - - g_uint32 = 0; - ASSERT_NUM_EQUALS(rc_client_raintegration_activate_menu_item(g_client, 1600), 0); - ASSERT_NUM_EQUALS(g_uint32, 0); - - ASSERT_NUM_EQUALS(rc_client_raintegration_activate_menu_item(g_client, 1704), 1); - ASSERT_NUM_EQUALS(g_uint32, 1704); -} - -static void rc_client_integration_set_console_id(int console_id) -{ - g_uint32 = console_id; -} - -static void test_set_console_id(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->set_console_id = rc_client_integration_set_console_id; - - g_uint32 = 0; - rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_NINTENDO); - ASSERT_NUM_EQUALS(g_uint32, RC_CONSOLE_NINTENDO); - - rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_PLAYSTATION); - ASSERT_NUM_EQUALS(g_uint32, RC_CONSOLE_PLAYSTATION); - - rc_client_raintegration_set_console_id(g_client, RC_CONSOLE_MEGA_DRIVE); - ASSERT_NUM_EQUALS(g_uint32, RC_CONSOLE_MEGA_DRIVE); -} - -static int rc_client_integration_has_modifications(void) -{ - return (int)g_uint32; -} - -static void test_has_modifications(void) -{ - g_client = mock_client_with_integration(); - g_client->state.raintegration->has_modifications = rc_client_integration_has_modifications; - - g_uint32 = 0; - ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 0); - - g_uint32 = 1; - ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 0); - - g_client->state.raintegration->bIsInited = 1; - ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 1); - - g_uint32 = 0; - ASSERT_NUM_EQUALS(rc_client_raintegration_has_modifications(g_client), 0); -} - -/* ----- harness ----- */ - -void test_client_raintegration(void) { - TEST_SUITE_BEGIN(); - - /* version */ - TEST_PARAMS3(test_version_less, "0.0.0", "1.0.0", 1); - TEST_PARAMS3(test_version_less, "1.0.0", "0.0.0", 0); - TEST_PARAMS3(test_version_less, "1.2.0", "1.2.0", 0); - TEST_PARAMS3(test_version_less, "1.2.0", "1.1.0", 0); - TEST_PARAMS3(test_version_less, "1.2.0", "1.3.0", 1); - TEST_PARAMS3(test_version_less, "1.2.0", "1.10.0", 1); - TEST_PARAMS3(test_version_less, "1.2.0", "2.1.0", 1); - TEST_PARAMS3(test_version_less, "2.1.0", "1.2.0", 0); - - /* login */ - TEST(test_load_raintegration); - TEST(test_load_raintegration_aborted); - TEST(test_load_raintegration_outdated_version); - TEST(test_load_raintegration_supported_version); - TEST(test_load_raintegration_offline); - TEST(test_load_raintegration_after_login); - - /* windows */ - TEST(test_update_main_window_handle); - - /* menu */ - TEST(test_get_menu); - TEST(test_activate_menu_item); - - /* set_console_id */ - TEST(test_set_console_id); - - /* has_modifications */ - TEST(test_has_modifications); - - TEST_SUITE_END(); -} - -#endif /* RC_CLIENT_SUPPORTS_RAINTEGRATION */ diff --git a/src/rcheevos/test/test_rc_libretro.c b/src/rcheevos/test/test_rc_libretro.c deleted file mode 100644 index f750f574c5..0000000000 --- a/src/rcheevos/test/test_rc_libretro.c +++ /dev/null @@ -1,952 +0,0 @@ -#include "../rc_libretro.h" - -#include "../rc_compat.h" -#include "rc_consoles.h" - -#include "test_framework.h" -#include "rhash/mock_filereader.h" - -static void* retro_memory_data[4] = { NULL, NULL, NULL, NULL }; -static size_t retro_memory_size[4] = { 0, 0, 0, 0 }; - -static void libretro_get_core_memory_info(uint32_t id, rc_libretro_core_memory_info_t* info) -{ - info->data = retro_memory_data[id]; - info->size = retro_memory_size[id]; -} - -static int libretro_get_image_path(uint32_t index, char* buffer, size_t buffer_size) -{ - if (index < 0 || index > 9) - return 0; - - snprintf(buffer, buffer_size, "save%d.dsk", index); - return 1; -} - -static void test_allowed_setting(const char* library_name, const char* setting, const char* value) { - const rc_disallowed_setting_t* settings = rc_libretro_get_disallowed_settings(library_name); - if (!settings) - return; - - ASSERT_TRUE(rc_libretro_is_setting_allowed(settings, setting, value)); -} - -static void test_disallowed_setting(const char* library_name, const char* setting, const char* value) { - const rc_disallowed_setting_t* settings = rc_libretro_get_disallowed_settings(library_name); - ASSERT_PTR_NOT_NULL(settings); - ASSERT_FALSE(rc_libretro_is_setting_allowed(settings, setting, value)); -} - -static void test_allowed_system(const char* library_name, uint32_t console_id) { - ASSERT_TRUE(rc_libretro_is_system_allowed(library_name, console_id)); -} - -static void test_disallowed_system(const char* library_name, uint32_t console_id) { - ASSERT_FALSE(rc_libretro_is_system_allowed(library_name, console_id)); -} - -static void test_memory_init_without_regions() { - rc_libretro_memory_regions_t regions; - uint32_t avail; - #define BUFFER1_SIZE 16 - #define BUFFER2_SIZE 8 - /* put explicit gap between buffer1 and buffer2 to prevent them from being merged when building regions */ - uint8_t buffer[BUFFER1_SIZE + 4 + BUFFER2_SIZE]; - uint8_t* buffer1 = &buffer[0]; - uint8_t* buffer2 = &buffer[20]; - uint8_t buffer3[4]; - uint32_t i; - - for (i = 0; i < BUFFER1_SIZE; ++i) - buffer1[i] = i; - for (i = 0; i < BUFFER2_SIZE; ++i) - buffer2[i] = i; - for (i = 0; i < sizeof(buffer3); ++i) - buffer3[i] = i; - - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = BUFFER1_SIZE; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = BUFFER2_SIZE; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, BUFFER1_SIZE + BUFFER2_SIZE); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, BUFFER1_SIZE + 2), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, BUFFER1_SIZE + BUFFER2_SIZE + 2)); - - ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 2, &avail), &buffer1[2]); - ASSERT_NUM_EQUALS(avail, BUFFER1_SIZE - 2); - ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, BUFFER1_SIZE - 1, &avail), &buffer1[BUFFER1_SIZE - 1]); - ASSERT_NUM_EQUALS(avail, 1); - ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, BUFFER1_SIZE + 2, &avail), &buffer2[2]); - ASSERT_NUM_EQUALS(avail, BUFFER2_SIZE - 2); - ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, BUFFER1_SIZE + BUFFER2_SIZE + 2, &avail)); - ASSERT_NUM_EQUALS(avail, 0); - - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 2, buffer3, 1), 1); - ASSERT_TRUE(memcmp(buffer3, &buffer1[2], 1) == 0); - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 7, buffer3, 4), 4); - ASSERT_TRUE(memcmp(buffer3, &buffer1[7], 4) == 0); - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, BUFFER1_SIZE - 2, buffer3, 3), 3); /* read across boundary */ - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, BUFFER1_SIZE + BUFFER2_SIZE + 2, buffer3, 1), 0); - - #undef BUFFER2_SIZE - #undef BUFFER1_SIZE -} - -static void test_memory_init_without_regions_system_ram_only() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[16]; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); - - ASSERT_NUM_EQUALS(regions.count, 1); - ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer1)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer1[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer1) + 2)); -} - -static void test_memory_init_without_regions_save_ram_only() { - rc_libretro_memory_regions_t regions; - uint8_t buffer2[8]; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); - - ASSERT_NUM_EQUALS(regions.count, 1); - ASSERT_NUM_EQUALS(regions.total_size, sizeof(buffer2)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 2), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, sizeof(buffer2) + 2)); -} - -static void test_memory_init_without_regions_no_ram() { - rc_libretro_memory_regions_t regions; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_HUBS)); - - ASSERT_NUM_EQUALS(regions.count, 0); - ASSERT_NUM_EQUALS(regions.total_size, 0); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 2)); -} - -static void test_memory_init_from_unmapped_memory() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8], buffer2[8]; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0x10000; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_unmapped_memory_null_filler() { - rc_libretro_memory_regions_t regions; - uint32_t avail; - uint8_t buffer1[16], buffer2[8]; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = sizeof(buffer2); - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 4); /* two valid regions and two null fillers */ - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00012)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x1000A)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); - - ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 0x00002, &avail), &buffer1[2]); - ASSERT_NUM_EQUALS(avail, sizeof(buffer1) - 2); - ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, 0x00012, &avail)); - ASSERT_NUM_EQUALS(avail, 0); - ASSERT_PTR_EQUALS(rc_libretro_memory_find_avail(®ions, 0x10002, &avail), &buffer2[2]); - ASSERT_NUM_EQUALS(avail, sizeof(buffer2) - 2); - ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, 0x1000A, &avail)); - ASSERT_NUM_EQUALS(avail, 0); -} - -static void test_memory_init_from_unmapped_memory_no_save_ram() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[16]; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_unmapped_memory_merge_neighbors() { - rc_libretro_memory_regions_t regions; - uint8_t* buffer1 = (uint8_t*)malloc(0x10000); /* have to malloc to prevent array-bounds compiler warnings */ - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x10000; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_ATARI_LYNX)); - - ASSERT_NUM_EQUALS(regions.count, 1); /* all regions are adjacent, so should be merged */ - ASSERT_NUM_EQUALS(regions.total_size, 0x10000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer1[0x0002]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer1[0x0102]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0xFFFF), &buffer1[0xFFFF]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10000)); - - free(buffer1); -} - -static void test_memory_init_from_unmapped_memory_no_ram() { - rc_libretro_memory_regions_t regions; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - /* init returns false */ - ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - /* but one null-filled region is still generated */ - ASSERT_NUM_EQUALS(regions.count, 1); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_unmapped_memory_ds() { - rc_libretro_memory_regions_t regions; - uint8_t* buffer1 = (uint8_t*)malloc(0x02000000); /* have to malloc to prevent array-bounds compiler warnings */ - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0x02000000; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - /* init returns true */ - ASSERT_TRUE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_NINTENDO_DS)); - - /* primary region should be generated, but TCM won't be available */ - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x1004000); - ASSERT_PTR_NOT_NULL(rc_libretro_memory_find(®ions, 0x00000002)); - ASSERT_PTR_NOT_NULL(rc_libretro_memory_find(®ions, 0x003FFFFF)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00400000)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x01000002)); - - free(buffer1); -} - -static void test_memory_init_from_unmapped_memory_wii() { - rc_libretro_memory_regions_t regions; - retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = 0; - retro_memory_data[RETRO_MEMORY_SAVE_RAM] = NULL; - retro_memory_size[RETRO_MEMORY_SAVE_RAM] = 0; - - /* init returns false */ - ASSERT_FALSE(rc_libretro_memory_init(®ions, NULL, libretro_get_core_memory_info, RC_CONSOLE_WII)); - - /* but one null-filled region is still generated */ - ASSERT_NUM_EQUALS(regions.count, 1); - ASSERT_NUM_EQUALS(regions.total_size, 0x14000000); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00000002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x14000002)); -} - -static void test_memory_init_from_memory_map() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8], buffer2[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, - { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_null_filler() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8], buffer2[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, - { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_no_save_ram() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_merge_neighbors() { - rc_libretro_memory_regions_t regions; - uint8_t* buffer1 = (uint8_t*)malloc(0x10000); /* have to malloc to prevent array-bounds compiler warnings */ - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0x0000], 0, 0x0000U, 0, 0, 0xFC00, "RAM" }, - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0xFC00], 0, 0xFC00U, 0, 0, 0x0400, "Hardware controllers" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_ATARI_LYNX)); - - ASSERT_NUM_EQUALS(regions.count, 1); /* all regions are adjacent, so should be merged */ - ASSERT_NUM_EQUALS(regions.total_size, 0x10000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer1[0x0002]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer1[0x0102]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0xFFFF), &buffer1[0xFFFF]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10000)); - - free(buffer1); -} - -static void test_memory_init_from_memory_map_no_ram() { - rc_libretro_memory_regions_t regions; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, NULL, 0, 0xFF0000U, 0, 0, 0x10000, "RAM" }, - { RETRO_MEMDESC_SAVE_RAM, NULL, 0, 0x000000U, 0, 0, 0x10000, "SRAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - /* init returns false */ - ASSERT_FALSE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - /* but one null-filled region is still generated */ - ASSERT_NUM_EQUALS(regions.count, 1); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x10002)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_splice() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8], buffer2[8], buffer3[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x08000, "RAM1" }, - { RETRO_MEMDESC_SYSTEM_RAM, &buffer2[0], 0, 0xFF8000U, 0, 0, 0x08000, "RAM2" }, - { RETRO_MEMDESC_SAVE_RAM, &buffer3[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 3); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer2[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer3[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_mirrored() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8], buffer2[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0xFF0000U, 0x00C000U, 0x04000, "RAM" }, - { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0x000000U, 0x000000U, 0x10000, "SRAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - /* select of 0xFF0000 should mirror the 0x4000 bytes at 0xFF0000 into 0xFF4000, 0xFF8000, and 0xFFC000 */ - ASSERT_NUM_EQUALS(regions.count, 5); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x04002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x08002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0C002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_out_of_order() { - rc_libretro_memory_regions_t regions; - uint8_t buffer1[8], buffer2[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SAVE_RAM, &buffer2[0], 0, 0x000000U, 0, 0, 0x10000, "SRAM" }, - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0xFF0000U, 0, 0, 0x10000, "RAM" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MEGA_DRIVE)); - - ASSERT_NUM_EQUALS(regions.count, 2); - ASSERT_NUM_EQUALS(regions.total_size, 0x20000); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00002), &buffer1[2]); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x10002), &buffer2[2]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x20002)); -} - -static void test_memory_init_from_memory_map_disconnect_gaps() { - rc_libretro_memory_regions_t regions; - uint8_t buffer[256]; - /* the disconnect bit is smaller than the region size, so only parts of the memory map - * will be filled by the region. in this case, 00-1F will be buffer[00-1F], but - * buffer[20-3F] will be associated to addresses 40-5F! */ - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer[0], 0, 0x0000, 0xFC20, 0x0020, sizeof(buffer), "RAM" }, - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_MAGNAVOX_ODYSSEY2)); - - ASSERT_NUM_EQUALS(regions.count, 10); - ASSERT_NUM_EQUALS(regions.total_size, 0x140); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0002), &buffer[0x02]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0022)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0042), &buffer[0x22]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0062)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0082), &buffer[0x42]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00A2)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x00C2), &buffer[0x62]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x00E2)); - ASSERT_PTR_EQUALS(rc_libretro_memory_find(®ions, 0x0102), &buffer[0x82]); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0122)); - ASSERT_PTR_NULL(rc_libretro_memory_find(®ions, 0x0142)); -} - - -static void test_memory_read() -{ - rc_libretro_memory_regions_t regions; - /* intentionally put buffer3 between buffer1 and buffer2 for the read that spans buffers */ - uint8_t buffer1[8], buffer3[4], buffer2[8]; - const struct retro_memory_descriptor mmap_desc[] = { - { RETRO_MEMDESC_SYSTEM_RAM, &buffer1[0], 0, 0x000000U, 0, 0, 0x000008, "RAM A" }, - { RETRO_MEMDESC_SYSTEM_RAM, &buffer2[0], 0, 0x000008U, 0, 0, 0x000008, "RAM B" } - }; - const struct retro_memory_map mmap = { mmap_desc, sizeof(mmap_desc) / sizeof(mmap_desc[0]) }; - int i; - - for (i = 0; i < 8; ++i) { - buffer1[i] = i; - buffer2[i] = i + 8; - } - - ASSERT_TRUE(rc_libretro_memory_init(®ions, &mmap, libretro_get_core_memory_info, RC_CONSOLE_NINTENDO)); - - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 0, buffer3, 4), 4); - ASSERT_NUM_EQUALS(buffer3[0], 0); - ASSERT_NUM_EQUALS(buffer3[1], 1); - ASSERT_NUM_EQUALS(buffer3[2], 2); - ASSERT_NUM_EQUALS(buffer3[3], 3); - - /* only requesting two bytes, other two remain unmodified */ - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 3, buffer3, 2), 2); - ASSERT_NUM_EQUALS(buffer3[0], 3); - ASSERT_NUM_EQUALS(buffer3[1], 4); - ASSERT_NUM_EQUALS(buffer3[2], 2); - ASSERT_NUM_EQUALS(buffer3[3], 3); - - /* one two bytes are available in buffer1, the rest have to be read from buffer2 */ - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 6, buffer3, 4), 4); - ASSERT_NUM_EQUALS(buffer3[0], 6); - ASSERT_NUM_EQUALS(buffer3[1], 7); - ASSERT_NUM_EQUALS(buffer3[2], 8); /* this comes from buffer2 */ - ASSERT_NUM_EQUALS(buffer3[3], 9); - - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 8, buffer3, 4), 4); - ASSERT_NUM_EQUALS(buffer3[0], 8); - ASSERT_NUM_EQUALS(buffer3[1], 9); - ASSERT_NUM_EQUALS(buffer3[2], 10); - ASSERT_NUM_EQUALS(buffer3[3], 11); - - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 11, buffer3, 4), 4); - ASSERT_NUM_EQUALS(buffer3[0], 11); - ASSERT_NUM_EQUALS(buffer3[1], 12); - ASSERT_NUM_EQUALS(buffer3[2], 13); - ASSERT_NUM_EQUALS(buffer3[3], 14); - - /* only requesting 1 byte. other three remain unmodified */ - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 13, buffer3, 1), 1); - ASSERT_NUM_EQUALS(buffer3[0], 13); - ASSERT_NUM_EQUALS(buffer3[1], 12); - ASSERT_NUM_EQUALS(buffer3[2], 13); - ASSERT_NUM_EQUALS(buffer3[3], 14); - - /* only two bytes are available at address 14 */ - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 14, buffer3, 3), 2); - ASSERT_NUM_EQUALS(buffer3[0], 14); - ASSERT_NUM_EQUALS(buffer3[1], 15); - ASSERT_NUM_EQUALS(buffer3[2], 13); - ASSERT_NUM_EQUALS(buffer3[3], 14); - - /* no bytes are available at invalid address */ - ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 16, buffer3, 4), 0); - ASSERT_NUM_EQUALS(buffer3[0], 14); - ASSERT_NUM_EQUALS(buffer3[1], 15); - ASSERT_NUM_EQUALS(buffer3[2], 13); - ASSERT_NUM_EQUALS(buffer3[3], 14); -} - -static void test_hash_set_add_single() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char hash[] = "ABCDEF01234567899876543210ABCDEF"; - - get_mock_filereader(&file_reader); - - rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path, &file_reader); - - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.rom")); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); - - rc_libretro_hash_set_add(&hash_set, "file.rom", 1234, hash); - - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 1234); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static void test_hash_set_update_single() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char hash[] = "ABCDEF01234567899876543210ABCDEF"; - const char hash2[] = "0123456789ABCDEF0123456789ABCDEF"; - - get_mock_filereader(&file_reader); - - rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path, &file_reader); - - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.rom")); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); - - rc_libretro_hash_set_add(&hash_set, "file.rom", 99, hash); - - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 99); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 0); - - rc_libretro_hash_set_add(&hash_set, "file.rom", 1234, hash2); - ASSERT_NUM_EQUALS(hash_set.entries_count, 1); - - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file.rom"), hash2); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 1234); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static void test_hash_set_add_many() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char hash1[] = "ABCDEF01234567899876543210ABCDE1"; - const char hash2[] = "ABCDEF01234567899876543210ABCDE2"; - const char hash3[] = "ABCDEF01234567899876543210ABCDE3"; - const char hash4[] = "ABCDEF01234567899876543210ABCDE4"; - const char hash5[] = "ABCDEF01234567899876543210ABCDE5"; - const char hash6[] = "ABCDEF01234567899876543210ABCDE6"; - const char hash7[] = "ABCDEF01234567899876543210ABCDE7"; - const char hash8[] = "ABCDEF01234567899876543210ABCDE8"; - - get_mock_filereader(&file_reader); - - rc_libretro_hash_set_init(&hash_set, "file.rom", libretro_get_image_path, &file_reader); - - rc_libretro_hash_set_add(&hash_set, "file1.rom", 1, hash1); - rc_libretro_hash_set_add(&hash_set, "file2.rom", 2, hash2); - rc_libretro_hash_set_add(&hash_set, "file3.rom", 3, hash3); - rc_libretro_hash_set_add(&hash_set, "file4.rom", 4, hash4); - rc_libretro_hash_set_add(&hash_set, "file5.rom", 5, hash5); - rc_libretro_hash_set_add(&hash_set, "file6.rom", 6, hash6); - rc_libretro_hash_set_add(&hash_set, "file7.rom", 7, hash7); - rc_libretro_hash_set_add(&hash_set, "file8.rom", 8, hash8); - - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file1.rom"), hash1); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash1), 1); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file2.rom"), hash2); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash2), 2); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file3.rom"), hash3); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash3), 3); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file4.rom"), hash4); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash4), 4); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file5.rom"), hash5); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash5), 5); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file6.rom"), hash6); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash6), 6); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file7.rom"), hash7); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash7), 7); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "file8.rom"), hash8); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash8), 8); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static void test_hash_set_m3u_single() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char hash[] = "ABCDEF01234567899876543210ABCDEF"; - const char* m3u_contents = "file.dsk"; - - get_mock_filereader(&file_reader); - mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); - - rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); - - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); - ASSERT_NUM_EQUALS(rc_libretro_hash_set_get_game_id(&hash_set, hash), 0); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk")); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static void test_hash_set_m3u_savedisk() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char* m3u_contents = "file.dsk\n#SAVEDISK:"; - - get_mock_filereader(&file_reader); - mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); - - rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); - - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static void test_hash_set_m3u_savedisk_volume_label() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char* m3u_contents = "file.dsk\n#SAVEDISK:DSAVE"; - - get_mock_filereader(&file_reader); - mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); - - rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); - - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static void test_hash_set_m3u_savedisk_multiple_with_comments_and_whitespace() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char* m3u_contents = - "#EXTM3U\n" - "file.dsk\n" /* index 0 */ - "\n" - "#Save disk in the middle, because why not?\n" - "#SAVEDISK:\n" /* index 1 */ - " \r\n" - "\tfile2.dsk|File 2\n" /* index 2 */ - "#SAVEDISK:DSAVE\n" /* index 3 */ - "\t\r\n" - "#LABEL:My Custom Disk Label\n" - "file3.dsk" /* index 4 */ - "\r\n" - "#SAVEDISK:|No Custom Label for Save Disk"; /* index 5 */ - - get_mock_filereader(&file_reader); - mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); - - rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path, &file_reader); - - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file.dsk")); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk"), "[SAVE DISK]"); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file2.dsk")); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save3.dsk"), "[SAVE DISK]"); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file3.dsk")); - ASSERT_STR_EQUALS(rc_libretro_hash_set_get_hash(&hash_set, "save5.dsk"), "[SAVE DISK]"); - - rc_libretro_hash_set_destroy(&hash_set); -} - -static int libretro_get_image_path_no_core_support(uint32_t index, char* buffer, size_t buffer_size) -{ - if (index < 0 || index > 1) - return 0; - - snprintf(buffer, buffer_size, "file%d.dsk", index); - return 1; -} - -static void test_hash_set_m3u_savedisk_no_core_support() { - rc_libretro_hash_set_t hash_set; - rc_hash_filereader_t file_reader; - const char* m3u_contents = "file1.dsk\n#SAVEDISK:\nfile2.dsk"; - - get_mock_filereader(&file_reader); - mock_file(0, "game.m3u", (uint8_t*)m3u_contents, strlen(m3u_contents)); - - rc_libretro_hash_set_init(&hash_set, "game.m3u", libretro_get_image_path_no_core_support, &file_reader); - - ASSERT_NUM_EQUALS(hash_set.entries_count, 0); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file1.dsk")); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file2.dsk")); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "file3.dsk")); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save1.dsk")); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save2.dsk")); - ASSERT_PTR_NULL(rc_libretro_hash_set_get_hash(&hash_set, "save3.dsk")); - - rc_libretro_hash_set_destroy(&hash_set); -} - - -void test_rc_libretro(void) { - TEST_SUITE_BEGIN(); - - /* rc_libretro_disallowed_settings */ - TEST_PARAMS3(test_allowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "750%"); - TEST_PARAMS3(test_allowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "100%(native)"); - TEST_PARAMS3(test_disallowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "99%"); - TEST_PARAMS3(test_disallowed_setting, "Beetle PSX", "beetle_psx_cpu_freq_scale", "50%"); - - TEST_PARAMS3(test_allowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "750%"); - TEST_PARAMS3(test_allowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "100%(native)"); - TEST_PARAMS3(test_disallowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "99%"); - TEST_PARAMS3(test_disallowed_setting, "Beetle PSX HW", "beetle_psx_hw_cpu_freq_scale", "50%"); - - TEST_PARAMS3(test_allowed_setting, "bsnes-mercury", "bsnes_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "bsnes-mercury", "bsnes_region", "NTSC"); - TEST_PARAMS3(test_disallowed_setting, "bsnes-mercury", "bsnes_region", "PAL"); - - TEST_PARAMS3(test_allowed_setting, "cap32", "cap32_autorun", "enabled"); - TEST_PARAMS3(test_disallowed_setting, "cap32", "cap32_autorun", "disabled"); - - TEST_PARAMS3(test_allowed_setting, "dolphin-emu", "dolphin_cheats_enabled", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "dolphin-emu", "dolphin_cheats_enabled", "enabled"); - - TEST_PARAMS3(test_allowed_setting, "DOSBox-pure", "dosbox_pure_strict_mode", "true"); - TEST_PARAMS3(test_disallowed_setting, "DOSBox-pure", "dosbox_pure_strict_mode", "false"); - - TEST_PARAMS3(test_allowed_setting, "DuckStation", "duckstation_CDROM.LoadImagePatches", "false"); - TEST_PARAMS3(test_disallowed_setting, "DuckStation", "duckstation_CDROM.LoadImagePatches", "true"); - - TEST_PARAMS3(test_allowed_setting, "ecwolf", "ecwolf-invulnerability", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "ecwolf", "ecwolf-invulnerability", "enabled"); - - TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_region", "NTSC"); - TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "PAL"); - TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "pal"); /* case insensitive */ - TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_region", "Dendy"); - TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_palette", "default"); /* setting we don't care about */ - TEST_PARAMS3(test_allowed_setting, "FCEUmm", "fceumm_game_genie", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "FCEUmm", "fceumm_game_genie", "enabled"); - - TEST_PARAMS3(test_allowed_setting, "Flycast", "reicast_sh4clock", "500"); - TEST_PARAMS3(test_allowed_setting, "Flycast", "reicast_sh4clock", "200"); - TEST_PARAMS3(test_disallowed_setting, "Flycast", "reicast_sh4clock", "190"); - TEST_PARAMS3(test_disallowed_setting, "Flycast", "reicast_sh4clock", "50"); - - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-allow-patched-romsets", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-allow-patched-romsets", "enabled"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "disabled"); /* wildcard key match */ - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "enabled"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "0 - Disabled"); /* multi-not value match */ - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cheat-mvsc-P1_Char_1_Easy_Hyper_Combo", "1 - Enabled"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "25%"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "50%"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "75%"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "90%"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "95%"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "99%"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "100%"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "150%"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "200%"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-cpu-speed-adjust", "400%"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-dipswitch-mslug-BIOS", "MVS Asia/Europe ver. 6 (1 slot)"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-dipswitch-mslug-BIOS", "Universe BIOS ver. 2.3 (alt)"); - TEST_PARAMS3(test_allowed_setting, "FinalBurn Neo", "fbneo-neogeo-mode", "DIPSWITCH"); - TEST_PARAMS3(test_disallowed_setting, "FinalBurn Neo", "fbneo-neogeo-mode", "UNIBIOS"); - - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "action replay (pro)"); - TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "game genie"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_lock_on", "sonic & knuckles"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "Auto"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "NTSC-U"); - TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "PAL"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX", "genesis_plus_gx_region_detect", "NTSC-J"); - - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "action replay (pro)"); - TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "game genie"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_lock_on", "sonic & knuckles"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "Auto"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "NTSC-U"); - TEST_PARAMS3(test_disallowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "PAL"); - TEST_PARAMS3(test_allowed_setting, "Genesis Plus GX Wide", "genesis_plus_gx_wide_region_detect", "NTSC-J"); - - TEST_PARAMS3(test_allowed_setting, "Mesen", "mesen_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "Mesen", "mesen_region", "NTSC"); - TEST_PARAMS3(test_disallowed_setting, "Mesen", "mesen_region", "PAL"); - TEST_PARAMS3(test_disallowed_setting, "Mesen", "mesen_region", "Dendy"); - - TEST_PARAMS3(test_allowed_setting, "Mesen-S", "mesen-s_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "Mesen-S", "mesen-s_region", "NTSC"); - TEST_PARAMS3(test_disallowed_setting, "Mesen-S", "mesen-s_region", "PAL"); - - TEST_PARAMS3(test_allowed_setting, "NeoCD", "neocd_bios", "neocd.bin (CDZ)"); - TEST_PARAMS3(test_disallowed_setting, "NeoCD", "neocd_bios", "uni-bioscd.rom (CDZ, Universe 3.3)"); - - TEST_PARAMS3(test_allowed_setting, "PPSSPP", "ppsspp_cheats", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "PPSSPP", "ppsspp_cheats", "enabled"); - - TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "NTSC"); - TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_region", "PAL"); - TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "auto"); - TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "100"); - TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "57"); - TEST_PARAMS3(test_allowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "55"); - TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "54"); - TEST_PARAMS3(test_disallowed_setting, "PCSX-ReARMed", "pcsx_rearmed_psxclock", "30"); - - TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "US"); - TEST_PARAMS3(test_allowed_setting, "PicoDrive", "picodrive_region", "Japan NTSC"); - TEST_PARAMS3(test_disallowed_setting, "PicoDrive", "picodrive_region", "Europe"); - TEST_PARAMS3(test_disallowed_setting, "PicoDrive", "picodrive_region", "Japan PAL"); - - TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "16"); - TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "8"); - TEST_PARAMS3(test_allowed_setting, "QUASI88", "q88_cpu_clock", "4"); - TEST_PARAMS3(test_disallowed_setting, "QUASI88", "q88_cpu_clock", "2"); - TEST_PARAMS3(test_disallowed_setting, "QUASI88", "q88_cpu_clock", "1"); - - TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "auto"); - TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "ntsc-u"); - TEST_PARAMS3(test_disallowed_setting, "SMS Plus GX", "smsplus_region", "pal"); - TEST_PARAMS3(test_allowed_setting, "SMS Plus GX", "smsplus_region", "ntsc-j"); - - TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_region", "Auto"); - TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_region", "NTSC"); - TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_region", "PAL"); - TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_gfx_clip", "enabled"); - TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_gfx_clip", "disabled"); - TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_gfx_transp", "enabled"); - TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_gfx_transp", "disabled"); - TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_layer_1", "enabled"); - TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_layer_1", "disabled"); - TEST_PARAMS3(test_allowed_setting, "Snes9x", "snes9x_layer_5", "enabled"); - TEST_PARAMS3(test_disallowed_setting, "Snes9x", "snes9x_layer_5", "disabled"); - - TEST_PARAMS3(test_allowed_setting, "SwanStation", "swanstation_CPU_Overclock", "1000"); - TEST_PARAMS3(test_allowed_setting, "SwanStation", "swanstation_CPU_Overclock", "100"); - TEST_PARAMS3(test_disallowed_setting, "SwanStation", "swanstation_CPU_Overclock", "99"); - TEST_PARAMS3(test_disallowed_setting, "SwanStation", "swanstation_CPU_Overclock", "50"); - - TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_autostart", "enabled"); - TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_autostart", "disabled"); - TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_autostart", "warp"); - TEST_PARAMS3(test_allowed_setting, "VICE x64", "vice_reset", "autostart"); - TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "soft"); - TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "hard"); - TEST_PARAMS3(test_disallowed_setting, "VICE x64", "vice_reset", "freeze"); - - TEST_PARAMS3(test_allowed_setting, "Virtual Jaguar", "virtualjaguar_pal", "disabled"); - TEST_PARAMS3(test_disallowed_setting, "Virtual Jaguar", "virtualjaguar_pal", "enabled"); - - /* rc_libretro_is_system_allowed */ - TEST_PARAMS2(test_allowed_system, "FCEUmm", RC_CONSOLE_NINTENDO); - - TEST_PARAMS2(test_allowed_system, "Mesen-S", RC_CONSOLE_SUPER_NINTENDO); - TEST_PARAMS2(test_disallowed_system, "Mesen-S", RC_CONSOLE_GAMEBOY); - TEST_PARAMS2(test_disallowed_system, "Mesen-S", RC_CONSOLE_GAMEBOY_COLOR); - - /* rc_libretro_memory_init */ - TEST(test_memory_init_without_regions); - TEST(test_memory_init_without_regions_system_ram_only); - TEST(test_memory_init_without_regions_save_ram_only); - TEST(test_memory_init_without_regions_no_ram); - - TEST(test_memory_init_from_unmapped_memory); - TEST(test_memory_init_from_unmapped_memory_null_filler); - TEST(test_memory_init_from_unmapped_memory_no_save_ram); - TEST(test_memory_init_from_unmapped_memory_merge_neighbors); - TEST(test_memory_init_from_unmapped_memory_no_ram); - TEST(test_memory_init_from_unmapped_memory_ds); - TEST(test_memory_init_from_unmapped_memory_wii); - - TEST(test_memory_init_from_memory_map); - TEST(test_memory_init_from_memory_map_null_filler); - TEST(test_memory_init_from_memory_map_no_save_ram); - TEST(test_memory_init_from_memory_map_merge_neighbors); - TEST(test_memory_init_from_memory_map_no_ram); - TEST(test_memory_init_from_memory_map_splice); - TEST(test_memory_init_from_memory_map_mirrored); - TEST(test_memory_init_from_memory_map_out_of_order); - TEST(test_memory_init_from_memory_map_disconnect_gaps); - - /* rc_libretro_memory_read */ - TEST(test_memory_read) - - /* rc_libretro_hash_set_t */ - TEST(test_hash_set_add_single); - TEST(test_hash_set_update_single); - TEST(test_hash_set_add_many); - - TEST(test_hash_set_m3u_single); - TEST(test_hash_set_m3u_savedisk); - TEST(test_hash_set_m3u_savedisk_volume_label); - TEST(test_hash_set_m3u_savedisk_multiple_with_comments_and_whitespace); - TEST(test_hash_set_m3u_savedisk_no_core_support); - - TEST_SUITE_END(); -} diff --git a/src/rcheevos/test/test_types.natvis b/src/rcheevos/test/test_types.natvis deleted file mode 100644 index 2721f39985..0000000000 --- a/src/rcheevos/test/test_types.natvis +++ /dev/null @@ -1,9 +0,0 @@ - - - - - ((__rc_memref_list_t*)&memrefs) - ((__rc_value_list_t*)&variables) - - - diff --git a/src/rcheevos/validator/Makefile b/src/rcheevos/validator/Makefile deleted file mode 100644 index 22d6e06461..0000000000 --- a/src/rcheevos/validator/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -RC_SRC=../src -RC_CHEEVOS_SRC=$(RC_SRC)/rcheevos -RC_HASH_SRC=$(RC_SRC)/rhash -RC_API_SRC=$(RC_SRC)/rapi - -OBJ=$(RC_CHEEVOS_SRC)/alloc.o $(RC_CHEEVOS_SRC)/condition.o $(RC_CHEEVOS_SRC)/condset.o \ - $(RC_CHEEVOS_SRC)/consoleinfo.o $(RC_CHEEVOS_SRC)/format.o $(RC_CHEEVOS_SRC)/lboard.o \ - $(RC_CHEEVOS_SRC)/memref.o $(RC_CHEEVOS_SRC)/operand.o $(RC_CHEEVOS_SRC)/rc_validate.o \ - $(RC_CHEEVOS_SRC)/richpresence.o $(RC_CHEEVOS_SRC)/runtime.o $(RC_CHEEVOS_SRC)/trigger.o \ - $(RC_CHEEVOS_SRC)/value.o \ - $(RC_SRC)/rc_compat.o $(RC_SRC)/rc_util.o \ - $(RC_HASH_SRC)/md5.o \ - $(RC_API_SRC)/rc_api_common.o $(RC_API_SRC)/rc_api_runtime.o - validator.o - -all: validator - -%.o: %.c - gcc -Wall -O0 -g -std=c89 -ansi -Wno-long-long -I../include -I$(RC_CHEEVOS_SRC) -c $< -o $@ - -validator: $(OBJ) - gcc -o $@ $+ -lm - -clean: - rm -f test $(OBJ) diff --git a/src/rcheevos/validator/validator.c b/src/rcheevos/validator/validator.c deleted file mode 100644 index c984533595..0000000000 --- a/src/rcheevos/validator/validator.c +++ /dev/null @@ -1,658 +0,0 @@ -#include "rc_internal.h" -#include "rc_api_runtime.h" -#include "rc_consoles.h" -#include "rc_validate.h" - -#include -#include -#include -#include -#include /* memset */ -#include - -#ifdef _MSC_VER /* windows build */ - #define WIN32_LEAN_AND_MEAN - #include -#else - #include - #include - #define stricmp strcasecmp -#endif - -/* usage exmaple: - * - * ./validator.exe d "E:\RetroAchievements\dump" | sort > results.txt - * grep -v ": OK" results.txt | grep -v "File: " | grep . - */ - -static const char* flag_string(char type) { - switch (type) { - case RC_CONDITION_STANDARD: return ""; - case RC_CONDITION_PAUSE_IF: return "PauseIf "; - case RC_CONDITION_RESET_IF: return "ResetIf "; - case RC_CONDITION_MEASURED_IF: return "MeasuredIf "; - case RC_CONDITION_TRIGGER: return "Trigger "; - case RC_CONDITION_MEASURED: return "Measured "; - case RC_CONDITION_ADD_SOURCE: return "AddSource "; - case RC_CONDITION_SUB_SOURCE: return "SubSource "; - case RC_CONDITION_REMEMBER: return "Remember "; - case RC_CONDITION_ADD_ADDRESS: return "AddAddress "; - case RC_CONDITION_ADD_HITS: return "AddHits "; - case RC_CONDITION_SUB_HITS: return "SubHits "; - case RC_CONDITION_RESET_NEXT_IF: return "ResetNextIf "; - case RC_CONDITION_AND_NEXT: return "AndNext "; - case RC_CONDITION_OR_NEXT: return "OrNext "; - default: return "Unknown"; - } -} - -static const char* type_string(char type) { - switch (type) { - case RC_OPERAND_ADDRESS: return "Mem"; - case RC_OPERAND_DELTA: return "Delta"; - case RC_OPERAND_CONST: return "Value"; - case RC_OPERAND_FP: return "Float"; - case RC_OPERAND_FUNC: return "Func"; - case RC_OPERAND_PRIOR: return "Prior"; - case RC_OPERAND_BCD: return "BCD"; - case RC_OPERAND_INVERTED: return "Inverted"; - case RC_OPERAND_RECALL: return "Recall"; - default: return "Unknown"; - } -} - -static const char* size_string(char size) { - switch (size) { - case RC_MEMSIZE_8_BITS: return "8-bit"; - case RC_MEMSIZE_16_BITS: return "16-bit"; - case RC_MEMSIZE_24_BITS: return "24-bit"; - case RC_MEMSIZE_32_BITS: return "32-bit"; - case RC_MEMSIZE_LOW: return "Lower4"; - case RC_MEMSIZE_HIGH: return "Upper4"; - case RC_MEMSIZE_BIT_0: return "Bit0"; - case RC_MEMSIZE_BIT_1: return "Bit1"; - case RC_MEMSIZE_BIT_2: return "Bit2"; - case RC_MEMSIZE_BIT_3: return "Bit3"; - case RC_MEMSIZE_BIT_4: return "Bit4"; - case RC_MEMSIZE_BIT_5: return "Bit5"; - case RC_MEMSIZE_BIT_6: return "Bit6"; - case RC_MEMSIZE_BIT_7: return "Bit7"; - case RC_MEMSIZE_BITCOUNT: return "BitCount"; - case RC_MEMSIZE_16_BITS_BE: return "16-bit BE"; - case RC_MEMSIZE_24_BITS_BE: return "24-bit BE"; - case RC_MEMSIZE_32_BITS_BE: return "32-bit BE"; - case RC_MEMSIZE_VARIABLE: return "Variable"; - default: return "Unknown"; - } -} - -static const char* operator_string(char oper) { - switch (oper) { - case RC_OPERATOR_NONE: return ""; - case RC_OPERATOR_AND: return "&"; - case RC_OPERATOR_XOR: return "^"; - case RC_OPERATOR_MULT: return "*"; - case RC_OPERATOR_DIV: return "/"; - case RC_OPERATOR_MOD: return "%"; - case RC_OPERATOR_ADD: return "+"; - case RC_OPERATOR_SUB: return "-"; - case RC_OPERATOR_EQ: return "="; - case RC_OPERATOR_NE: return "!="; - case RC_OPERATOR_GE: return ">="; - case RC_OPERATOR_GT: return ">"; - case RC_OPERATOR_LE: return "<="; - case RC_OPERATOR_LT: return "<"; - default: return "?"; - } -} - -static void append_condition(char result[], size_t result_size, const rc_condition_t* cond) { - const char* flag = flag_string(cond->type); - const char* src_type = type_string(cond->operand1.type); - const char* tgt_type = type_string(cond->operand2.type); - const char* cmp = operator_string(cond->oper); - char val1[32], val2[32]; - - if (rc_operand_is_memref(&cond->operand1)) - snprintf(val1, sizeof(val1), "%s 0x%06x", size_string(cond->operand1.size), cond->operand1.value.memref->address); - else - snprintf(val1, sizeof(val1), "0x%06x", cond->operand1.value.num); - - if (rc_operand_is_memref(&cond->operand2)) - snprintf(val2, sizeof(val2), "%s 0x%06x", size_string(cond->operand2.size), cond->operand2.value.memref->address); - else - snprintf(val2, sizeof(val2), "0x%06x", cond->operand2.value.num); - - const size_t message_len = strlen(result); - result += message_len; - result_size -= message_len; - - if (cond->oper == RC_OPERATOR_NONE) - snprintf(result, result_size, ": %s%s %s", flag, src_type, val1); - else - snprintf(result, result_size, ": %s%s %s %s %s %s", flag, src_type, val1, cmp, tgt_type, val2); -} - -static void append_invalid_condition(char result[], size_t result_size, const rc_condset_t* condset) { - if (strncmp(result, "Condition ", 10) == 0) { - int index = atoi(&result[10]); - const rc_condition_t* cond; - for (cond = condset->conditions; cond; cond = cond->next) { - if (--index == 0) { - const size_t error_length = strlen(result); - append_condition(result + error_length, result_size - error_length, cond); - return; - } - } - } -} - -static void append_invalid_trigger_condition(char result[], size_t result_size, const rc_trigger_t* trigger) { - if (strncmp(result, "Alt", 3) == 0) { - int index = atoi(&result[3]); - const rc_condset_t* condset; - for (condset = trigger->alternative; condset; condset = condset->next) { - if (--index == 0) { - result += 4; - result_size -= 4; - while (isdigit(*result)) { - ++result; - --result_size; - } - ++result; - --result_size; - append_invalid_condition(result, result_size, condset); - return; - } - } - } - else if (strncmp(result, "Core ", 5) == 0) { - append_invalid_condition(result + 5, result_size - 5, trigger->requirement); - } - else { - append_invalid_condition(result, result_size, trigger->requirement); - } -} - -static int validate_trigger(const char* trigger, char result[], size_t result_size, uint32_t console_id) { - char* buffer; - rc_trigger_t* compiled; - int success = 0; - - int ret = rc_trigger_size(trigger); - if (ret < 0) { - snprintf(result, result_size, "%s", rc_error_str(ret)); - return 0; - } - - buffer = (char*)malloc(ret + 4); - if (!buffer) { - snprintf(result, result_size, "malloc failed"); - return 0; - } - - memset(buffer + ret, 0xCD, 4); - compiled = rc_parse_trigger(buffer, trigger, NULL, 0); - if (compiled == NULL) { - snprintf(result, result_size, "parse failed"); - } - else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { - snprintf(result, result_size, "write past end of buffer"); - } - else if (rc_validate_trigger_for_console(compiled, result, result_size, console_id)) { - snprintf(result, result_size, "%d OK", ret); - success = 1; - } - else { - append_invalid_trigger_condition(result, result_size, compiled); - } - - free(buffer); - return success; -} - -static int validate_leaderboard(const char* leaderboard, char result[], const size_t result_size, uint32_t console_id) -{ - char* buffer; - rc_lboard_t* compiled; - int success = 0; - - int ret = rc_lboard_size(leaderboard); - if (ret < 0) { - /* generic problem parsing the leaderboard, attempt to report where */ - const char* start = leaderboard; - char part[4] = { 0,0,0,0 }; - do { - char* next = strstr(start, "::"); - part[0] = toupper((int)start[0]); - part[1] = toupper((int)start[1]); - part[2] = toupper((int)start[2]); - start += 4; - - if (strcmp(part, "VAL") == 0) { - int ret2 = rc_value_size(start); - if (ret2 == ret) { - snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); - return 0; - } - } - else { - int ret2 = rc_trigger_size(start); - if (ret2 == ret) { - snprintf(result, result_size, "%s: %s", part, rc_error_str(ret)); - return 0; - } - } - - if (!next) - break; - - start = next + 2; - } while (1); - - snprintf(result, result_size, "%s", rc_error_str(ret)); - return 0; - } - - buffer = (char*)malloc(ret + 4); - if (!buffer) { - snprintf(result, result_size, "malloc failed"); - return 0; - } - - memset(buffer + ret, 0xCD, 4); - compiled = rc_parse_lboard(buffer, leaderboard, NULL, 0); - if (compiled == NULL) { - snprintf(result, result_size, "parse failed"); - } - else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { - snprintf(result, result_size, "write past end of buffer"); - } - else { - snprintf(result, result_size, "STA: "); - success = rc_validate_trigger_for_console(&compiled->start, result + 5, result_size - 5, console_id); - if (!success) { - append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->start); - } - else { - snprintf(result, result_size, "SUB: "); - success = rc_validate_trigger_for_console(&compiled->submit, result + 5, result_size - 5, console_id); - if (!success) { - append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->submit); - } - else { - snprintf(result, result_size, "CAN: "); - success = rc_validate_trigger_for_console(&compiled->cancel, result + 5, result_size - 5, console_id); - - if (!success) { - append_invalid_trigger_condition(result + 5, result_size - 5, &compiled->cancel); - } - else { - snprintf(result, result_size, "%d OK", ret); - } - } - } - } - - free(buffer); - return success; -} - -static int validate_macros(const rc_richpresence_t* richpresence, const char* script, char result[], const size_t result_size) -{ - const uint16_t RC_FORMAT_UNKNOWN_MACRO = 103; /* enum not exposed by header */ - - rc_richpresence_display_t* display = richpresence->first_display; - while (display != NULL) { - rc_richpresence_display_part_t* part = display->display; - while (part != NULL) { - if (part->display_type == RC_FORMAT_UNKNOWN_MACRO) { - /* include opening parenthesis to prevent partial match */ - size_t macro_len = strchr(part->text, '(') - part->text + 1; - - /* find the display portion of the script */ - const char* ptr = script; - int line = 1; - while (strncmp(ptr, "Display:", 8) != 0) { - while (*ptr != '\n') - ++ptr; - - ++line; - ++ptr; - } - - /* find the first matching reference to the unknown macro */ - do { - while (*ptr != '@') { - if (*ptr == '\n') - ++line; - - if (*ptr == '\0') { - /* unexpected, but prevent potential infinite loop */ - snprintf(result, result_size, "Unknown macro \"%.*s\"", (int)(macro_len - 1), part->text); - return 0; - } - - ++ptr; - } - ++ptr; - - if (strncmp(ptr, part->text, macro_len) == 0) { - snprintf(result, result_size, "Line %d: Unknown macro \"%.*s\"", line, (int)(macro_len - 1), part->text); - return 0; - } - } while (1); - } - - part = part->next; - } - - display = display->next; - } - - return 1; -} - -static int validate_richpresence(const char* script, char result[], const size_t result_size, uint32_t console_id) -{ - char* buffer; - rc_richpresence_t* compiled; - int lines; - int success = 0; - - int ret = rc_richpresence_size_lines(script, &lines); - if (ret < 0) { - snprintf(result, result_size, "Line %d: %s", lines, rc_error_str(ret)); - return 0; - } - - buffer = (char*)malloc(ret + 4); - if (!buffer) { - snprintf(result, result_size, "malloc failed"); - return 0; - } - - memset(buffer + ret, 0xCD, 4); - compiled = rc_parse_richpresence(buffer, script, NULL, 0); - if (compiled == NULL) { - snprintf(result, result_size, "parse failed"); - } - else if (*(unsigned*)&buffer[ret] != 0xCDCDCDCD) { - snprintf(result, result_size, "write past end of buffer"); - } - else { - const rc_richpresence_display_t* display; - int index = 1; - for (display = compiled->first_display; display; display = display->next) { - const size_t prefix_length = snprintf(result, result_size, "Display%d: ", index++); - success = rc_validate_trigger_for_console(&display->trigger, result + prefix_length, result_size - prefix_length, console_id); - if (!success) - break; - } - - if (success) - success = rc_validate_memrefs_for_console(rc_richpresence_get_memrefs(compiled), result, result_size, console_id); - if (success) - success = validate_macros(compiled, script, result, result_size); - if (success) - snprintf(result, result_size, "%d OK", ret); - } - - free(buffer); - return success; -} - -static void validate_richpresence_file(const char* richpresence_file, char result[], const size_t result_size) -{ - char* file_contents; - size_t file_size; - FILE* file; - - fopen_s(&file, richpresence_file, "rb"); - if (!file) { - snprintf(result, result_size, "could not open file"); - return; - } - - fseek(file, 0, SEEK_END); - file_size = ftell(file); - fseek(file, 0, SEEK_SET); - - file_contents = (char*)malloc(file_size + 1); - if (!file_contents) { - snprintf(result, result_size, "malloc failed"); - return; - } - - fread(file_contents, 1, file_size, file); - file_contents[file_size] = '\0'; - fclose(file); - - validate_richpresence(file_contents, result, result_size, 0); - - free(file_contents); -} - -static int validate_patchdata_file(const char* patchdata_file, const char* filename, int errors_only) { - char* file_contents; - size_t file_size; - FILE* file; - rc_api_fetch_game_data_response_t fetch_game_data_response; - int result; - size_t i; - char file_title[256]; - char buffer[256]; - int success = 1; - - fopen_s(&file, patchdata_file, "rb"); - if (!file) { - printf("File: %s: could not open file\n", filename); - return 0; - } - - fseek(file, 0, SEEK_END); - file_size = ftell(file); - fseek(file, 0, SEEK_SET); - - file_contents = (char*)malloc(file_size + 1); - fread(file_contents, 1, file_size, file); - file_contents[file_size] = '\0'; - fclose(file); - - /* rc_api_process_fetch_game_data_response expects the PatchData to be - * a subobject, but the DLL strips that when writing to the RACache. - * if it looks like the nested object, wrap it again */ - if (strncmp(file_contents, "{\"ID\":", 6) == 0) { - char* expanded_contents = (char*)malloc(file_size + 15); - memcpy(expanded_contents, "{\"PatchData\":", 13); - memcpy(&expanded_contents[13], file_contents, file_size); - expanded_contents[file_size + 13] = '}'; - expanded_contents[file_size + 14] = '\0'; - - free(file_contents); - file_contents = expanded_contents; - } - - result = rc_api_process_fetch_game_data_response(&fetch_game_data_response, file_contents); - if (result != RC_OK) { - if (fetch_game_data_response.response.error_message) - printf("File: %s: %s\n", filename, fetch_game_data_response.response.error_message); - else - printf("File: %s: %s\n", filename, rc_error_str(result)); - return 0; - } - - free(file_contents); - - snprintf(file_title, sizeof(file_title), "File: %s: %s\n", filename, fetch_game_data_response.title); - - if (fetch_game_data_response.rich_presence_script && *fetch_game_data_response.rich_presence_script) { - result = validate_richpresence(fetch_game_data_response.rich_presence_script, - buffer, sizeof(buffer), fetch_game_data_response.console_id); - success &= result; - - if (!result || !errors_only) { - printf("%s", file_title); - file_title[0] = '\0'; - - printf(" rich presence %d: %s\n", fetch_game_data_response.id, buffer); - } - } - - for (i = 0; i < fetch_game_data_response.num_achievements; ++i) { - const char* trigger = fetch_game_data_response.achievements[i].definition; - result = validate_trigger(trigger, buffer, sizeof(buffer), fetch_game_data_response.console_id); - success &= result; - - if (!result || !errors_only) { - if (file_title[0]) { - printf("%s", file_title); - file_title[0] = '\0'; - } - - printf(" achievement %d%s: %s\n", fetch_game_data_response.achievements[i].id, - (fetch_game_data_response.achievements[i].category == 3) ? "" : " (Unofficial)", buffer); - } - } - - for (i = 0; i < fetch_game_data_response.num_leaderboards; ++i) { - result = validate_leaderboard(fetch_game_data_response.leaderboards[i].definition, - buffer, sizeof(buffer), fetch_game_data_response.console_id); - success &= result; - - if (!result || !errors_only) { - if (file_title[0]) { - printf("%s", file_title); - file_title[0] = '\0'; - } - - printf(" leaderboard %d: %s\n", fetch_game_data_response.leaderboards[i].id, buffer); - } - } - - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); - - return success; -} - -#ifdef _MSC_VER -static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { - WIN32_FIND_DATA fdFile; - HANDLE hFind = NULL; - int need_newline = 0; - - char filename[MAX_PATH]; - snprintf(filename, sizeof(filename), "%s\\*.json", patchdata_directory); - - if ((hFind = FindFirstFile(filename, &fdFile)) == INVALID_HANDLE_VALUE) { - printf("failed to open directory"); - return; - } - - do - { - if (!(fdFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { - if (need_newline) { - printf("\n"); - need_newline = 0; - } - - snprintf(filename, sizeof(filename), "%s\\%s", patchdata_directory, fdFile.cFileName); - if (!validate_patchdata_file(filename, fdFile.cFileName, errors_only) || !errors_only) - need_newline = 1; - } - } while(FindNextFile(hFind, &fdFile)); - - FindClose(hFind); -} -#else -static void validate_patchdata_directory(const char* patchdata_directory, int errors_only) { - struct dirent* entry; - char* filename; - size_t filename_len; - char path[2048]; - int need_newline = 0; - - DIR* dir = opendir(patchdata_directory); - if (!dir) { - printf("failed to open directory"); - return; - } - - while ((entry = readdir(dir)) != NULL) { - filename = entry->d_name; - filename_len = strlen(filename); - if (filename_len > 5 && stricmp(&filename[filename_len - 5], ".json") == 0) { - if (need_newline) { - printf("\n"); - need_newline = 0; - } - - sprintf(path, "%s/%s", patchdata_directory, filename); - if (!validate_patchdata_file(path, filename, errors_only) || !errors_only) - need_newline = 1; - } - } - - closedir(dir); -} -#endif - -static int usage() { - printf("validator [type] [data]\n" - "\n" - "where [type] is one of the following:\n" - " a achievement, [data] = trigger definition\n" - " l leaderboard, [data] = leaderboard definition\n" - " r rich presence, [data] = path to rich presence script\n" - " f patchdata file, [data] = path to patchdata json file\n" - " d patchdata directory, [data] = path to directory containing one or more patchdata json files\n" - " e same as 'd', but only reports errors\n" - ); - - return 0; -} - -int main(int argc, char* argv[]) { - char buffer[256]; - - if (argc < 3) - return usage(); - - switch (argv[1][0]) - { - case 'a': - validate_trigger(argv[2], buffer, sizeof(buffer), 0); - printf("Achievement: %s\n", buffer); - break; - - case 'l': - validate_leaderboard(argv[2], buffer, sizeof(buffer), 0); - printf("Leaderboard: %s\n", buffer); - break; - - case 'r': - validate_richpresence_file(argv[2], buffer, sizeof(buffer)); - printf("Rich Presence: %s\n", buffer); - break; - - case 'f': - validate_patchdata_file(argv[2], argv[2], 0); - break; - - case 'd': - printf("Directory: %s:\n", argv[2]); - validate_patchdata_directory(argv[2], 0); - break; - - case 'e': - printf("Directory: %s:\n", argv[2]); - validate_patchdata_directory(argv[2], 1); - break; - - default: - return usage(); - } - - return 0; -} diff --git a/src/rcheevos/validator/validator.vcxproj b/src/rcheevos/validator/validator.vcxproj deleted file mode 100644 index c108bea51a..0000000000 --- a/src/rcheevos/validator/validator.vcxproj +++ /dev/null @@ -1,152 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 15.0 - {16FABFA7-A2EC-4CD0-9E04-50315A2BB613} - rcheevostest - Application - MultiByte - v143 - - - v142 - - - v141 - - - - true - - - false - true - - - true - - - false - true - - - - - - - - - - - - - - - - - - - - - - - Level3 - Disabled - true - true - $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - - - Console - - - - - Level3 - Disabled - true - true - $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - - - Console - - - - - Level3 - MaxSpeed - true - true - true - true - $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - - - true - true - Console - - - - - Level3 - MaxSpeed - true - true - true - true - $(ProjectDir)..\include;$(ProjectDir)..\src\rcheevos - - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/rcheevos/validator/validator.vcxproj.filters b/src/rcheevos/validator/validator.vcxproj.filters deleted file mode 100644 index 52206787d6..0000000000 --- a/src/rcheevos/validator/validator.vcxproj.filters +++ /dev/null @@ -1,82 +0,0 @@ - - - - - {125bb837-6e5b-4a66-a607-e2250b31f108} - - - {DD82F9EB-1066-40C6-9568-D5C7EEB2A49B} - - - {efdc689a-471b-432e-8ecb-dde3a3202949} - - - {8570c19e-e882-409f-84d8-b87887371c26} - - - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - src\rcheevos - - - - src\rhash - - - src\rapi - - - src\rapi - - - src\rcheevos - - - src\rcheevos - - - src - - - src - - - - - src\rcheevos - - - src\rcheevos - - - \ No newline at end of file From 2d951aa95e9b451945ac0524363ab476b970346f Mon Sep 17 00:00:00 2001 From: PanMenel <90268767+PanMenel@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:38:00 +0100 Subject: [PATCH 41/41] mainly revert changes from my other fork --- src/frontend/qt_sdl/Config.cpp | 2 -- src/frontend/qt_sdl/EmuInstance.h | 1 - src/frontend/qt_sdl/EmuInstanceInput.cpp | 1 - src/frontend/qt_sdl/EmuThread.cpp | 3 --- src/frontend/qt_sdl/EmuThread.h | 1 - .../qt_sdl/InputConfig/InputConfigDialog.h | 2 -- src/frontend/qt_sdl/Window.cpp | 14 -------------- src/frontend/qt_sdl/Window.h | 1 - src/frontend/qt_sdl/toast/ToastManager.cpp | 1 - 9 files changed, 26 deletions(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 816cf8b766..0b73fef0f4 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -171,7 +171,6 @@ LegacyEntry LegacyFile[] = {"HKKey_FastForward", 0, "Keyboard.HK_FastForward", true}, {"HKKey_FastForwardToggle", 0, "Keyboard.HK_FrameLimitToggle", true}, {"HKKey_FullscreenToggle", 0, "Keyboard.HK_FullscreenToggle", true}, - {"HKKey_MenuBarToggle", 0, "Keyboard.HK_MenuBarToggle", true}, {"HKKey_SwapScreens", 0, "Keyboard.HK_SwapScreens", true}, {"HKKey_SwapScreenEmphasis", 0, "Keyboard.HK_SwapScreenEmphasis", true}, {"HKKey_SolarSensorDecrease", 0, "Keyboard.HK_SolarSensorDecrease", true}, @@ -192,7 +191,6 @@ LegacyEntry LegacyFile[] = {"HKJoy_FastForward", 0, "Joystick.HK_FastForward", true}, {"HKJoy_FastForwardToggle", 0, "Joystick.HK_FrameLimitToggle", true}, {"HKJoy_FullscreenToggle", 0, "Joystick.HK_FullscreenToggle", true}, - {"HKJoy_MenuBarToggle", 0, "Joystick.HK_MenuBarToggle", true}, {"HKJoy_SwapScreens", 0, "Joystick.HK_SwapScreens", true}, {"HKJoy_SwapScreenEmphasis", 0, "Joystick.HK_SwapScreenEmphasis", true}, {"HKJoy_SolarSensorDecrease", 0, "Joystick.HK_SolarSensorDecrease", true}, diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index 55b413a8ba..6b8325a1c0 100755 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -59,7 +59,6 @@ enum HK_GuitarGripRed, HK_GuitarGripYellow, HK_GuitarGripBlue, - HK_MenuBarToggle, HK_MAX }; diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index c26d0e6e1f..866eb29ab5 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -52,7 +52,6 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_FastForward", "HK_FrameLimitToggle", "HK_FullscreenToggle", - "HK_MenuBarToggle", "HK_SwapScreens", "HK_SwapScreenEmphasis", "HK_SolarSensorDecrease", diff --git a/src/frontend/qt_sdl/EmuThread.cpp b/src/frontend/qt_sdl/EmuThread.cpp index 44843f349e..77ae458e25 100755 --- a/src/frontend/qt_sdl/EmuThread.cpp +++ b/src/frontend/qt_sdl/EmuThread.cpp @@ -81,7 +81,6 @@ void EmuThread::attachWindow(MainWindow* window) connect(this, SIGNAL(windowEmuReset()), window, SLOT(onEmuReset())); connect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); connect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); - connect(this, SIGNAL(windowMenuBarToggle()), window, SLOT(onMenuBarToggled())); connect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); if (window->winHasMenu()) @@ -100,7 +99,6 @@ void EmuThread::detachWindow(MainWindow* window) disconnect(this, SIGNAL(windowEmuReset()), window, SLOT(onEmuReset())); disconnect(this, SIGNAL(autoScreenSizingChange(int)), window->panel, SLOT(onAutoScreenSizingChanged(int))); disconnect(this, SIGNAL(windowFullscreenToggle()), window, SLOT(onFullscreenToggled())); - disconnect(this, SIGNAL(windowMenuBarToggle()), window, SLOT(onMenuBarToggled())); disconnect(this, SIGNAL(screenEmphasisToggle()), window, SLOT(onScreenEmphasisToggled())); if (window->winHasMenu()) @@ -171,7 +169,6 @@ void EmuThread::run() if (emuInstance->hotkeyPressed(HK_FrameStep)) emuFrameStep(); if (emuInstance->hotkeyPressed(HK_FullscreenToggle)) emit windowFullscreenToggle(); - if (emuInstance->hotkeyPressed(HK_MenuBarToggle)) emit windowMenuBarToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreens)) emit swapScreensToggle(); if (emuInstance->hotkeyPressed(HK_SwapScreenEmphasis)) emit screenEmphasisToggle(); diff --git a/src/frontend/qt_sdl/EmuThread.h b/src/frontend/qt_sdl/EmuThread.h index 8462ee91f5..7a7f060052 100755 --- a/src/frontend/qt_sdl/EmuThread.h +++ b/src/frontend/qt_sdl/EmuThread.h @@ -157,7 +157,6 @@ class EmuThread : public QThread void autoScreenSizingChange(int sizing); void windowFullscreenToggle(); - void windowMenuBarToggle(); void swapScreensToggle(); void screenEmphasisToggle(); diff --git a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h index dcefc76d3d..0dd384429f 100644 --- a/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h +++ b/src/frontend/qt_sdl/InputConfig/InputConfigDialog.h @@ -61,7 +61,6 @@ static constexpr std::initializer_list hk_general = HK_SlowMoToggle, HK_FrameLimitToggle, HK_FullscreenToggle, - HK_MenuBarToggle, HK_Lid, HK_Mic, HK_SwapScreens, @@ -82,7 +81,6 @@ static constexpr std::initializer_list hk_general_labels = "Toggle slow mo", "Toggle FPS limit", "Toggle fullscreen", - "Toggle menu bar", "Close/open lid", "Microphone", "Swap screens", diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 4de23dfd5b..b0785ac5fa 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -2405,20 +2405,6 @@ void MainWindow::onFullscreenToggled() toggleFullscreen(); } -void MainWindow::onMenuBarToggled() -{ - if (!hasMenu) return; - if (menuBar()->maximumHeight() != 0) - { - menuBar()->setFixedHeight(0); - } - else - { - int menuBarHeight = menuBar()->sizeHint().height(); - menuBar()->setFixedHeight(menuBarHeight); - } -} - void MainWindow::onScreenEmphasisToggled() { int currentSizing = windowCfg.GetInt("ScreenSizing"); diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index 5b0e719a33..525cdcad3c 100755 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -259,7 +259,6 @@ private slots: void onUpdateVideoSettings(bool glchange); void onFullscreenToggled(); - void onMenuBarToggled(); void onScreenEmphasisToggled(); private: diff --git a/src/frontend/qt_sdl/toast/ToastManager.cpp b/src/frontend/qt_sdl/toast/ToastManager.cpp index 3118baed04..ebe0ba3e4e 100644 --- a/src/frontend/qt_sdl/toast/ToastManager.cpp +++ b/src/frontend/qt_sdl/toast/ToastManager.cpp @@ -21,7 +21,6 @@ void ToastManager::Init(QWidget* widget) void ToastManager::ShowAchievement(const QString& title, const QString& description, const QPixmap& icon) { if (!m_overlay) { - printf("ToastManager: Próba pokazania toasta przed Init()!\n"); return; }