diff --git a/.agents/skills/hayday-zoom-injector/SKILL.md b/.agents/skills/hayday-zoom-injector/SKILL.md new file mode 100644 index 0000000..011786f --- /dev/null +++ b/.agents/skills/hayday-zoom-injector/SKILL.md @@ -0,0 +1,64 @@ +--- +name: hayday-zoom-injector +description: >- + Automated tool and runbook to extract, modify, and inject custom zoom levels, view distances, + and synchronized fingerprint.json hashes into Supercell's Hay Day client on Android emulators. +--- + +# Hay Day Zoom Injector Skill + +Use this skill whenever Hay Day updates to a new version (e.g., 1.72.x -> 1.73.x) or when `premium.exe` requires updated camera zoom overrides (`MinZoom`, `DefaultZoom`, `MaxZoom`). + +--- + +## 1. When to Use This Skill + +* **After a Game Update:** Hay Day updates wipe or update `fingerprint.json` and change CSV schemas. +* **Black Screen / Zoom Reversion:** When `game_config.csv` is rejected by Supercell's dynamic asset validator. +* **New Emulator Setup:** Initializing `injecthacks/` and the emulator's `/update/` directory. + +--- + +## 2. Automated Runbook + +### Step 1: Run the Universal Zoom Injector Tool +Point the tool to the new game APK or APKM bundle and the active emulator: + +```powershell +& "D:\02_Projects\Hayday-Bot-Pro\.venv\Scripts\python.exe" tools/hayday_zoom_injector.py ` + --apkm "D:\Path\To\HayDay_new_version.apkm" ` + --device "127.0.0.1:21503" ` + --min-zoom 30 ` + --default-zoom 10 ` + --max-zoom 170 +``` + +### Step 2: What the Tool Automatically Performs +1. **Extracts** `assets/data/game_config.csv` from the official game split asset pack. +2. **Patches** `MinZoom: 30`, `DefaultZoom: 10`, and `MaxZoom: 170` (and mobile equivalents). +3. **Calculates** the new SHA-1 cryptographic hash of the modified CSV. +4. **Pushes** the file to `HD/x64/Release/injecthacks/game_config.csv` for `premium.exe`. +5. **Synchronizes** the new SHA-1 hash into `/data/data/com.supercell.hayday/update/fingerprint.json` on the device. +6. **Sets Permissions** (`chmod 777`) on the remote update directory. + +--- + +## 3. Verification Commands + +Verify that the patched file and fingerprint are aligned on the emulator: + +```powershell +adb -s 127.0.0.1:21503 shell "ls -la /data/data/com.supercell.hayday/update/data/game_config.csv" +adb -s 127.0.0.1:21503 shell "cat /data/data/com.supercell.hayday/update/fingerprint.json | grep -o '\"file\":\"data/game_config.csv\",\"sha\":\"[^\"]*\"'" +``` + +--- + +## 4. Manual Fallback Steps + +If running without the CLI script: +1. Open the APK with 7-Zip or Python `zipfile`. +2. Extract `assets/data/game_config.csv`. +3. Modify line `MinZoom,50,,` to `MinZoom,30,,`. +4. Calculate SHA-1: `Get-FileHash -Algorithm SHA1 game_config.csv`. +5. Pull `/data/data/com.supercell.hayday/update/fingerprint.json`, replace the `"sha"` value under `"data/game_config.csv"`, and push both files back with `chmod 777`. diff --git a/.gitignore b/.gitignore index 38df1ed..4151541 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,7 @@ cow/ *.apk *.idsig +# Temporary analysis screenshots and VM test boots +network_study/android/*.png +network_study/android/magisk_memu_boot/ + diff --git a/HD/BotEngine.cpp b/HD/BotEngine.cpp index 008d523..ca15f30 100644 --- a/HD/BotEngine.cpp +++ b/HD/BotEngine.cpp @@ -335,7 +335,14 @@ static bool DismissUnknownOverlay(int instanceId, const char* reason) { } static bool IsSiloFullPopupVisible(const cv::Mat& screen, MatchResult* outPopup = nullptr) { - MatchResult popup = FindImage(screen, silo_full_templatePath, 0.75f, false, 1.0f, false); + if (screen.empty()) return false; + MatchResult popup = FindImage(screen, silo_full_templatePath, 0.70f, false, 1.0f, false); + if (!popup.found) { + popup = FindImage(screen, "templates\\silo_full_banner.png", 0.70f, false, 1.0f, false); + } + if (!popup.found) { + popup = FindImage(screen, silo_full_cross_templatePath, std::max(0.65f, g_Thresholds.siloFullCrossThreshold), false, 1.0f, false); + } if (outPopup) *outPopup = popup; return popup.found; } @@ -505,11 +512,25 @@ static float GetSaleProductThresholdForCrop(int cropMode) { static MatchResult FindShopIconPreferCache(const cv::Mat& screen, AccountSlot& account) { MatchResult shopRes{ false, 0, 0, 0.0 }; + auto isInvalidShopPoint = [&](int x, int y) { + if (x < 120 && y < 100) return true; // Top-left HUD (Level star / coins) + if (y > screen.rows - 90) return true; // Bottom HUD (Buttons / tools) + return false; + }; + if (account.cachedShopX >= 0 && account.cachedShopY >= 0) { shopRes = FindTemplateNearCachedPoint(screen, account.cachedShopX, account.cachedShopY, 90, 90, shop_templatePath, g_Thresholds.shopThreshold); + if (shopRes.found && isInvalidShopPoint(shopRes.x, shopRes.y)) { + shopRes = MatchResult{ false, 0, 0, 0.0 }; + account.cachedShopX = -1; + account.cachedShopY = -1; + } } if (!shopRes.found) { shopRes = FindImage(screen, shop_templatePath, g_Thresholds.shopThreshold); + if (shopRes.found && isInvalidShopPoint(shopRes.x, shopRes.y)) { + shopRes = MatchResult{ false, 0, 0, 0.0 }; + } } if (shopRes.found) { account.cachedShopX = shopRes.x; @@ -2716,9 +2737,6 @@ bool AutoDetectTouchDevice(int instanceId) { void ExecuteDenseGridGesture(int instanceId, int startX, int startY, const std::vector& fields) { if (fields.empty()) return; - std::string gestureDetail = "Start=(" + std::to_string(startX) + "," + std::to_string(startY) + "), TargetFields=" + std::to_string(fields.size()); - AppendActionDebugLog(instanceId, "SWIPE_GRID", "MinitouchSweep", gestureDetail); - AddLog(instanceId, "[INFO] Executing Sweep...", ImVec4(0.8f, 0.4f, 1.0f, 1.0f)); // 1. FIND FIELD'S BORDER SO THE BOT CAN DRAG IT TO THE CORNER OF THE FIELDS. @@ -2730,14 +2748,13 @@ void ExecuteDenseGridGesture(int instanceId, int startX, int startY, const std:: if (f.y > maxY) maxY = f.y; } - int marginX = 80; int marginY = 80; - int targetMinX = std::max(0, minX - marginX); - int targetMaxX = std::min(1280, maxX + marginX); - int targetMinY = std::max(0, minY - marginY); - int targetMaxY = std::min(720, maxY + marginY); + int targetMinX = (std::max)(0, minX - marginX); + int targetMaxX = (std::min)(640, maxX + marginX); + int targetMinY = (std::max)(0, minY - marginY); + int targetMaxY = (std::min)(480, maxY + marginY); WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return; @@ -2780,10 +2797,10 @@ void ExecuteDenseGridGesture(int instanceId, int startX, int startY, const std:: auto interpolate = [&](int x0, int y0, int x1, int y1, int tX0, int tY0, int tX1, int tY1) { float dist0 = std::hypot(tX0 - x0, tY0 - y0); float dist1 = std::hypot(tX1 - x1, tY1 - y1); - float maxDist = std::max(dist0, dist1); + float maxDist = (std::max)(dist0, dist1); // THE LESSER THE SPEED, THE BETTER FOR THE BOT TO MISS EMPTY FIELDS. - int steps = std::max(20, (int)(maxDist / 5.0f)); + int steps = (std::max)(20, (int)(maxDist / 5.0f)); for (int i = 1; i <= steps; ++i) { float t = (float)i / steps; @@ -2792,33 +2809,24 @@ void ExecuteDenseGridGesture(int instanceId, int startX, int startY, const std:: int cx1 = x1 + (int)((tX1 - x1) * t); int cy1 = y1 + (int)((tY1 - y1) * t); - // Add humanized micro-jitter to drag waypoints - int jx0 = cx0 + (rand() % 5 - 2); - int jy0 = cy0 + (rand() % 5 - 2); - int jx1 = cx1 + (rand() % 5 - 2); - int jy1 = cy1 + (rand() % 5 - 2); - - std::string mCmd = "m 0 " + std::to_string(jx0) + " " + std::to_string(jy0) + " 50\n" + - "m 1 " + std::to_string(jx1) + " " + std::to_string(jy1) + " 50\nc\n"; + std::string mCmd = "m 0 " + std::to_string(cx0) + " " + std::to_string(cy0) + " 50\n" + + "m 1 " + std::to_string(cx1) + " " + std::to_string(cy1) + " 50\nc\n"; send(sock, mCmd.c_str(), mCmd.length(), 0); - // Micro-variable step delay (18ms - 23ms) - int stepDelay = 18 + (rand() % 6); - std::this_thread::sleep_for(std::chrono::milliseconds(stepDelay)); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); } - }; + }; interpolate(p1_startX, p1_startY, p2_startX, p2_startY, p1_x1, p1_y1, p2_x1, p2_y1); interpolate(p1_x1, p1_y1, p2_x1, p2_y1, p1_x2, p1_y2, p2_x2, p2_y2); interpolate(p1_x2, p1_y2, p2_x2, p2_y2, p1_x3, p1_y3, p2_x3, p2_y3); - std::string cmdUp0 = "u 0\nc\n"; send(sock, cmdUp0.c_str(), cmdUp0.length(), 0); std::this_thread::sleep_for(std::chrono::milliseconds(20)); // RELEASE FIRST FINGER std::string cmdUp1 = "u 1\nc\n"; - send(sock, cmdUp1.c_str(), cmdUp1.length(), 0); // RELEASE SECOND FINGER + send(sock, cmdUp1.c_str(), cmdUp1.length(), 0); // RELEASE SECOND FINGER // GIVE SOME TIME TO ANDROID BEFORE CLOSING TCP CONNECTION std::this_thread::sleep_for(std::chrono::milliseconds(100)); diff --git a/HD/Premium bot/bot.vcxproj b/HD/Premium bot/bot.vcxproj index c61ae08..6e15c6d 100644 --- a/HD/Premium bot/bot.vcxproj +++ b/HD/Premium bot/bot.vcxproj @@ -96,26 +96,26 @@ Application true - v145 + v143 Unicode Application false - v145 + v143 true Unicode Application true - v145 + v143 Unicode Application false - v145 + v143 true Unicode @@ -203,7 +203,7 @@ Windows true - libcurl.lib;archive.lib;libcrypto.lib;libssl.lib;bz2.lib;lz4.lib;glfw3.lib;tesseract55.lib;leptonica-1.87.0.lib;libwebp.lib;libwebpmux.lib;libwebpdecoder.lib;libwebpdemux.lib;libsharpyuv.lib;libpng16.lib;zlib.lib;jpeg.lib;tiff.lib;gif.lib;openjp2.lib;lzma.lib;zstd.lib;Gdi32.lib;User32.lib;Ws2_32.lib;Wldap32.lib;Normaliz.lib;Crypt32.lib;Advapi32.lib;Iphlpapi.lib;Secur32.lib;XmlLite.lib;opencv_calib3d4.lib;opencv_core4.lib;opencv_dnn4.lib;opencv_features2d4.lib;opencv_flann4.lib;opencv_highgui4.lib;opencv_imgcodecs4.lib;opencv_imgproc4.lib;opencv_ml4.lib;opencv_objdetect4.lib;opencv_photo4.lib;opencv_stitching4.lib;opencv_video4.lib;opencv_videoio4.lib + libcurl.lib;archive.lib;libcrypto.lib;libssl.lib;bz2.lib;lz4.lib;glfw3dll.lib;tesseract55.lib;leptonica-1.87.0.lib;libwebp.lib;libwebpmux.lib;libwebpdecoder.lib;libwebpdemux.lib;libsharpyuv.lib;libpng16.lib;z.lib;jpeg.lib;tiff.lib;gif.lib;openjp2.lib;lzma.lib;zstd.lib;Gdi32.lib;User32.lib;Ws2_32.lib;Wldap32.lib;Normaliz.lib;Crypt32.lib;Advapi32.lib;Iphlpapi.lib;Secur32.lib;XmlLite.lib;opencv_calib3d4.lib;opencv_core4.lib;opencv_dnn4.lib;opencv_features2d4.lib;opencv_flann4.lib;opencv_highgui4.lib;opencv_imgcodecs4.lib;opencv_imgproc4.lib;opencv_ml4.lib;opencv_objdetect4.lib;opencv_photo4.lib;opencv_stitching4.lib;opencv_video4.lib;opencv_videoio4.lib $(OutDir);$(HaydayVcpkgRoot)\lib;$(ProjectDir)discord-rpc;%(AdditionalLibraryDirectories) RequireAdministrator mainCRTStartup @@ -212,9 +212,9 @@ false - + - + diff --git a/HD/bot_logic.cpp b/HD/bot_logic.cpp index aeaea04..36faa33 100644 --- a/HD/bot_logic.cpp +++ b/HD/bot_logic.cpp @@ -283,19 +283,22 @@ std::string ResolveTemplatePath(const std::string& templatePath) { return ""; }; - if (fs::exists(requested, ec)) return requested.lexically_normal().string(); + // 1. First priority: Check relative to executable directory (where premium.exe is located) + char modulePath[MAX_PATH] = {}; + DWORD moduleLen = GetModuleFileNameA(nullptr, modulePath, MAX_PATH); + if (moduleLen > 0 && moduleLen < MAX_PATH) { + fs::path exeDir = fs::path(modulePath).parent_path(); + if (std::string resolved = findFromBase(exeDir); !resolved.empty()) return resolved; + } + // 2. Second priority: Check relative to current working directory ec.clear(); fs::path currentDir = fs::current_path(ec); if (!ec) { if (std::string resolved = findFromBase(currentDir); !resolved.empty()) return resolved; } - char modulePath[MAX_PATH] = {}; - DWORD moduleLen = GetModuleFileNameA(nullptr, modulePath, MAX_PATH); - if (moduleLen > 0 && moduleLen < MAX_PATH) { - if (std::string resolved = findFromBase(fs::path(modulePath).parent_path()); !resolved.empty()) return resolved; - } + if (fs::exists(requested, ec)) return requested.lexically_normal().string(); return templatePath; } diff --git a/HD/cpp/src/bot/ScreenRecovery.cpp b/HD/cpp/src/bot/ScreenRecovery.cpp index 5090306..e3bc4ef 100644 --- a/HD/cpp/src/bot/ScreenRecovery.cpp +++ b/HD/cpp/src/bot/ScreenRecovery.cpp @@ -357,7 +357,14 @@ bool IsNotRespondingDialogVisible(const cv::Mat& screen, MatchResult* outDialog) } bool IsSiloFullPopupVisible(const cv::Mat& screen, MatchResult* outPopup) { - MatchResult popup = FindImage(screen, silo_full_templatePath, 0.75f, false, 1.0f, false); + if (screen.empty()) return false; + MatchResult popup = FindImage(screen, silo_full_templatePath, 0.70f, false, 1.0f, false); + if (!popup.found) { + popup = FindImage(screen, "templates\\silo_full_banner.png", 0.70f, false, 1.0f, false); + } + if (!popup.found) { + popup = FindImage(screen, silo_full_cross_templatePath, std::max(0.65f, g_Thresholds.siloFullCrossThreshold), false, 1.0f, false); + } if (outPopup) *outPopup = popup; return popup.found; } diff --git a/HD/cpp/src/operations/market/State.cpp b/HD/cpp/src/operations/market/State.cpp index a67a58e..162d45f 100644 --- a/HD/cpp/src/operations/market/State.cpp +++ b/HD/cpp/src/operations/market/State.cpp @@ -454,7 +454,7 @@ float GetSaleProductThresholdForCrop(int cropMode) { MatchResult FindShopIconPreferCache(const cv::Mat& screen, AccountSlot& account) { MatchResult shopRes{ false, 0, 0, 0.0 }; auto rejectHudMatch = [&]() { - if (shopRes.found && IsTopLeftHudPoint(screen, shopRes.x, shopRes.y)) { + if (shopRes.found && (IsTopLeftHudPoint(screen, shopRes.x, shopRes.y) || shopRes.y > screen.rows - 90)) { shopRes = MatchResult{ false, 0, 0, 0.0 }; account.cachedShopX = -1; account.cachedShopY = -1; diff --git a/HD/premium gui.cpp b/HD/premium gui.cpp index e3bec5f..29c985b 100644 --- a/HD/premium gui.cpp +++ b/HD/premium gui.cpp @@ -60,7 +60,9 @@ std::string GetAppDataPath() { #define IMGUI_IMPL_OPENGL_LOADER_GLAD #include #define GLFW_DLL +#define GLFW_EXPOSE_NATIVE_WIN32 #include +#include #include "backends/imgui_impl_glfw.h" #include "backends/imgui_impl_opengl3.h" #include "imgui.h" @@ -3191,16 +3193,20 @@ static void ClassicTemplateManagerTab() { strftime(timeBuf, sizeof(timeBuf), "%H%M%S", &tstruct); std::error_code ec; - if (!fs::exists("templates", ec)) fs::create_directories("templates", ec); + char exePathBuf[MAX_PATH] = {}; + GetModuleFileNameA(NULL, exePathBuf, MAX_PATH); + fs::path exeDir = fs::path(exePathBuf).parent_path(); + fs::path templatesDir = exeDir / "templates"; + if (!fs::exists(templatesDir, ec)) fs::create_directories(templatesDir, ec); std::string filename = "screenshot_inst" + std::to_string(inst + 1) + "_" + std::string(timeBuf) + ".png"; - std::string fullPath = "templates\\" + filename; + fs::path fullPath = templatesDir / filename; - if (cv::imwrite(fullPath, rawScreen)) { - std::string msg = "Saved: " + fullPath; + if (cv::imwrite(fullPath.string(), rawScreen)) { + std::string msg = "Saved: " + fullPath.string(); strncpy(tmplStatus, msg.c_str(), sizeof(tmplStatus) - 1); tmplStatus[sizeof(tmplStatus) - 1] = '\0'; tmplColor = ImVec4(0.00f, 0.48f, 0.10f, 1.0f); - AddLog(inst, "Screenshot saved to: " + fullPath, ImVec4(0, 1, 0, 1)); + AddLog(inst, "Screenshot saved to: " + fullPath.string(), ImVec4(0, 1, 0, 1)); } else { strncpy(tmplStatus, "Error: could not write screenshot.", sizeof(tmplStatus) - 1); @@ -4890,16 +4896,21 @@ void RenderApp() { localtime_s(&tstruct, &now); strftime(timeBuf, sizeof(timeBuf), "%H%M%S", &tstruct); - if (!fs::exists("templates")) fs::create_directories("templates"); + std::error_code ec; + char exePathBuf[MAX_PATH] = {}; + GetModuleFileNameA(NULL, exePathBuf, MAX_PATH); + fs::path exeDir = fs::path(exePathBuf).parent_path(); + fs::path templatesDir = exeDir / "templates"; + if (!fs::exists(templatesDir, ec)) fs::create_directories(templatesDir, ec); std::string filename = "screenshot_inst" + std::to_string(inst + 1) + "_" + std::string(timeBuf) + ".png"; - std::string fullPath = "templates\\" + filename; + fs::path fullPath = templatesDir / filename; try { - if (cv::imwrite(fullPath, rawScreen)) { - std::string msg = std::string(Tr("Saved: ")) + fullPath; + if (cv::imwrite(fullPath.string(), rawScreen)) { + std::string msg = std::string(Tr("Saved: ")) + fullPath.string(); strncpy(tmplStatus, msg.c_str(), sizeof(tmplStatus) - 1); tmplColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); - AddLog(inst, std::string(Tr("Screenshot saved to: ")) + fullPath, ImVec4(0, 1, 0, 1)); + AddLog(inst, std::string(Tr("Screenshot saved to: ")) + fullPath.string(), ImVec4(0, 1, 0, 1)); } else { strcpy(tmplStatus, Tr("Error: Write failed (Permissions?)")); @@ -5526,6 +5537,31 @@ void RenderApp() { ImGui::End(); } +static WNDPROC g_OriginalWndProc = nullptr; +static LRESULT CALLBACK ResizableWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_NCHITTEST) { + POINT pt = { (short)LOWORD(lParam), (short)HIWORD(lParam) }; + RECT rect; + GetWindowRect(hwnd, &rect); + const int border = 8; + + bool onLeft = (pt.x >= rect.left && pt.x < rect.left + border); + bool onRight = (pt.x < rect.right && pt.x >= rect.right - border); + bool onTop = (pt.y >= rect.top && pt.y < rect.top + border); + bool onBottom = (pt.y < rect.bottom && pt.y >= rect.bottom - border); + + if (onTop && onLeft) return HTTOPLEFT; + if (onTop && onRight) return HTTOPRIGHT; + if (onBottom && onLeft) return HTBOTTOMLEFT; + if (onBottom && onRight) return HTBOTTOMRIGHT; + if (onLeft) return HTLEFT; + if (onRight) return HTRIGHT; + if (onTop) return HTTOP; + if (onBottom) return HTBOTTOM; + } + return CallWindowProc(g_OriginalWndProc, hwnd, msg, wParam, lParam); +} + int main() { SetUnhandledExceptionFilter(NxrthCrashHandler); if (!glfwInit()) return 1; @@ -5535,10 +5571,21 @@ int main() { glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); GLFWwindow* window = glfwCreateWindow(1350, 820, "NXRTH Premium", NULL, NULL); if (!window) return 1; + glfwSetWindowSizeLimits(window, 900, 550, GLFW_DONT_CARE, GLFW_DONT_CARE); + + HWND hwnd = glfwGetWin32Window(window); + if (hwnd) { + LONG style = GetWindowLong(hwnd, GWL_STYLE); + SetWindowLong(hwnd, GWL_STYLE, style | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX); + SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER); + g_OriginalWndProc = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)ResizableWndProc); + } + glfwMakeContextCurrent(window); glfwSwapInterval(1); diff --git a/HD/templates/Roadside_Shop.png b/HD/templates/Roadside_Shop.png index b3417e2..8cccbd8 100644 Binary files a/HD/templates/Roadside_Shop.png and b/HD/templates/Roadside_Shop.png differ diff --git a/HD/templates/barn_market.png b/HD/templates/barn_market.png index 0bc8f3f..681d237 100644 Binary files a/HD/templates/barn_market.png and b/HD/templates/barn_market.png differ diff --git a/HD/templates/carrot.png b/HD/templates/carrot.png index 7eabb83..12c556a 100644 Binary files a/HD/templates/carrot.png and b/HD/templates/carrot.png differ diff --git a/HD/templates/corn.png b/HD/templates/corn.png index 59a1c50..f863efd 100644 Binary files a/HD/templates/corn.png and b/HD/templates/corn.png differ diff --git a/HD/templates/create_sale.png b/HD/templates/create_sale.png index 5804d11..396fccc 100644 Binary files a/HD/templates/create_sale.png and b/HD/templates/create_sale.png differ diff --git a/HD/templates/createad.png b/HD/templates/createad.png index b683d92..9ae9a13 100644 Binary files a/HD/templates/createad.png and b/HD/templates/createad.png differ diff --git a/HD/templates/market_close_cross.png b/HD/templates/market_close_cross.png index fc67879..8352d36 100644 Binary files a/HD/templates/market_close_cross.png and b/HD/templates/market_close_cross.png differ diff --git a/HD/templates/plus.png b/HD/templates/plus.png index c80b2c9..3ce98ce 100644 Binary files a/HD/templates/plus.png and b/HD/templates/plus.png differ diff --git a/HD/templates/shop.png b/HD/templates/shop.png index 85f6665..7641632 100644 Binary files a/HD/templates/shop.png and b/HD/templates/shop.png differ diff --git a/HD/templates/shop_crate.png b/HD/templates/shop_crate.png index c8f006a..b37f1ab 100644 Binary files a/HD/templates/shop_crate.png and b/HD/templates/shop_crate.png differ diff --git a/HD/templates/silo_full.png b/HD/templates/silo_full.png index dec92ac..4b9403c 100644 Binary files a/HD/templates/silo_full.png and b/HD/templates/silo_full.png differ diff --git a/HD/templates/silo_full_banner.png b/HD/templates/silo_full_banner.png new file mode 100644 index 0000000..a1f42bb Binary files /dev/null and b/HD/templates/silo_full_banner.png differ diff --git a/HD/templates/silo_full_cross.png b/HD/templates/silo_full_cross.png index fc67879..42c24fb 100644 Binary files a/HD/templates/silo_full_cross.png and b/HD/templates/silo_full_cross.png differ diff --git a/HD/templates/silo_market.png b/HD/templates/silo_market.png index d65deb8..bd672d9 100644 Binary files a/HD/templates/silo_market.png and b/HD/templates/silo_market.png differ diff --git a/HD/templates/soybean.png b/HD/templates/soybean.png index 8e22709..5348b09 100644 Binary files a/HD/templates/soybean.png and b/HD/templates/soybean.png differ diff --git a/HD/templates/sugarcane.png b/HD/templates/sugarcane.png index b47c27a..23e2d06 100644 Binary files a/HD/templates/sugarcane.png and b/HD/templates/sugarcane.png differ diff --git a/HD/templates/wheat.png b/HD/templates/wheat.png index df38b77..ba900bd 100644 Binary files a/HD/templates/wheat.png and b/HD/templates/wheat.png differ diff --git a/HD/templates/wheat_shop.png b/HD/templates/wheat_shop.png index 096a030..e9899c5 100644 Binary files a/HD/templates/wheat_shop.png and b/HD/templates/wheat_shop.png differ diff --git a/docs/guides/ZOOM_INJECTION_GUIDE.md b/docs/guides/ZOOM_INJECTION_GUIDE.md new file mode 100644 index 0000000..34f5aa2 --- /dev/null +++ b/docs/guides/ZOOM_INJECTION_GUIDE.md @@ -0,0 +1,64 @@ +# Hay Day Zoom Injection & Camera Modding Guide + +This document explains the technical architecture, validation mechanisms, and automated workflow for injecting custom camera and zoom configurations into Supercell's Hay Day. + +--- + +## 1. Architectural Overview + +Hay Day manages camera bounds, view distances, and UI scales via a primary CSV configuration file: +* **Internal Path:** `assets/data/game_config.csv` (inside `split_install_time_asset_pack.apk` or `base.apk`) +* **Runtime Override Path:** `/data/data/com.supercell.hayday/update/data/game_config.csv` +* **Validation Manifest:** `/data/data/com.supercell.hayday/update/fingerprint.json` + +### Key Zoom Parameters in `game_config.csv` + +| Parameter | Stock Value | Optimized Value | Description | +| :--- | :--- | :--- | :--- | +| `MinZoom` | `50` | `30` | Maximum distance the camera can zoom out (lower = wider view). | +| `DefaultZoom` | `100` | `10` | Initial zoom level upon entering farm / switching accounts. | +| `MaxZoom` | `150` | `170` | Maximum close-up zoom level. | +| `MinZoomMobile` | `25` | `20` | Minimum zoom on mobile layout devices. | +| `DefaultZoomMobile`| `60` | `10` | Default zoom on mobile layout devices. | +| `MaxZoomMobile` | `100` | `170` | Maximum zoom on mobile layout devices. | +| `MinZoomSlowMobile`| `37` | `20` | Minimum zoom for slower animation modes. | + +--- + +## 2. Supercell Fingerprint & Asset Validation + +In modern Hay Day versions (1.70+), Supercell enforces cryptographic integrity over the `update/` directory: + +1. **Hash Verification:** When Hay Day boots, `libg.so` reads `/data/data/com.supercell.hayday/update/fingerprint.json`. +2. **SHA-1 Matching:** It computes the `SHA-1` hash of all files in `update/data/`. +3. **Rejection Condition:** If the SHA-1 of `/data/data/com.supercell.hayday/update/data/game_config.csv` does **not** match the `sha` field in `fingerprint.json`, the game engine discards the override file and reverts to stock internal assets. +4. **Resolution:** Whenever `game_config.csv` is modified, `fingerprint.json` **must** be updated simultaneously with the exact new SHA-1 hash before launching the game. + +--- + +## 3. Automated Injection Workflow + +```mermaid +flowchart TD + A[New Hay Day APK / APKM] -->|Extract| B[assets/data/game_config.csv] + B -->|Patch Parameters| C[Patched game_config.csv] + C -->|Compute SHA-1| D[New Hash] + D -->|Update Entry| E[fingerprint.json] + C -->|Push via ADB| F[/data/data/com.supercell.hayday/update/data/game_config.csv] + E -->|Push via ADB| G[/data/data/com.supercell.hayday/update/fingerprint.json] + C -->|Sync to Bot| H[HD/x64/Release/injecthacks/game_config.csv] +``` + +### Command-Line Execution +Run the automated tool against any APKM package or active emulator: +```powershell +python tools/hayday_zoom_injector.py --apkm "D:\Path\To\HayDay.apkm" --device 127.0.0.1:21503 +``` + +--- + +## 4. Troubleshooting + +* **Black Screen on Launch:** Occurs when an outdated `game_config.csv` with an incompatible schema (e.g. 52 KB vs 54 KB) is injected into a newer game engine. Always extract the base CSV from the matching game version. +* **Zoom Reverts to Stock:** Occurs when `fingerprint.json` was not updated with the new SHA-1 hash. Re-run `hayday_zoom_injector.py` with `--sync-fingerprint`. +* **Permission Denied:** Ensure ADB has root access (`adb shell id` shows `uid=0(root)`), or run `chmod 777` on the update directory. diff --git a/tools/apply_zoom_hack_17284.py b/tools/apply_zoom_hack_17284.py new file mode 100644 index 0000000..b31b871 --- /dev/null +++ b/tools/apply_zoom_hack_17284.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Generate and inject Hay Day 1.72.84 Zoom Hack with synchronized fingerprint.json SHA1 verification. +""" + +import zipfile +import hashlib +import json +import subprocess +import os +import shutil +import tempfile +from pathlib import Path + +APKM_PATH = r"D:\Download\LDplayer\com.supercell.hayday_1.72.84-2201_2arch_6dpi_25lang_1feat_bd1e15235028441de44b5e7b9d5c238f_apkmirror.com.apkm" +RELEASE_INJECT_DIR = Path(r"D:\02_Projects\Hayday-Bot-Pro\HD\x64\Release\injecthacks") +ADB_DEVICE = "127.0.0.1:21503" + +def extract_stock_game_config() -> bytes: + with zipfile.ZipFile(APKM_PATH, 'r') as z: + with zipfile.ZipFile(z.open('split_install_time_asset_pack.apk'), 'r') as az: + return az.read('assets/data/game_config.csv') + +def patch_zoom(stock_csv: bytes) -> bytes: + lines = stock_csv.decode('utf-8', errors='ignore').splitlines() + patched_lines = [] + for line in lines: + parts = line.split(',') + key = parts[0].strip() + if key == 'MinZoom': + line = 'MinZoom,30,,' + elif key == 'DefaultZoom': + line = 'DefaultZoom,10,,' + elif key == 'MaxZoom': + line = 'MaxZoom,170,,' + elif key == 'MinZoomMobile': + line = 'MinZoomMobile,20,,' + elif key == 'DefaultZoomMobile': + line = 'DefaultZoomMobile,10,,' + elif key == 'MaxZoomMobile': + line = 'MaxZoomMobile,170,,' + elif key == 'MinZoomSlowMobile': + line = 'MinZoomSlowMobile,20,,' + elif key == 'DefaultZoomSlowMobile': + line = 'DefaultZoomSlowMobile,10,,' + elif key == 'MaxZoomSlowMobile': + line = 'MaxZoomSlowMobile,170,,' + patched_lines.append(line) + return ('\r\n'.join(patched_lines) + '\r\n').encode('utf-8') + +def main(): + print("[+] Extracting official 1.72.84 game_config.csv from asset pack...") + stock_csv = extract_stock_game_config() + print(f"[+] Stock CSV size: {len(stock_csv)} bytes, Stock SHA1: {hashlib.sha1(stock_csv).hexdigest()}") + + print("[+] Applying optimized zoom parameters (MinZoom: 30, DefaultZoom: 10, MaxZoom: 170)...") + patched_csv = patch_zoom(stock_csv) + new_sha1 = hashlib.sha1(patched_csv).hexdigest() + print(f"[+] Patched CSV size: {len(patched_csv)} bytes, New SHA1: {new_sha1}") + + # 1. Update local injecthacks directory for premium.exe + RELEASE_INJECT_DIR.mkdir(parents=True, exist_ok=True) + local_zoom_file = RELEASE_INJECT_DIR / "game_config.csv" + local_zoom_file.write_bytes(patched_csv) + print(f"[+] Saved updated 1.72.84 zoom hack to {local_zoom_file}") + + # 2. Pull fingerprint.json from device + print(f"[+] Pulling fingerprint.json from emulator ({ADB_DEVICE})...") + tmp_dir = tempfile.mkdtemp(prefix="hd_zoom_") + try: + tmp_fp = Path(tmp_dir) / "fingerprint.json" + cmd = ["adb", "-s", ADB_DEVICE, "exec-out", "cat", "/data/data/com.supercell.hayday/update/fingerprint.json"] + res = subprocess.run(cmd, capture_output=True) + if res.returncode == 0 and len(res.stdout) > 100: + fp_data = json.loads(res.stdout.decode('utf-8')) + updated = False + for entry in fp_data.get('files', []): + if entry.get('file') == 'data/game_config.csv': + print(f"[+] Updating fingerprint entry from {entry.get('sha')} -> {new_sha1}") + entry['sha'] = new_sha1 + updated = True + break + if updated: + tmp_fp.write_text(json.dumps(fp_data, separators=(',', ':')), encoding='utf-8') + + # Push both to emulator + print("[+] Pushing updated fingerprint.json and game_config.csv to emulator...") + subprocess.run(["adb", "-s", ADB_DEVICE, "push", str(local_zoom_file), "/sdcard/temp_game_config.csv"], check=True) + subprocess.run(["adb", "-s", ADB_DEVICE, "push", str(tmp_fp), "/sdcard/temp_fingerprint.json"], check=True) + + cmds = ( + "su -c 'mkdir -p /data/data/com.supercell.hayday/update/data && " + "cp /sdcard/temp_game_config.csv /data/data/com.supercell.hayday/update/data/game_config.csv && " + "chmod 777 /data/data/com.supercell.hayday/update/data/game_config.csv && " + "cp /sdcard/temp_fingerprint.json /data/data/com.supercell.hayday/update/fingerprint.json && " + "chmod 777 /data/data/com.supercell.hayday/update/fingerprint.json && " + "rm /sdcard/temp_game_config.csv /sdcard/temp_fingerprint.json'" + ) + subprocess.run(["adb", "-s", ADB_DEVICE, "shell", cmds], check=True) + print("[SUCCESS] 1.72.84 Zoom Hack and Synchronized Fingerprint Injected Successfully!") + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + +if __name__ == '__main__': + main() diff --git a/tools/hayday_zoom_injector.py b/tools/hayday_zoom_injector.py new file mode 100644 index 0000000..a4daae1 --- /dev/null +++ b/tools/hayday_zoom_injector.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Universal Hay Day Zoom Injector & Fingerprint Synchronizer +Works across any Hay Day version (1.72.x+) by extracting the exact game_config.csv +schema from the game package, applying zoom overrides, recalculating SHA-1 hashes, +and updating both the device update/ directory and the bot's injecthacks/ directory. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import tempfile +import zipfile +from pathlib import Path + +DEFAULT_RELEASE_INJECT_DIR = Path(r"D:\02_Projects\Hayday-Bot-Pro\HD\x64\Release\injecthacks") +DEFAULT_DEVICE = "127.0.0.1:21503" +PACKAGE_NAME = "com.supercell.hayday" + +def extract_game_config_from_package(package_path: Path) -> bytes: + """Extracts game_config.csv from either an APKM bundle, asset pack APK, or standard APK.""" + print(f"[*] Reading package: {package_path}") + with zipfile.ZipFile(package_path, "r") as z: + # Check if it's an APKM bundle containing split APKs + namelist = z.namelist() + for name in namelist: + if "asset_pack" in name and name.endswith(".apk"): + print(f"[*] Found embedded asset pack: {name}") + with zipfile.ZipFile(z.open(name), "r") as az: + for inner in az.namelist(): + if inner.endswith("game_config.csv") and not inner.startswith("map"): + print(f"[+] Extracted {inner} from {name}") + return az.read(inner) + + # Check standard APK or directly contained game_config.csv + for name in namelist: + if name.endswith("game_config.csv") and not name.startswith("map"): + print(f"[+] Extracted {name}") + return z.read(name) + + raise FileNotFoundError(f"Could not find game_config.csv inside {package_path}") + +def patch_zoom_configuration( + csv_bytes: bytes, + min_zoom: int = 30, + default_zoom: int = 10, + max_zoom: int = 170, + min_zoom_mobile: int = 20, + default_zoom_mobile: int = 10, + max_zoom_mobile: int = 170, +) -> bytes: + """Patches zoom parameters in the raw CSV string while maintaining exact line endings and structure.""" + lines = csv_bytes.decode("utf-8", errors="ignore").splitlines() + patched_lines = [] + + replacements = { + "MinZoom": str(min_zoom), + "DefaultZoom": str(default_zoom), + "MaxZoom": str(max_zoom), + "MinZoomMobile": str(min_zoom_mobile), + "DefaultZoomMobile": str(default_zoom_mobile), + "MaxZoomMobile": str(max_zoom_mobile), + "MinZoomSlowMobile": str(min_zoom_mobile), + "DefaultZoomSlowMobile": str(default_zoom_mobile), + "MaxZoomSlowMobile": str(max_zoom_mobile), + } + + for line in lines: + parts = line.split(",") + key = parts[0].strip() if parts else "" + if key in replacements: + new_val = replacements[key] + # Replace the second token with our new value + if len(parts) > 1: + parts[1] = new_val + line = ",".join(parts) + else: + line = f"{key},{new_val},," + patched_lines.append(line) + + return ("\r\n".join(patched_lines) + "\r\n").encode("utf-8") + +def sync_to_emulator(device: str, patched_csv: bytes, sha1_hash: str) -> None: + """Pushes patched game_config.csv and updates fingerprint.json on the emulator.""" + print(f"[*] Connecting to emulator: {device}") + tmp_dir = tempfile.mkdtemp(prefix="hd_sync_") + try: + local_csv = Path(tmp_dir) / "game_config.csv" + local_fp = Path(tmp_dir) / "fingerprint.json" + local_csv.write_bytes(patched_csv) + + # Pull fingerprint.json from device + res = subprocess.run( + ["adb", "-s", device, "exec-out", f"cat /data/data/{PACKAGE_NAME}/update/fingerprint.json"], + capture_output=True, + check=False, + ) + + if res.returncode == 0 and len(res.stdout) > 50: + fp_data = json.loads(res.stdout.decode("utf-8")) + updated = False + for entry in fp_data.get("files", []): + if entry.get("file") == "data/game_config.csv": + old_sha = entry.get("sha") + print(f"[*] Updating fingerprint.json entry: {old_sha} -> {sha1_hash}") + entry["sha"] = sha1_hash + updated = True + break + + if updated: + local_fp.write_text(json.dumps(fp_data, separators=(",", ":")), encoding="utf-8") + # Push both files + subprocess.run(["adb", "-s", device, "push", str(local_csv), "/sdcard/temp_game_config.csv"], check=True) + subprocess.run(["adb", "-s", device, "push", str(local_fp), "/sdcard/temp_fingerprint.json"], check=True) + + remote_cmds = ( + f"su -c 'mkdir -p /data/data/{PACKAGE_NAME}/update/data && " + f"cp /sdcard/temp_game_config.csv /data/data/{PACKAGE_NAME}/update/data/game_config.csv && " + f"chmod 777 /data/data/{PACKAGE_NAME}/update/data/game_config.csv && " + f"cp /sdcard/temp_fingerprint.json /data/data/{PACKAGE_NAME}/update/fingerprint.json && " + f"chmod 777 /data/data/{PACKAGE_NAME}/update/fingerprint.json && " + f"rm /sdcard/temp_game_config.csv /sdcard/temp_fingerprint.json'" + ) + subprocess.run(["adb", "-s", device, "shell", remote_cmds], check=True) + print(f"[SUCCESS] Injected game_config.csv and synchronized fingerprint.json on {device}!") + else: + print("[-] Warning: data/game_config.csv not found in fingerprint.json, pushing raw file.") + subprocess.run(["adb", "-s", device, "push", str(local_csv), f"/data/data/{PACKAGE_NAME}/update/data/game_config.csv"], check=True) + else: + print("[-] Warning: fingerprint.json not found on device, pushing game_config.csv directly.") + subprocess.run(["adb", "-s", device, "push", str(local_csv), f"/data/data/{PACKAGE_NAME}/update/data/game_config.csv"], check=True) + + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + +def main(): + parser = argparse.ArgumentParser(description="Universal Hay Day Zoom Injector & Fingerprint Synchronizer") + parser.add_argument("--apkm", type=Path, help="Path to Hay Day APK or APKM package") + parser.add_argument("--device", type=str, default=DEFAULT_DEVICE, help="Target ADB device endpoint") + parser.add_argument("--min-zoom", type=int, default=30, help="Minimum zoom override (default: 30)") + parser.add_argument("--default-zoom", type=int, default=10, help="Default zoom override (default: 10)") + parser.add_argument("--max-zoom", type=int, default=170, help="Maximum zoom override (default: 170)") + parser.add_argument("--output-dir", type=Path, default=DEFAULT_RELEASE_INJECT_DIR, help="Local injecthacks destination") + parser.add_argument("--no-device-sync", action="store_true", help="Skip pushing directly to ADB device") + + args = parser.parse_args() + + # Determine input package + if args.apkm and args.apkm.exists(): + raw_csv = extract_game_config_from_package(args.apkm) + else: + # Fallback to searching common download folders + candidates = [ + Path(r"D:\Download\LDplayer\com.supercell.hayday_1.72.84-2201_2arch_6dpi_25lang_1feat_bd1e15235028441de44b5e7b9d5c238f_apkmirror.com.apkm"), + Path(r"D:\Download\com.supercell.hayday_1.72.84-2201_2arch_2dpi_24lang_4bdc49c3c4b2cc459e1687ce27a2d7ad_apkmirror.com.apkm"), + ] + found = None + for c in candidates: + if c.exists(): + found = c + break + if not found: + raise FileNotFoundError("Please provide --apkm path to Hay Day APK/APKM bundle.") + raw_csv = extract_game_config_from_package(found) + + print(f"[+] Stock configuration extracted ({len(raw_csv)} bytes)") + patched = patch_zoom_configuration( + raw_csv, + min_zoom=args.min_zoom, + default_zoom=args.default_zoom, + max_zoom=args.max_zoom, + ) + new_sha1 = hashlib.sha1(patched).hexdigest() + print(f"[+] Patched configuration generated ({len(patched)} bytes, SHA-1: {new_sha1})") + + # Save to local injecthacks directory + if args.output_dir: + args.output_dir.mkdir(parents=True, exist_ok=True) + dest = args.output_dir / "game_config.csv" + dest.write_bytes(patched) + print(f"[+] Saved patched file to: {dest}") + + # Synchronize to emulator + if not args.no_device_sync: + sync_to_emulator(args.device, patched, new_sha1) + +if __name__ == "__main__": + main() diff --git a/tools/inject_cpuinfo_direct_vhd.py b/tools/inject_cpuinfo_direct_vhd.py new file mode 100644 index 0000000..751a320 --- /dev/null +++ b/tools/inject_cpuinfo_direct_vhd.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +""" +Direct VHD injector for cpuinfo.arm64.txt and cpuinfo.arm.txt on MEmu Android 12 base image. +Injects missing Houdini Native Bridge files directly into /system/etc ext4 partition on sda6. +""" + +from __future__ import annotations + +import argparse +import math +import shutil +import struct +import time +from pathlib import Path + +SECTOR = 512 +PARTITION_START_SECTOR = 67384 # sda6 on Android 12 disk1.vmdk +PARTITION_OFFSET = PARTITION_START_SECTOR * SECTOR + +EXT4_SUPER_MAGIC = 0xEF53 +EXT4_EXTENTS_FL = 0x00080000 +EXT4_INDEX_FL = 0x00001000 +EXT4_NOCOMPR_FL = 0x00000080 +EXTENT_HEADER_MAGIC = 0xF30A + +CPUINFO_ARM64_CONTENT = b"""processor\t: 0 +BogoMIPS\t: 38.40 +Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp +CPU implementer\t: 0x41 +CPU architecture: 8 +CPU variant\t: 0x0 +CPU part\t: 0xd03 +CPU revision\t: 4 + +processor\t: 1 +BogoMIPS\t: 38.40 +Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp +CPU implementer\t: 0x41 +CPU architecture: 8 +CPU variant\t: 0x0 +CPU part\t: 0xd03 +CPU revision\t: 4 + +processor\t: 2 +BogoMIPS\t: 38.40 +Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp +CPU implementer\t: 0x41 +CPU architecture: 8 +CPU variant\t: 0x0 +CPU part\t: 0xd03 +CPU revision\t: 4 + +processor\t: 3 +BogoMIPS\t: 38.40 +Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp +CPU implementer\t: 0x41 +CPU architecture: 8 +CPU variant\t: 0x0 +CPU part\t: 0xd03 +CPU revision\t: 4 + +Hardware\t: Qualcomm Technologies, Inc SM8450 +""" + +def le16(buf: bytes | bytearray, off: int) -> int: + return struct.unpack_from(" int: + return struct.unpack_from(" None: + struct.pack_into(" None: + struct.pack_into(" int: + return ((8 + name_len + 3) // 4) * 4 + +from tools.inject_cert_direct_vhd import DynamicVhd + +class Ext4Image12: + def __init__(self, disk: DynamicVhd) -> None: + self.disk = disk + self.base = PARTITION_OFFSET + self.super_off = self.base + 1024 + self.super = bytearray(self.disk.read(self.super_off, 1024)) + if le16(self.super, 56) != EXT4_SUPER_MAGIC: + raise RuntimeError(f"partition at offset {self.base} does not look like ext4") + + self.block_size = 1024 << le32(self.super, 24) + self.blocks_count = le32(self.super, 4) + self.free_blocks = le32(self.super, 12) + self.free_inodes = le32(self.super, 16) + self.blocks_per_group = le32(self.super, 32) + self.inodes_per_group = le32(self.super, 40) + self.inode_size = le16(self.super, 88) + self.groups = math.ceil(self.blocks_count / self.blocks_per_group) + self.gdt_off = self.base + self.block_size + self.gdt = bytearray(self.disk.read(self.gdt_off, self.groups * 32)) + + def block_off(self, block_no: int) -> int: + return self.base + block_no * self.block_size + + def read_block(self, block_no: int) -> bytes: + return self.disk.read(self.block_off(block_no), self.block_size) + + def write_block(self, block_no: int, data: bytes) -> None: + if len(data) != self.block_size: + raise ValueError("wrong block size") + self.disk.write(self.block_off(block_no), data) + + def group_desc(self, group: int) -> dict[str, int]: + off = group * 32 + return { + "block_bitmap": le32(self.gdt, off), + "inode_bitmap": le32(self.gdt, off + 4), + "inode_table": le32(self.gdt, off + 8), + "free_blocks": le16(self.gdt, off + 12), + "free_inodes": le16(self.gdt, off + 14), + "used_dirs": le16(self.gdt, off + 16), + "flags": le16(self.gdt, off + 18), + } + + def put_group_count(self, group: int, free_blocks: int | None = None, free_inodes: int | None = None) -> None: + off = group * 32 + if free_blocks is not None: + put_le16(self.gdt, off + 12, free_blocks) + if free_inodes is not None: + put_le16(self.gdt, off + 14, free_inodes) + + def inode_offset(self, inode_no: int) -> int: + group = (inode_no - 1) // self.inodes_per_group + index = (inode_no - 1) % self.inodes_per_group + gd = self.group_desc(group) + return self.base + gd["inode_table"] * self.block_size + index * self.inode_size + + def read_inode(self, inode_no: int) -> bytearray: + return bytearray(self.disk.read(self.inode_offset(inode_no), self.inode_size)) + + def write_inode(self, inode_no: int, data: bytes | bytearray) -> None: + self.disk.write(self.inode_offset(inode_no), data) + + def extents(self, inode: bytes | bytearray) -> list[tuple[int, int, int]]: + data = inode[40 : 40 + 60] + if le16(data, 0) != EXTENT_HEADER_MAGIC: + raise NotImplementedError("direct blocks only implemented for extent inodes") + entries = le16(data, 2) + depth = le16(data, 6) + if depth != 0: + raise NotImplementedError("multi-depth extents not implemented") + out = [] + for i in range(entries): + off = 12 + i * 12 + logical = le32(data, off) + length = le16(data, off + 4) + start_hi = le16(data, off + 6) + start_lo = le32(data, off + 8) + out.append((logical, length, (start_hi << 32) | start_lo)) + return out + + def file_blocks(self, inode: bytes | bytearray) -> tuple[int, list[int]]: + size = le32(inode, 4) | (le32(inode, 108) << 32) + blocks: list[int] = [] + for _logical, length, start in self.extents(inode): + blocks.extend(start + i for i in range(length)) + return size, blocks + + def directory_entries(self, inode_no: int) -> list[dict[str, int | str]]: + inode = self.read_inode(inode_no) + _size, blocks = self.file_blocks(inode) + entries: list[dict[str, int | str]] = [] + for block in blocks: + data = self.read_block(block) + pos = 0 + while pos < self.block_size: + inode_ref = le32(data, pos) + rec_len = le16(data, pos + 4) + name_len = data[pos + 6] + file_type = data[pos + 7] + if rec_len < 8: + break + name = data[pos + 8 : pos + 8 + name_len].decode("utf-8", "replace") + if inode_ref: + entries.append({ + "name": name, + "inode": inode_ref, + "file_type": file_type, + "block": block, + "pos": pos, + "rec_len": rec_len, + "name_len": name_len, + }) + pos += rec_len + return entries + + def lookup(self, path: str) -> int: + inode_no = 2 + for part in [p for p in path.split("/") if p]: + found = None + for entry in self.directory_entries(inode_no): + if entry["name"] == part: + found = int(entry["inode"]) + break + if found is None: + raise FileNotFoundError(path) + inode_no = found + return inode_no + + def find_free_inode(self, preferred_group: int = 0) -> int: + for group in list(range(preferred_group, self.groups)) + list(range(0, preferred_group)): + gd = self.group_desc(group) + if gd["free_inodes"] == 0: + continue + bitmap = self.read_block(gd["inode_bitmap"]) + for idx in range(self.inodes_per_group): + byte = bitmap[idx // 8] + if not (byte & (1 << (idx % 8))): + return group * self.inodes_per_group + idx + 1 + raise RuntimeError("no free inode found") + + def find_free_block(self, preferred_group: int) -> int: + groups = list(range(preferred_group, self.groups)) + list(range(0, preferred_group)) + for group in groups: + gd = self.group_desc(group) + if gd["free_blocks"] == 0: + continue + bitmap = self.read_block(gd["block_bitmap"]) + group_start = group * self.blocks_per_group + group_limit = min(self.blocks_per_group, self.blocks_count - group_start) + for idx in range(group_limit): + if bitmap[idx // 8] & (1 << (idx % 8)): + continue + block = group_start + idx + raw_off = self.block_off(block) + if self.disk.is_allocated_range(raw_off, self.block_size): + return block + raise RuntimeError("no free block found in allocated VHD ranges") + + def set_inode_bitmap(self, inode_no: int) -> None: + group = (inode_no - 1) // self.inodes_per_group + idx = (inode_no - 1) % self.inodes_per_group + gd = self.group_desc(group) + bitmap = bytearray(self.read_block(gd["inode_bitmap"])) + if bitmap[idx // 8] & (1 << (idx % 8)): + raise RuntimeError(f"inode {inode_no} is already allocated") + bitmap[idx // 8] |= 1 << (idx % 8) + self.write_block(gd["inode_bitmap"], bitmap) + self.put_group_count(group, free_inodes=gd["free_inodes"] - 1) + put_le32(self.super, 16, le32(self.super, 16) - 1) + + def set_block_bitmap(self, block: int) -> None: + group = block // self.blocks_per_group + idx = block % self.blocks_per_group + gd = self.group_desc(group) + bitmap = bytearray(self.read_block(gd["block_bitmap"])) + if bitmap[idx // 8] & (1 << (idx % 8)): + raise RuntimeError(f"block {block} is already allocated") + bitmap[idx // 8] |= 1 << (idx % 8) + self.write_block(gd["block_bitmap"], bitmap) + self.put_group_count(group, free_blocks=gd["free_blocks"] - 1) + put_le32(self.super, 12, le32(self.super, 12) - 1) + + def write_super_and_gdt(self) -> None: + put_le32(self.super, 48, int(time.time())) + self.disk.write(self.super_off, self.super) + self.disk.write(self.gdt_off, self.gdt) + + def add_dir_entry(self, dir_inode_no: int, new_inode_no: int, name: str) -> None: + entries = self.directory_entries(dir_inode_no) + if any(e["name"] == name for e in entries): + print(f"[*] {name} already exists in directory") + return + + required = min_dir_rec_len(len(name)) + for entry in entries: + current_min = min_dir_rec_len(int(entry["name_len"])) + slack = int(entry["rec_len"]) - current_min + if slack < required: + continue + + block_no = int(entry["block"]) + pos = int(entry["pos"]) + block = bytearray(self.read_block(block_no)) + put_le16(block, pos + 4, current_min) + + new_pos = pos + current_min + new_rec_len = int(entry["rec_len"]) - current_min + put_le32(block, new_pos, new_inode_no) + put_le16(block, new_pos + 4, new_rec_len) + block[new_pos + 6] = len(name) + block[new_pos + 7] = 1 # EXT4_FT_REG_FILE + block[new_pos + 8 : new_pos + 8 + len(name)] = name.encode("ascii") + self.write_block(block_no, block) + print(f"[+] Added dir entry: {name} -> inode {new_inode_no}") + return + + raise RuntimeError(f"no directory slack available for entry {name}") + + def build_regular_inode(self, data_len: int, block: int) -> bytearray: + inode = bytearray(self.inode_size) + now = int(time.time()) + put_le16(inode, 0, 0o100644) + put_le32(inode, 4, data_len) + put_le32(inode, 8, now) + put_le32(inode, 12, now) + put_le32(inode, 16, now) + put_le16(inode, 26, 1) + put_le32(inode, 28, self.block_size // SECTOR) + put_le32(inode, 32, EXT4_EXTENTS_FL | EXT4_NOCOMPR_FL) + put_le16(inode, 40, EXTENT_HEADER_MAGIC) + put_le16(inode, 42, 1) # entries + put_le16(inode, 44, 4) # max extents in inode body + put_le16(inode, 46, 0) # depth + put_le32(inode, 52, 0) # ee_block + put_le16(inode, 56, 1) # ee_len + put_le16(inode, 58, (block >> 32) & 0xFFFF) + put_le32(inode, 60, block & 0xFFFFFFFF) + put_le16(inode, 128, 28) # i_extra_isize + return inode + + def inject_file(self, parent_dir_path: str, filename: str, content: bytes) -> None: + parent_inode = self.lookup(parent_dir_path) + existing = [e for e in self.directory_entries(parent_inode) if e["name"] == filename] + if existing: + print(f"[*] File {filename} already exists at inode {existing[0]['inode']}") + return + + parent_blocks = self.file_blocks(self.read_inode(parent_inode))[1] + preferred_block_group = parent_blocks[0] // self.blocks_per_group + new_inode = self.find_free_inode(0) + new_block = self.find_free_block(preferred_block_group) + + self.set_inode_bitmap(new_inode) + self.set_block_bitmap(new_block) + + payload = content + b"\0" * (self.block_size - len(content)) + self.write_block(new_block, payload) + self.write_inode(new_inode, self.build_regular_inode(len(content), new_block)) + self.add_dir_entry(parent_inode, new_inode, filename) + self.write_super_and_gdt() + print(f"[SUCCESS] Injected {filename} ({len(content)} bytes) into {parent_dir_path}") + +def inject_all_cpuinfo(vhd_path: Path): + vhd = DynamicVhd(vhd_path, writable=True) + try: + ext = Ext4Image12(vhd) + print(f"[+] Connected to Android 12 ext4 filesystem on sda6") + ext.inject_file("system/etc", "cpuinfo.arm64.txt", CPUINFO_ARM64_CONTENT) + ext.inject_file("system/etc", "cpuinfo.arm.txt", CPUINFO_ARM64_CONTENT) + finally: + vhd.close() + +if __name__ == '__main__': + vhd_path = Path(r"D:\01_Apps\Installed\Program Files\Microvirt\MEmu\image\120\MEmu120-2026080500037FFF-disk1.vmdk") + inject_all_cpuinfo(vhd_path) diff --git a/tools/inspect_system_sda6.py b/tools/inspect_system_sda6.py new file mode 100644 index 0000000..e004ab3 --- /dev/null +++ b/tools/inspect_system_sda6.py @@ -0,0 +1,47 @@ +from tools.inject_cert_direct_vhd import DynamicVhd, le16, le32 +from pathlib import Path + +vhd = DynamicVhd(Path(r"D:\01_Apps\Installed\Program Files\Microvirt\MEmu\image\120\MEmu120-2026080500037FFF-disk1.vmdk")) +base_sector = 67384 +base = base_sector * 512 +super_off = base + 1024 +super_data = bytearray(vhd.read(super_off, 1024)) +block_size = 1024 << le32(super_data, 24) +blocks_count = le32(super_data, 4) +free_blocks = le32(super_data, 12) +free_inodes = le32(super_data, 16) +blocks_per_group = le32(super_data, 32) +inodes_per_group = le32(super_data, 40) +inode_size = le16(super_data, 88) +groups = (blocks_count + blocks_per_group - 1) // blocks_per_group +print(f"sda6: block_size={block_size}, groups={groups}, free_blocks={free_blocks}, free_inodes={free_inodes}") + +# Read root inode 2 +bg0 = vhd.read(base + block_size, 32) +itable_blk = le32(bg0, 8) +root_ino_data = vhd.read(base + itable_blk * block_size + (2-1)*inode_size, inode_size) +print(f"Root inode mode: {oct(le16(root_ino_data, 0))}, flags: {hex(le32(root_ino_data, 32))}") + +# Read root dir blocks +# Check if extent based +magic = le16(root_ino_data, 40) +print(f"Extent magic: {hex(magic)}") +if magic == 0xF30A: + entries = le16(root_ino_data, 42) + depth = le16(root_ino_data, 46) + print(f"Extent entries: {entries}, depth: {depth}") + # read leaf + leaf_blk = le32(root_ino_data, 52 + 4) | (le16(root_ino_data, 52 + 2) << 32) + leaf_len = le16(root_ino_data, 52 + 0) + print(f"Leaf physical block: {leaf_blk}, len: {leaf_len}") + dir_data = vhd.read(base + leaf_blk * block_size, block_size) + off = 0 + while off < len(dir_data): + ino = le32(dir_data, off) + rec_len = le16(dir_data, off + 4) + name_len = dir_data[off + 6] + ftype = dir_data[off + 7] + if rec_len == 0 or ino == 0: break + name = dir_data[off + 8 : off + 8 + name_len].decode('latin1', errors='replace') + print(f" Entry: {name} (ino={ino}, ftype={ftype})") + off += rec_len diff --git a/tools/install_hayday_apkm.py b/tools/install_hayday_apkm.py new file mode 100644 index 0000000..c63a343 --- /dev/null +++ b/tools/install_hayday_apkm.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Install Hay Day APKM Bundle directly to connected Android Emulators (LDPlayer / MEmu) +using Android Split APK multi-package manager session. +""" + +import os +import sys +import zipfile +import subprocess +import tempfile +import argparse + +def get_adb_devices(adb_cmd="adb"): + try: + res = subprocess.run([adb_cmd, "devices"], capture_output=True, text=True, check=True) + lines = res.stdout.strip().splitlines()[1:] + devices = [] + for line in lines: + parts = line.split() + if len(parts) >= 2 and parts[1] == "device": + devices.append(parts[0]) + return devices + except Exception as e: + print(f"[-] Failed to run adb devices: {e}") + return [] + +def install_apkm(apkm_path, device=None, adb_cmd="adb"): + if not os.path.isfile(apkm_path): + print(f"[-] Error: APKM file not found at: {apkm_path}") + return False + + devices = get_adb_devices(adb_cmd) + if not devices: + print("[-] No running Android emulator / device detected via ADB.") + print(" Please start your LDPlayer or MEmu instance first.") + return False + + target_device = device if device else devices[0] + print(f"[+] Target ADB Device: {target_device}") + + # Check device ABI + abi_res = subprocess.run([adb_cmd, "-s", target_device, "shell", "getprop", "ro.product.cpu.abi"], + capture_output=True, text=True) + device_abi = abi_res.stdout.strip() + print(f"[+] Device primary ABI: {device_abi}") + + is_64bit = "arm64" in device_abi or "x86_64" in device_abi + selected_arch = "split_config.arm64_v8a.apk" if is_64bit else "split_config.armeabi_v7a.apk" + + print(f"[+] Reading APKM bundle: {apkm_path}") + with zipfile.ZipFile(apkm_path, 'r') as z: + entries = z.namelist() + + # Select required splits + splits_to_extract = ["base.apk", "split_install_time_asset_pack.apk"] + if selected_arch in entries: + splits_to_extract.append(selected_arch) + elif "split_config.arm64_v8a.apk" in entries: + splits_to_extract.append("split_config.arm64_v8a.apk") + + # Include standard dpi and en language if available + for name in entries: + if name.startswith("split_config.") and (name.endswith("dpi.apk") or name in ["split_config.en.apk", "split_config.zh.apk"]): + if name not in splits_to_extract and "arm" not in name: + splits_to_extract.append(name) + + print(f"[+] Splits selected for installation ({len(splits_to_extract)} files):") + for s in splits_to_extract: + print(f" - {s}") + + with tempfile.TemporaryDirectory(prefix="hayday_install_") as tmpdir: + extracted_files = [] + for split in splits_to_extract: + if split in entries: + print(f"[+] Extracting {split}...") + target_file = os.path.join(tmpdir, split) + with open(target_file, "wb") as f_out, z.open(split) as f_in: + f_out.write(f_in.read()) + extracted_files.append(target_file) + + print("[+] Installing Split APKs to emulator via 'adb install-multiple'...") + cmd = [adb_cmd, "-s", target_device, "install-multiple", "-r", "-d"] + extracted_files + res = subprocess.run(cmd, capture_output=True, text=True) + + if res.returncode == 0 and "Success" in res.stdout: + print("\n" + "="*50) + print(" [SUCCESS] Hay Day installed successfully with all assets!") + print("="*50) + return True + else: + print(f"[-] Installation failed: {res.stderr}\n{res.stdout}") + return False + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Install Hay Day APKM bundle to Android emulator") + parser.add_argument("--apkm", default=r"D:\Download\LDplayer\com.supercell.hayday_1.72.84-2201_2arch_6dpi_25lang_1feat_bd1e15235028441de44b5e7b9d5c238f_apkmirror.com.apkm", + help="Path to the .apkm file") + parser.add_argument("--device", default=None, help="ADB Device ID") + parser.add_argument("--adb", default="adb", help="Path to adb executable") + args = parser.parse_args() + + install_apkm(args.apkm, device=args.device, adb_cmd=args.adb) diff --git a/tools/patch_sda3_magisk_direct_vhd.py b/tools/patch_sda3_magisk_direct_vhd.py new file mode 100644 index 0000000..6966b53 --- /dev/null +++ b/tools/patch_sda3_magisk_direct_vhd.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Direct VHD patcher for MEmu Android 12 Magisk boot ramdisk. +Patches Inode 19 (ramdisk) in sda3 ext2 partition in-place. +""" + +import os +import sys +import shutil +import struct +from pathlib import Path +from tools.inject_cert_direct_vhd import DynamicVhd, le16, le32, put_le32 + +def patch_magisk_ramdisk(vhd_path: Path, new_ramdisk_path: Path, make_backup: bool = True): + if not vhd_path.is_file(): + raise FileNotFoundError(f"VHD not found at {vhd_path}") + if not new_ramdisk_path.is_file(): + raise FileNotFoundError(f"New ramdisk not found at {new_ramdisk_path}") + + new_ramdisk_data = new_ramdisk_path.read_bytes() + new_len = len(new_ramdisk_data) + print(f"[+] Loaded new Magisk ramdisk: {new_len} bytes") + + if make_backup: + bak_path = vhd_path.with_suffix('.vmdk.bak_stock_boot') + if not bak_path.exists(): + print(f"[+] Creating backup of base image: {bak_path}") + shutil.copy2(vhd_path, bak_path) + print("[+] Backup created successfully.") + else: + print(f"[+] Backup already exists: {bak_path}") + + vhd = DynamicVhd(vhd_path, writable=True) + try: + sda3_off = 34304 * 512 + block_size = 1024 + inode_size = 128 + inodes_per_group = 16 + + group = (19 - 1) // inodes_per_group + index = (19 - 1) % inodes_per_group + + bg_off = sda3_off + 2048 + group * 32 + bg = vhd.read(bg_off, 32) + inode_table_block = le32(bg, 8) + + inode_off = sda3_off + inode_table_block * block_size + index * inode_size + inode_data = bytearray(vhd.read(inode_off, inode_size)) + old_file_size = le32(inode_data, 4) + print(f"[+] Current Inode 19 file_size: {old_file_size} bytes") + + # Read block pointers + blocks = [] + for i in range(12): + b = le32(inode_data, 40 + i*4) + if b: blocks.append(b) + + singly = le32(inode_data, 40 + 12*4) + if singly: + s_data = vhd.read(sda3_off + singly * block_size, block_size) + for i in range(0, len(s_data), 4): + b = le32(s_data, i) + if b: blocks.append(b) + + doubly = le32(inode_data, 40 + 13*4) + if doubly: + d_data = vhd.read(sda3_off + doubly * block_size, block_size) + for i in range(0, len(d_data), 4): + s_b = le32(d_data, i) + if s_b: + s_data = vhd.read(sda3_off + s_b * block_size, block_size) + for j in range(0, len(s_data), 4): + b = le32(s_data, j) + if b: blocks.append(b) + + total_cap = len(blocks) * block_size + print(f"[+] Total allocated blocks for Inode 19: {len(blocks)} ({total_cap} bytes)") + if new_len > total_cap: + raise ValueError(f"New ramdisk size ({new_len} bytes) exceeds allocated blocks ({total_cap} bytes)") + + # Pad new_ramdisk_data to fill the used blocks with trailing zeros + padded_data = new_ramdisk_data + b'\x00' * (total_cap - new_len) + + print("[+] Writing new Magisk ramdisk into allocated blocks...") + for i, b in enumerate(blocks): + chunk = padded_data[i * block_size : (i + 1) * block_size] + vhd.write(sda3_off + b * block_size, chunk) + + # Update Inode 19 file_size + put_le32(inode_data, 4, new_len) + vhd.write(inode_off, inode_data) + print(f"[+] Updated Inode 19 file_size to {new_len} bytes.") + + # Read back to verify + read_back_inode = vhd.read(inode_off, inode_size) + verified_size = le32(read_back_inode, 4) + print(f"[+] Verified Inode 19 file_size on disk: {verified_size} bytes") + + read_first_block = vhd.read(sda3_off + blocks[0] * block_size, 16) + print(f"[+] Verified magic header of block 0: {read_first_block[:4].hex()} (1f8b0800 = gzip)") + + print("[SUCCESS] Magisk ramdisk patched successfully into base image!") + return True + finally: + vhd.close() + +if __name__ == '__main__': + vhd = Path(r"D:\01_Apps\Installed\Program Files\Microvirt\MEmu\image\120\MEmu120-2026080500037FFF-disk1.vmdk") + ramdisk = Path(r"network_study/android/magisk_memu_boot/test.cpio.gz") + patch_magisk_ramdisk(vhd, ramdisk, make_backup=True) diff --git a/tools/setup_build_environment.bat b/tools/setup_build_environment.bat new file mode 100644 index 0000000..b3dff90 --- /dev/null +++ b/tools/setup_build_environment.bat @@ -0,0 +1,15 @@ +@echo off +setlocal EnableDelayedExpansion +TITLE Hayday-Bot-Pro - Build Tools Setup + +:: Check for administrative privileges +net session >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo [i] Requesting Administrator privileges to install Visual Studio Build Tools... + powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process cmd.exe -ArgumentList '/c \"\"%~dp0tools\setup_build_environment.bat\"\"' -Verb RunAs" + exit /b 0 +) + +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\setup_build_environment.ps1" + +pause diff --git a/tools/setup_build_environment.ps1 b/tools/setup_build_environment.ps1 new file mode 100644 index 0000000..1b75245 --- /dev/null +++ b/tools/setup_build_environment.ps1 @@ -0,0 +1,134 @@ +#Requires -RunAsAdministrator +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$Host.UI.RawUI.WindowTitle = "Hayday-Bot-Pro Build Tools Setup" + +Write-Host "==========================================================" -ForegroundColor Cyan +Write-Host " Hayday-Bot-Pro - Build Tools & Dependencies Setup " -ForegroundColor Cyan +Write-Host "==========================================================" -ForegroundColor Cyan +Write-Host "" + +# 1. Check & Install Visual Studio 2022 Build Tools +Write-Host "[1/5] Checking Visual Studio C++ Build Tools..." -ForegroundColor Yellow +$vswherePath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +$msbuildPath = $null + +if (Test-Path $vswherePath) { + $msbuildPath = & $vswherePath -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1 +} + +if (-not $msbuildPath) { + Write-Host "[-] Visual Studio Build Tools not found. Installing VS 2022 Build Tools..." -ForegroundColor Cyan + Write-Host " Workload: Desktop development with C++ (MSVC v143, Windows SDK, MSBuild, CMake)" -ForegroundColor Gray + + $vsInstaller = "$env:TEMP\vs_BuildTools.exe" + if (-not (Test-Path $vsInstaller)) { + Write-Host " Downloading vs_BuildTools.exe..." -ForegroundColor Gray + Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vs_BuildTools.exe" -OutFile $vsInstaller + } + + Write-Host " Launching installer (UI progress window will show)..." -ForegroundColor Cyan + $vsArgs = "--passive --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" + $process = Start-Process -FilePath $vsInstaller -ArgumentList $vsArgs -Wait -PassThru + + if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { + Write-Host "[!] VS Installer finished with exit code $($process.ExitCode). Checking if tools were installed..." -ForegroundColor Yellow + } +} else { + Write-Host "[OK] Visual Studio Build Tools already installed at: $msbuildPath" -ForegroundColor Green +} + +# Re-check vswhere for MSBuild +if (Test-Path $vswherePath) { + $msbuildPath = & $vswherePath -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1 +} + +# 2. Check CMake +Write-Host "`n[2/5] Checking CMake..." -ForegroundColor Yellow +$cmakePath = "C:\Program Files\CMake\bin" +if (-not (Test-Path "$cmakePath\cmake.exe")) { + $cmakeCmd = Get-Command cmake -ErrorAction SilentlyContinue + if ($cmakeCmd) { + $cmakePath = Split-Path $cmakeCmd.Source + } else { + Write-Host "[-] CMake not found. Installing CMake via winget..." -ForegroundColor Cyan + winget install --id Kitware.CMake --source winget --accept-package-agreements --accept-source-agreements --exact + } +} +Write-Host "[OK] CMake is ready at: $cmakePath" -ForegroundColor Green + +# 3. Setup vcpkg and Environment Variables +Write-Host "`n[3/5] Configuring Environment Variables and PATH..." -ForegroundColor Yellow +$vcpkgDir = "D:\01_Apps\DevTools\vcpkg" +$vcpkgInstalledDir = "$vcpkgDir\installed\x64-windows" + +# Set persistent User environment variables +[System.Environment]::SetEnvironmentVariable("VCPKG_ROOT", $vcpkgDir, "User") +[System.Environment]::SetEnvironmentVariable("HaydayVcpkgRoot", $vcpkgInstalledDir, "User") +$env:VCPKG_ROOT = $vcpkgDir +$env:HaydayVcpkgRoot = $vcpkgInstalledDir + +Write-Host " [OK] Set VCPKG_ROOT = $vcpkgDir" -ForegroundColor Green +Write-Host " [OK] Set HaydayVcpkgRoot = $vcpkgInstalledDir" -ForegroundColor Green + +# Update User PATH +$userPath = [System.Environment]::GetEnvironmentVariable("PATH", "User") +$pathParts = $userPath -split ';' | Where-Object { $_ -ne '' } + +$pathsToAdd = @($vcpkgDir, $cmakePath) +if ($msbuildPath) { + $msbuildBinDir = Split-Path $msbuildPath + $pathsToAdd += $msbuildBinDir +} + +foreach ($p in $pathsToAdd) { + if ($p -and (Test-Path $p) -and ($pathParts -notcontains $p)) { + $pathParts += $p + Write-Host " [+] Added to PATH: $p" -ForegroundColor Green + } +} + +$newUserPath = ($pathParts | Select-Object -Unique) -join ';' +[System.Environment]::SetEnvironmentVariable("PATH", $newUserPath, "User") +$env:PATH = "$newUserPath;$env:PATH" +Write-Host "[OK] PATH updated successfully." -ForegroundColor Green + +# 4. Bootstrap and Configure vcpkg +Write-Host "`n[4/5] Bootstrapping vcpkg & installing dependencies..." -ForegroundColor Yellow +if (-not (Test-Path "$vcpkgDir\vcpkg.exe")) { + Write-Host " Bootstrapping vcpkg..." -ForegroundColor Gray + & "$vcpkgDir\bootstrap-vcpkg.bat" -disableMetrics +} + +Write-Host " Integrating vcpkg with MSBuild / Visual Studio..." -ForegroundColor Gray +& "$vcpkgDir\vcpkg.exe" integrate install + +Write-Host " Installing C++ libraries (glfw3, opencv4, tesseract:x64-windows)..." -ForegroundColor Cyan +Write-Host " (This may take a few minutes as packages compile)" -ForegroundColor Gray +& "$vcpkgDir\vcpkg.exe" install glfw3:x64-windows opencv4:x64-windows tesseract:x64-windows --triplet x64-windows + +# 5. Build Verification +Write-Host "`n[5/5] Verifying C++ Build of premium.exe..." -ForegroundColor Yellow +$solutionPath = "D:\02_Projects\Hayday-Bot-Pro\HD\Premium bot.sln" + +if ($msbuildPath -and (Test-Path $solutionPath)) { + Write-Host " Building solution: $solutionPath (Release|x64)..." -ForegroundColor Cyan + & "$msbuildPath" "$solutionPath" /p:Configuration=Release /p:Platform=x64 /maxcpucount + + $exePath = "D:\02_Projects\Hayday-Bot-Pro\HD\x64\Release\premium.exe" + if (Test-Path $exePath) { + Write-Host "" + Write-Host "==========================================================" -ForegroundColor Green + Write-Host " [SUCCESS] premium.exe built successfully! " -ForegroundColor Green + Write-Host " Path: $exePath" -ForegroundColor Green + Write-Host "==========================================================" -ForegroundColor Green + } else { + Write-Host "[!] Build finished, check output above." -ForegroundColor Yellow + } +} else { + Write-Host "[!] MSBuild or solution path not found for test build." -ForegroundColor Yellow +} + +Write-Host "`nSetup complete! You can now run quickstart.bat or build in Visual Studio / MSBuild anytime." -ForegroundColor Green diff --git a/tools/test_lookup_sda6.py b/tools/test_lookup_sda6.py new file mode 100644 index 0000000..2035fcc --- /dev/null +++ b/tools/test_lookup_sda6.py @@ -0,0 +1,30 @@ +from tools.inject_cert_direct_vhd import DynamicVhd, Ext4Image, le16, le32 +from pathlib import Path + +vhd = DynamicVhd(Path(r"D:\01_Apps\Installed\Program Files\Microvirt\MEmu\image\120\MEmu120-2026080500037FFF-disk1.vmdk")) +ext = Ext4Image(vhd) +ext.base = 67384 * 512 +ext.super_off = ext.base + 1024 +ext.super = bytearray(ext.disk.read(ext.super_off, 1024)) +ext.block_size = 1024 << le32(ext.super, 24) +ext.blocks_count = le32(ext.super, 4) +ext.free_blocks = le32(ext.super, 12) +ext.free_inodes = le32(ext.super, 16) +ext.blocks_per_group = le32(ext.super, 32) +ext.inodes_per_group = le32(ext.super, 40) +ext.inode_size = le16(ext.super, 88) +ext.groups = (ext.blocks_count + ext.blocks_per_group - 1) // ext.blocks_per_group +ext.gdt_off = ext.base + ext.block_size +ext.gdt = bytearray(ext.disk.read(ext.gdt_off, ext.groups * 32)) + +print("Looking up /system/etc or /etc on sda6...") +for path in ["/", "/etc", "/system", "/system/etc"]: + try: + ino = ext.lookup(path) + print(f" Found {path} -> inode {ino}") + entries = ext.directory_entries(ino) + print(f" Total entries in {path}: {len(entries)}") + for e in entries[:10]: + print(f" {e['name']} ({e['file_type']}, ino={e['inode']})") + except Exception as ex: + print(f" Lookup {path} failed: {ex}")