diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9f51ebc --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +DIST_DIR := OctoLair +BIN_DIR := bin + +UNAME_S := $(shell uname -s) + +CFLAGS := "" +LDFLAGS := "" +CC := "" + + +ifeq ($(UNAME_S), Linux) + SYSROOT := /usr/local/aarch64-linux-gnu-7.5.0-linaro/sysroot + CFLAGS = -I${SYSROOT}/usr/include -I${SYSROOT}/usr/include/SDL2 -I/usr/include/aarch64-linux-gnu/curl -I ./include -D_REENTRANT + LDFLAGS = -L${SYSROOT}/lib -L${SYSROOT}/usr/lib -L/usr/lib/aarch64-linux-gnu/ -lSDL2_image -lSDL2_ttf -lSDL2 -ldl -lpthread -lm -lstdc++ -lxml2 + CC = aarch64-linux-gnu-gcc --sysroot=${SYSROOT} +endif + +SRC := src/main.cpp src/utils.cpp src/theme.cpp +OBJ := $(SRC:.cpp=.o) +TARGET := octolair + +.PHONY: run build +.DEFAULT: build + +build: + @mkdir -p ${BIN_DIR} + @${CC} ${CFLAGS} ${SRC} -o ${BIN_DIR}/${TARGET} ${LDFLAGS} + +clean: + @rm -rf ${BIN_DIR}/* ${DIST_DIR}/* + +run: + @${BIN_DIR}/${TARGET} \ No newline at end of file diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..911f93a --- /dev/null +++ b/include/config.h @@ -0,0 +1,10 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define SCREEN_WIDTH 1280 +#define SCREEN_HEIGHT 720 + + + + +#endif \ No newline at end of file diff --git a/include/theme.h b/include/theme.h new file mode 100644 index 0000000..518e578 --- /dev/null +++ b/include/theme.h @@ -0,0 +1,18 @@ +#ifndef THEME_H +#define THEME_H + +#include + +struct Theme { + SDL_Color backgroundColor; + SDL_Color textColor; + SDL_Color highlightColor; + SDL_Color borderColor; + SDL_Color progressBarColor; +}; + +extern Theme currentTheme; + +void applyTheme(const Theme& theme); + +#endif // THEME_H \ No newline at end of file diff --git a/include/types.h b/include/types.h new file mode 100644 index 0000000..dc06b6e --- /dev/null +++ b/include/types.h @@ -0,0 +1,29 @@ +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +struct Game { + std::string title; + std::string region; + std::string version; + std::string languages; + std::string rating; + std::string url; +}; + +struct Console { + std::string name; + std::string url; +}; + +class Filter { +public: + std::string value; + Filter(const std::string& val) : value(val) {} +}; + + + +#endif // TYPES_H \ No newline at end of file diff --git a/include/utils.h b/include/utils.h new file mode 100644 index 0000000..5f13df3 --- /dev/null +++ b/include/utils.h @@ -0,0 +1,28 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "types.h" + +size_t header_callback(void* ptr, size_t size, size_t nmemb, std::string* filename); +std::string getHtml(const std::string& url); +std::vector parseHTML(const std::string& html); +std::vector parseGamesHTML(const std::string &htmlContent); +int downloadGame(std::string console, const std::string &htmlContent); + +#endif + + + + + + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..7cc4a92 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,514 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "types.h" +#include "utils.h" +#include "theme.h" + + +#include + +Theme darkTheme = { + {0, 0, 0, 255}, // backgroundColor + {255, 255, 255, 255}, // textColor + {255, 0, 0, 255}, // highlightColor + {255, 0, 0, 255}, // borderColor + {255, 255, 255, 255} // progressBarColor +}; + +Theme lightTheme = { + {255, 255, 255, 255}, // backgroundColor + {0, 0, 0, 255}, // textColor + {0, 0, 255, 255}, // highlightColor + {0, 0, 255, 255}, // borderColor + {0, 0, 0, 255} // progressBarColor +}; + +Theme purpleTheme = { + {0, 0, 0, 255}, // backgroundColor + {180, 157, 250, 255}, // textColor + {113, 66, 255, 255}, // highlightColor + {113, 66, 255, 255}, // borderColor + {113, 66, 255, 255} // progressBarColor +}; + + +extern Theme currentTheme; + + +std::atomic downloadProgress(0); +std::atomic isDownloading(false); + +std::queue> downloadQueue; +std::vector queuedGameTitles; + +std::mutex queueMutex; +std::condition_variable queueCV; + +std::string shortenText(const std::string& text, int maxLength) { + if (text.length() > maxLength) { + return text.substr(0, maxLength - 3) + "..."; + } + return text; +} + +std::string scrollText(const std::string& text, int offset) { + std::string spacedText; + if (text.length() >= 2) { + spacedText = text + " "; + } else { + spacedText = text + " "; + } + int length = spacedText.length(); + std::string scrolledText = spacedText.substr(offset % length) + spacedText.substr(0, offset % length); + return scrolledText.substr(0, 32); +} + +void DrawCircleOutline(SDL_Renderer* renderer, int centerX, int centerY, int radius) { + SDL_SetRenderDrawColor(renderer, currentTheme.highlightColor.r, currentTheme.highlightColor.g, currentTheme.highlightColor.b, currentTheme.highlightColor.a); + + for (int w = 0; w < radius * 2; w++) { + for (int h = 0; h < radius * 2; h++) { + int dx = radius - w; + int dy = radius - h; + if ((dx * dx + dy * dy) <= (radius * radius) && (dx * dx + dy * dy) >= ((radius - 1) * (radius - 1))) { + SDL_RenderDrawPoint(renderer, centerX + dx, centerY + dy); + } + } + } +} + +void DrawFilledCircle(SDL_Renderer* renderer, int centerX, int centerY, int radius) { + SDL_SetRenderDrawColor(renderer, currentTheme.highlightColor.r, currentTheme.highlightColor.g, currentTheme.highlightColor.b, currentTheme.highlightColor.a); + + for (int w = 0; w < radius * 2; w++) { + for (int h = 0; h < radius * 2; h++) { + int dx = radius - w; + int dy = radius - h; + if ((dx * dx + dy * dy) <= (radius * radius)) { + SDL_RenderDrawPoint(renderer, centerX + dx, centerY + dy); + } + } + } +} + + +void DrawRoundedRect(SDL_Renderer* renderer, SDL_Rect rect, int radius, int thickness) { + SDL_SetRenderDrawColor(renderer, currentTheme.highlightColor.r, currentTheme.highlightColor.g, currentTheme.highlightColor.b, currentTheme.highlightColor.a); + + if (radius <= 0 || thickness <= 0 || rect.w <= 0 || rect.h <= 0) return; + + for (int t = 0; t < thickness; t++) { + SDL_Rect innerRect = { rect.x + t, rect.y + t, rect.w - t * 2, rect.h - t * 2 }; + + SDL_RenderDrawLine(renderer, innerRect.x + radius, innerRect.y, innerRect.x + innerRect.w - radius, innerRect.y); + SDL_RenderDrawLine(renderer, innerRect.x + radius, innerRect.y + innerRect.h - 1, innerRect.x + innerRect.w - radius, innerRect.y + innerRect.h - 1); + + SDL_RenderDrawLine(renderer, innerRect.x, innerRect.y + radius, innerRect.x, innerRect.y + innerRect.h - radius); + SDL_RenderDrawLine(renderer, innerRect.x + innerRect.w - 1, innerRect.y + radius, innerRect.x + innerRect.w - 1, innerRect.y + innerRect.h - radius); + + DrawFilledCircle(renderer, innerRect.x + radius, innerRect.y + radius, radius - t); // Top-left corner + DrawFilledCircle(renderer, innerRect.x + innerRect.w - radius, innerRect.y + radius, radius - t); // Top-right corner + DrawFilledCircle(renderer, innerRect.x + radius, innerRect.y + innerRect.h - radius, radius - t); // Bottom-left corner + DrawFilledCircle(renderer, innerRect.x + innerRect.w - radius, innerRect.y + innerRect.h - radius, radius - t); // Bottom-right corner + } +} + + +void downloadGameThread(const std::string& console, const std::string& url, const std::string& gameTitle) { + + isDownloading = true; + downloadProgress = 0; + + int res = downloadGame(console, getHtml(url)); + + if (res == 0) { + std::cout << "Game downloaded successfully: " << gameTitle << std::endl; + } else { + std::cerr << "Failed to download game: " << gameTitle << std::endl; + } + + isDownloading = false; + queueCV.notify_one(); +} + + +void processDownloadQueue() { + while (true) { + std::unique_lock lock(queueMutex); + queueCV.wait(lock, [] { return !downloadQueue.empty() || !isDownloading; }); + + if (!downloadQueue.empty()) { + auto [console, url] = downloadQueue.front(); + downloadQueue.pop(); + std::string gameTitle = queuedGameTitles.front(); + lock.unlock(); + + downloadGameThread(console, url, gameTitle); + std::unique_lock lock(queueMutex); + queuedGameTitles.erase(queuedGameTitles.begin()); + } + } +} + +int main(int argc, char* argv[]) { + + applyTheme(purpleTheme); + + + std::string htmlConsoles = getHtml("https://vimm.net/vault"); + std::vector consoles = parseHTML(htmlConsoles); + + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) { + std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl; + return 1; + } + + SDL_Window* window = SDL_CreateWindow("OctoLair", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN); + if (window == nullptr) { + std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl; + SDL_Quit(); + return 1; + } + + SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (renderer == nullptr) { + SDL_DestroyWindow(window); + std::cerr << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl; + SDL_Quit(); + return 1; + } + + SDL_GameController* controller = nullptr; + for (int i = 0; i < SDL_NumJoysticks(); ++i) { + if (SDL_IsGameController(i)) { + controller = SDL_GameControllerOpen(i); + if (controller) { + break; + } + } + } + + TTF_Init(); + TTF_Font* font = TTF_OpenFont("res/HyperlacRegular.ttf", 26); + if (!font) { + std::cerr << "TTF_OpenFont Error: " << TTF_GetError() << std::endl; + return 1; + } + + if(IMG_Init(IMG_INIT_PNG) == 0) { + std::cerr << "IMG_Init Error: " << IMG_GetError() << std::endl; + return 1; + } + + if (!controller) { + std::cerr << "No controller found" << std::endl; + } + + std::vector filters = { + Filter("#"), Filter("A"), Filter("B"), Filter("C"), Filter("D"), Filter("E"), Filter("F"), Filter("G"), Filter("H"), Filter("I"), + Filter("J"), Filter("K"), Filter("L"), Filter("M"), Filter("N"), Filter("O"), Filter("P"), Filter("Q"), Filter("R"), Filter("S"), + Filter("T"), Filter("U"), Filter("V"), Filter("W"), Filter("X"), Filter("Y"), Filter("Z") + }; + + SDL_Event e; + bool quit = false; + int selectedConsole = 0; + int selectedGame = 0; + int selectedFilter = 0; + + std::vector games; + bool showGames = false; + bool showFilters = false; + + std::thread queueThread(processDownloadQueue); + queueThread.detach(); + + Uint32 lastButtonPressTime = 0; + Uint32 buttonPressDelay = 200; // Delay in milliseconds + Uint32 buttonHoldDelay = 500; // Delay before repeating action when holding button + bool buttonHeld = false; + + bool dpadUpPressed = false; + bool dpadDownPressed = false; + + while (!quit) { + static int scrollOffset = 0; + static int frameCount = 0; + frameCount++; + + if (frameCount % 8 == 0) { + scrollOffset++; + } + + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) { + quit = true; + } else if (e.type == SDL_CONTROLLERBUTTONDOWN || e.type == SDL_CONTROLLERBUTTONUP) { + Uint32 currentTime = SDL_GetTicks(); + + if (e.type == SDL_CONTROLLERBUTTONDOWN) { + lastButtonPressTime = currentTime; + std::cout << "Controller button pressed: " << (int)e.cbutton.button << std::endl; + if (e.cbutton.button == 3) { + quit = true; + } else if (e.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP) { + dpadUpPressed = true; + if (!showGames && !showFilters) { + selectedConsole = (selectedConsole - 1 + consoles.size()) % consoles.size(); + scrollOffset = 0; + } else if (showFilters) { + selectedFilter = (selectedFilter - 1 + filters.size()) % filters.size(); + scrollOffset = 0; + } else if (showGames) { + selectedGame = (selectedGame - 1 + games.size()) % games.size(); + scrollOffset = 0; + } + } else if (e.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { + dpadDownPressed = true; + if (!showGames && !showFilters) { + selectedConsole = (selectedConsole + 1) % consoles.size(); + scrollOffset = 0; + } else if (showFilters) { + selectedFilter = (selectedFilter + 1) % filters.size(); + scrollOffset = 0; + } else if (showGames) { + selectedGame = (selectedGame + 1) % games.size(); + scrollOffset = 0; + } + } else if (e.cbutton.button == 0) { + if (!showFilters && !showGames) { + showFilters = true; + } else if (showFilters) { + std::cout << "Selected console: " << consoles[selectedConsole].name << std::endl; + std::cout << "https://vimm.net" + consoles[selectedConsole].url + "/" + filters[selectedFilter].value << std::endl; + + std::string htmlGames = getHtml("https://vimm.net" + consoles[selectedConsole].url + "/" + filters[selectedFilter].value); + games = parseGamesHTML(htmlGames); + + std::cout << "Number of games parsed: " << games.size() << std::endl; + + showGames = true; + selectedGame = 0; + showFilters = false; + } else if (showGames && !games.empty()) { + std::cout << "Selected game: " << games[selectedGame].title << std::endl; + std::cout << "Queueing game for download..." << std::endl; + + { + std::lock_guard lock(queueMutex); + downloadQueue.push({consoles[selectedConsole].name, "https://vimm.net" + games[selectedGame].url}); + queuedGameTitles.push_back(games[selectedGame].title); + } + queueCV.notify_one(); + } + } else if (e.cbutton.button == 1) { + if (showGames) { + showGames = false; + showFilters = true; + } else if (showFilters) { + showFilters = false; + } + } + } else if (e.type == SDL_CONTROLLERBUTTONUP) { + if (e.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP) { + dpadUpPressed = false; + } else if (e.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) { + dpadDownPressed = false; + } + } + } + } + + // Handle button hold for D-Pad + Uint32 currentTime = SDL_GetTicks(); + if (dpadUpPressed && (currentTime - lastButtonPressTime) >= buttonPressDelay) { + lastButtonPressTime = currentTime; + if (!showGames && !showFilters) { + selectedConsole = (selectedConsole - 1 + consoles.size()) % consoles.size(); + scrollOffset = 0; + } else if (showFilters) { + selectedFilter = (selectedFilter - 1 + filters.size()) % filters.size(); + scrollOffset = 0; + } else if (showGames) { + selectedGame = (selectedGame - 1 + games.size()) % games.size(); + scrollOffset = 0; + } + } else if (dpadDownPressed && (currentTime - lastButtonPressTime) >= buttonPressDelay) { + lastButtonPressTime = currentTime; + if (!showGames && !showFilters) { + selectedConsole = (selectedConsole + 1) % consoles.size(); + scrollOffset = 0; + } else if (showFilters) { + selectedFilter = (selectedFilter + 1) % filters.size(); + scrollOffset = 0; + } else if (showGames) { + selectedGame = (selectedGame + 1) % games.size(); + scrollOffset = 0; + } + } + + SDL_SetRenderDrawColor(renderer, currentTheme.backgroundColor.r, currentTheme.backgroundColor.g, currentTheme.backgroundColor.b, currentTheme.backgroundColor.a); + SDL_RenderClear(renderer); + + const int maxItemsPerPage = 40; + const int itemsPerRow = 20; + const int columnWidth = 250; + const int rowHeight = 30; + const int leftSectionWidth = SCREEN_WIDTH / 2; + const int rightSectionWidth = SCREEN_WIDTH / 2; + const int borderThickness = 5; + const int offset = 10; + const int cornerRadius = 20; + + + int currentPage = 0; + if (!showGames && !showFilters) { + currentPage = selectedConsole / maxItemsPerPage; + } else if (showFilters) { + currentPage = selectedFilter / maxItemsPerPage; + } else if (showGames) { + currentPage = selectedGame / maxItemsPerPage; + } + + SDL_Rect leftBox = {offset, offset, leftSectionWidth - 2 * offset, SCREEN_HEIGHT - 2 * offset}; + DrawRoundedRect(renderer, leftBox, cornerRadius, borderThickness); + + if (!showGames && !showFilters) { + for (size_t i = currentPage * maxItemsPerPage; i < consoles.size() && i < (currentPage + 1) * maxItemsPerPage; i++) { + + SDL_Color color = currentTheme.textColor; + std::string displayText = shortenText(consoles[i].name, 20); + if (i == selectedConsole) { + color = currentTheme.highlightColor; + displayText = scrollText(consoles[i].name, scrollOffset); + } + SDL_Surface* surface = TTF_RenderText_Solid(font, displayText.c_str(), color); + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + + int column = (i % maxItemsPerPage) / itemsPerRow; + int row = (i % maxItemsPerPage) % itemsPerRow; + + SDL_Rect rect = {offset + 40 + column * columnWidth, offset + 40 + row * rowHeight, surface->w, surface->h}; + + SDL_RenderCopy(renderer, texture, nullptr, &rect); + SDL_FreeSurface(surface); + SDL_DestroyTexture(texture); + } + } else if (showFilters) { + for (size_t i = currentPage * maxItemsPerPage; i < filters.size() && i < (currentPage + 1) * maxItemsPerPage; i++) { + SDL_Color color = currentTheme.textColor; + std::string displayText = shortenText(filters[i].value, 20); + if (i == selectedFilter) { + color = currentTheme.highlightColor; + displayText = scrollText(filters[i].value, scrollOffset); + } + SDL_Surface* surface = TTF_RenderText_Solid(font, displayText.c_str(), color); + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + + int column = (i % maxItemsPerPage) / itemsPerRow; + int row = (i % maxItemsPerPage) % itemsPerRow; + + SDL_Rect rect = {offset + 40 + column * columnWidth, offset + 40 + row * rowHeight, surface->w, surface->h}; + + SDL_RenderCopy(renderer, texture, nullptr, &rect); + SDL_FreeSurface(surface); + SDL_DestroyTexture(texture); + } + } else if (showGames) { + for (size_t i = currentPage * maxItemsPerPage; i < games.size() && i < (currentPage + 1) * maxItemsPerPage; i++) { + SDL_Color color = currentTheme.textColor; + std::string displayText = shortenText(games[i].title, 20); + if (i == selectedGame) { + color = currentTheme.highlightColor; + displayText = scrollText(games[i].title, scrollOffset); + } + SDL_Surface* surface = TTF_RenderText_Solid(font, displayText.c_str(), color); + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + + int column = (i % maxItemsPerPage) / itemsPerRow; + int row = (i % maxItemsPerPage) % itemsPerRow; + + SDL_Rect rect = {offset + 40 + column * columnWidth, offset + 40 + row * rowHeight, surface->w, surface->h}; + + + SDL_RenderCopy(renderer, texture, nullptr, &rect); + SDL_FreeSurface(surface); + SDL_DestroyTexture(texture); + } + } + + SDL_Rect rightBox = {leftSectionWidth + offset, offset, rightSectionWidth - 2 * offset, SCREEN_HEIGHT - 2 * offset}; + DrawRoundedRect(renderer, rightBox, cornerRadius, borderThickness); + + SDL_Texture* imageTexture = nullptr; + if (!showGames && !showFilters && selectedConsole < consoles.size()) { + //Load console image + imageTexture = IMG_LoadTexture(renderer, ("res/placeholder.png")); + } else if (showGames && selectedGame < games.size()) { + //Load game image + imageTexture = IMG_LoadTexture(renderer, ("res/placeholder.png")); + } + + if (imageTexture) { + SDL_Rect imageRect = {leftSectionWidth + offset + 10, offset + 10, rightSectionWidth - 2 * offset - 20, SCREEN_HEIGHT - 2 * offset - 20}; + SDL_RenderCopy(renderer, imageTexture, nullptr, &imageRect); + SDL_DestroyTexture(imageTexture); + } + + if (isDownloading) { + + // Outline For Progress Box + SDL_Rect fullBox = {leftSectionWidth + offset + 70, SCREEN_HEIGHT - offset - 60, (rightSectionWidth - 2 * offset - 140), 25}; + SDL_SetRenderDrawColor(renderer, currentTheme.highlightColor.r, currentTheme.highlightColor.g, currentTheme.highlightColor.b, currentTheme.highlightColor.a); + SDL_RenderDrawRect(renderer, &fullBox); + + // Define Progress Bar + SDL_Rect progressBar = {leftSectionWidth + offset + 70, SCREEN_HEIGHT - offset - 60, (rightSectionWidth - 2 * offset - 140) * downloadProgress / 100, 25}; + SDL_SetRenderDrawColor(renderer, currentTheme.progressBarColor.r, currentTheme.progressBarColor.g, currentTheme.progressBarColor.b, currentTheme.progressBarColor.a); + SDL_RenderFillRect(renderer, &progressBar); + + SDL_Color color = currentTheme.textColor; + SDL_Surface* textSurface = TTF_RenderText_Solid(font, queuedGameTitles[0].c_str(), color); + SDL_Texture* textTexture = SDL_CreateTextureFromSurface(renderer, textSurface); + int textWidth = textSurface->w; + int textHeight = textSurface->h; + SDL_FreeSurface(textSurface); + SDL_Rect textRect = {leftSectionWidth + offset + 70, SCREEN_HEIGHT - offset - 90, textWidth, textHeight}; + SDL_RenderCopy(renderer, textTexture, NULL, &textRect); + SDL_DestroyTexture(textTexture); + + if (queuedGameTitles.size() > 1) { + SDL_Surface* nextTextSurface = TTF_RenderText_Solid(font, ("Next: " + queuedGameTitles[1]).c_str(), color); + SDL_Texture* nextTextTexture = SDL_CreateTextureFromSurface(renderer, nextTextSurface); + int nextTextWidth = nextTextSurface->w; + int nextTextHeight = nextTextSurface->h; + SDL_FreeSurface(nextTextSurface); + SDL_Rect nextTextRect = {leftSectionWidth + offset + 70, SCREEN_HEIGHT - offset - 120, nextTextWidth, nextTextHeight}; + SDL_RenderCopy(renderer, nextTextTexture, NULL, &nextTextRect); + SDL_DestroyTexture(nextTextTexture); + } + } + + SDL_RenderPresent(renderer); + } + + if (controller) { + SDL_GameControllerClose(controller); + } + + TTF_CloseFont(font); + + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} \ No newline at end of file diff --git a/src/theme.cpp b/src/theme.cpp new file mode 100644 index 0000000..2bee5f7 --- /dev/null +++ b/src/theme.cpp @@ -0,0 +1,7 @@ +#include "theme.h" + +Theme currentTheme; + +void applyTheme(const Theme& theme) { + currentTheme = theme; +} \ No newline at end of file diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..4e2dd50 --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,384 @@ +#include "utils.h" +#include "types.h" +#include + +std::unordered_map systemToRomFolder = { + {"Atari 2600", "ATARI2600"}, + {"Atari 5200", "ATARI5200"}, + {"Atari 7800", "ATARI7800"}, + {"Nintendo", "FC"}, // Assuming "Nintendo" refers to NES + {"Master System", "MS"}, + {"TurboGrafx-16", "PCE"}, // TurboGrafx-16 is equivalent to PC Engine + {"Genesis", "MD"}, // Genesis is equivalent to Mega Drive + {"TurboGrafx-CD", "PCECD"}, // TurboGrafx-CD is equivalent to PC Engine CD-ROM + {"Super Nintendo", "SFC"}, + {"Sega CD", "SEGACD"}, + {"Sega 32X", "SEGA32X"}, + {"Saturn", "SATURN"}, + {"PlayStation", "PS"}, + {"Nintendo 64", "N64"}, + {"Dreamcast", "DC"}, + {"Game Boy", "GB"}, // Assuming Game Boy is handled separately (add folder if known) + {"Lynx", "LYNX"}, // Add specific ROM folder if known + {"Game Gear", "GG"}, // Add specific ROM folder if known + {"Virtual Boy", "VB"}, // Add specific ROM folder if known + {"Game Boy Advance", "GBA"}, // Add specific ROM folder if known + {"Nintendo DS", "NDS"}, // Add specific ROM folder if known + {"PlayStation Portable", "PSP"} // Add specific ROM folder if known +}; + + +typedef void* CURL; +typedef CURL* (*curl_easy_init_t)(); +typedef void (*curl_easy_cleanup_t)(CURL*); +typedef int (*curl_easy_setopt_t)(CURL*, int, ...); +typedef int (*curl_easy_perform_t)(CURL*); +typedef const char* (*curl_easy_strerror_t)(int); +typedef struct curl_slist* (*curl_slist_append_t)(struct curl_slist*, const char*); + +// curl_off_t +typedef long long curl_off_t; + + +extern std::atomic downloadProgress; + +int progressCallback(void* ptr, curl_off_t total, curl_off_t now, curl_off_t, curl_off_t) { + if (total > 0) { + downloadProgress = static_cast((now * 100) / total); + } + return 0; +} + + +size_t header_callback(void* ptr, size_t size, size_t nmemb, std::string* filename) { + std::string header((char*)ptr, size * nmemb); + if (header.find("Content-Disposition:") != std::string::npos) { + size_t pos = header.find("filename=\""); + if (pos != std::string::npos) { + size_t endPos = header.find("\"", pos + 10); + if (endPos != std::string::npos) { + *filename = header.substr(pos + 10, endPos - pos - 10); + } + } + } + return size * nmemb; +} + +std::string getHtml(const std::string& url) { + void* handle = dlopen("/usr/lib/libcurl.so.4", RTLD_LAZY); + if (!handle) { + std::cerr << "Failed to load libcurl: " << dlerror() << std::endl; + return "1"; + } + + auto curl_easy_init = (curl_easy_init_t)dlsym(handle, "curl_easy_init"); + auto curl_easy_cleanup = (curl_easy_cleanup_t)dlsym(handle, "curl_easy_cleanup"); + auto curl_easy_setopt = (curl_easy_setopt_t)dlsym(handle, "curl_easy_setopt"); + auto curl_easy_perform = (curl_easy_perform_t)dlsym(handle, "curl_easy_perform"); + + if (!curl_easy_init || !curl_easy_cleanup || !curl_easy_setopt || !curl_easy_perform) { + std::cerr << "Failed to resolve libcurl functions." << std::endl; + dlclose(handle); + return "1"; + } + + CURL* curl = curl_easy_init(); + std::string html; + if (curl) { + curl_easy_setopt(curl, 10002 /* CURLOPT_URL */, url.c_str()); + curl_easy_setopt(curl, 64 /* CURLOPT_SSL_VERIFYPEER */, 0L); // Disable SSL verification + curl_easy_setopt(curl, 81 /* CURLOPT_SSL_VERIFYHOST */, 0L); // Disable host verification + curl_easy_setopt(curl, 10001 /* CURLOPT_WRITEDATA */, &html); + curl_easy_setopt(curl, 20011 /* CURLOPT_WRITEFUNCTION */, +[](char* ptr, size_t size, size_t nmemb, std::string* data) { + data->append(ptr, size * nmemb); + return size * nmemb; + }); + int res = curl_easy_perform(curl); + if (res != 0) { + std::cerr << "curl_easy_perform failed with error code: " << res << std::endl; + } + curl_easy_cleanup(curl); + dlclose(handle); + } + + return html; +} + +std::vector parseHTML(const std::string& html) { + std::vector consoles; + + htmlDocPtr doc = htmlReadMemory(html.c_str(), html.size(), NULL, NULL, HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING); + if (doc == NULL) { + std::cerr << "Failed to parse HTML" << std::endl; + return consoles; + } + + xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); + if (xpathCtx == NULL) { + std::cerr << "Failed to create XPath context" << std::endl; + xmlFreeDoc(doc); + return consoles; + } + + xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((xmlChar*)"//div[@style='display:flex; justify-content:center; align-items:flex-start; flex-wrap:wrap; gap:15px; margin:auto']//a", xpathCtx); + if (xpathObj == NULL) { + std::cerr << "Failed to evaluate XPath expression" << std::endl; + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + return consoles; + } + + xmlNodeSetPtr nodes = xpathObj->nodesetval; + for (int i = 0; i < nodes->nodeNr; ++i) { + xmlNodePtr node = nodes->nodeTab[i]; + xmlChar* href = xmlGetProp(node, (xmlChar*)"href"); + xmlChar* content = xmlNodeGetContent(node); + if (href && content) { + consoles.push_back({(char*)content, (char*)href}); + } + xmlFree(href); + xmlFree(content); + } + + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + + int i = 0; + for (const auto& console : consoles) { + i++; + std::cout << console.name << " - " << i << std::endl; + } + + return consoles; +} + +std::vector parseGamesHTML(const std::string &htmlContent) { + std::vector games; + + htmlDocPtr doc = htmlReadMemory(htmlContent.c_str(), htmlContent.size(), nullptr, nullptr, HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING); + if (!doc) { + std::cerr << "Error: unable to parse HTML document\n"; + return games; + } + + xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); + if (!xpathCtx) { + std::cerr << "Error: unable to create XPath context\n"; + xmlFreeDoc(doc); + return games; + } + + xmlXPathObjectPtr result = xmlXPathEvalExpression(reinterpret_cast("//tr[td/a]"), xpathCtx); + if (!result) { + std::cerr << "Error: unable to evaluate XPath expression\n"; + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + return games; + } + + for (int i = 0; i < result->nodesetval->nodeNr; ++i) { + xmlNodePtr row = result->nodesetval->nodeTab[i]; + Game game; + + xmlNodePtr titleNode = row->children; + while (titleNode && titleNode->type != XML_ELEMENT_NODE) { + titleNode = titleNode->next; + } + + if (titleNode && xmlStrEqual(titleNode->name, reinterpret_cast("td"))) { + xmlNodePtr aNode = titleNode->children; + while (aNode && aNode->type != XML_ELEMENT_NODE) { + aNode = aNode->next; + } + if (aNode && xmlStrEqual(aNode->name, reinterpret_cast("a"))) { + game.title = reinterpret_cast(xmlNodeGetContent(aNode)); + xmlChar* href = xmlGetProp(aNode, reinterpret_cast("href")); + if (href) { + game.url = reinterpret_cast(href); + xmlFree(href); + } + } + } + + xmlNodePtr regionNode = titleNode->next; + if (regionNode && regionNode->type == XML_ELEMENT_NODE && xmlStrEqual(regionNode->name, reinterpret_cast("td"))) { + xmlNodePtr imgNode = regionNode->children; + if (imgNode && imgNode->type == XML_ELEMENT_NODE && xmlStrEqual(imgNode->name, reinterpret_cast("img"))) { + game.region = reinterpret_cast(xmlGetProp(imgNode, reinterpret_cast("title"))); + } + } + + xmlNodePtr versionNode = regionNode->next; + if (versionNode && versionNode->type == XML_ELEMENT_NODE && xmlStrEqual(versionNode->name, reinterpret_cast("td"))) { + game.version = reinterpret_cast(xmlNodeGetContent(versionNode)); + } + + xmlNodePtr languagesNode = versionNode->next; + if (languagesNode && languagesNode->type == XML_ELEMENT_NODE && xmlStrEqual(languagesNode->name, reinterpret_cast("td"))) { + game.languages = reinterpret_cast(xmlNodeGetContent(languagesNode)); + } + + xmlNodePtr ratingNode = languagesNode->next; + if (ratingNode && ratingNode->type == XML_ELEMENT_NODE && xmlStrEqual(ratingNode->name, reinterpret_cast("td"))) { + xmlNodePtr aNode = ratingNode->children; + if (aNode && aNode->type == XML_ELEMENT_NODE && xmlStrEqual(aNode->name, reinterpret_cast("a"))) { + game.rating = reinterpret_cast(xmlNodeGetContent(aNode)); + } + } + + games.push_back(game); + } + + xmlXPathFreeObject(result); + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + + return games; +} + +int downloadGame(std::string console, const std::string &htmlContent) { + xmlInitParser(); + LIBXML_TEST_VERSION + + htmlDocPtr doc = htmlReadMemory(htmlContent.c_str(), htmlContent.size(), nullptr, nullptr, HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING); + if (doc == nullptr) { + std::cerr << "Failed to parse HTML" << std::endl; + return -1; + } + + xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); + if (xpathCtx == nullptr) { + std::cerr << "Failed to create XPath context" << std::endl; + xmlFreeDoc(doc); + return -1; + } + + xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar*)"//form[@id='dl_form']/input[@name='mediaId']", xpathCtx); + if (xpathObj == nullptr) { + std::cerr << "Failed to evaluate XPath expression" << std::endl; + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + return -1; + } + + std::string mediaId; + if (xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) { + xmlNodePtr node = xpathObj->nodesetval->nodeTab[0]; + xmlChar* value = xmlGetProp(node, (const xmlChar*)"value"); + if (value) { + mediaId = (const char*)value; + xmlFree(value); + } + } + + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(xpathCtx); + xmlFreeDoc(doc); + xmlCleanupParser(); + + std::cout << "Extracted mediaId: " << mediaId << std::endl; + + auto it = systemToRomFolder.find(console); + if (it == systemToRomFolder.end()) { + std::cerr << "Unsupported console: " << console << std::endl; + return -1; + } + std::string romFolder = it->second; + + std::string downloadUrl = "https://download2.vimm.net/?mediaId=" + mediaId; + std::string outputPath = "/mnt/SDCARD/Roms/" + romFolder + "/" + mediaId + ".zip"; + + void* handle = dlopen("/usr/lib/libcurl.so.4", RTLD_LAZY); + if (!handle) { + std::cerr << "Failed to load libcurl: " << dlerror() << std::endl; + return -1; + } + + auto curl_easy_init = (curl_easy_init_t)dlsym(handle, "curl_easy_init"); + auto curl_easy_cleanup = (curl_easy_cleanup_t)dlsym(handle, "curl_easy_cleanup"); + auto curl_easy_setopt = (curl_easy_setopt_t)dlsym(handle, "curl_easy_setopt"); + auto curl_easy_perform = (curl_easy_perform_t)dlsym(handle, "curl_easy_perform"); + auto curl_easy_strerror = (curl_easy_strerror_t)dlsym(handle, "curl_easy_strerror"); + auto curl_slist_append = (curl_slist_append_t)dlsym(handle, "curl_slist_append"); + + if (!curl_easy_init || !curl_easy_cleanup || !curl_easy_setopt || !curl_easy_perform) { + std::cerr << "Failed to resolve libcurl functions." << std::endl; + dlclose(handle); + return -1; + } + + CURL* curl = curl_easy_init(); + if (curl) { + FILE* fp = fopen(outputPath.c_str(), "wb"); + if (!fp) { + std::cerr << "Failed to open file for writing: " << outputPath << std::endl; + curl_easy_cleanup(curl); + dlclose(handle); + return -1; + } + + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "GET /?mediaId=33028 HTTP/1.1"); + headers = curl_slist_append(headers, "Host: download2.vimm.net"); + headers = curl_slist_append(headers, "Sec-Ch-Ua: \"Not;A=Brand\";v=\"24\", \"Chromium\";v=\"128\""); + headers = curl_slist_append(headers, "Sec-Ch-Ua-Mobile: ?0"); + headers = curl_slist_append(headers, "Sec-Ch-Ua-Platform: \"Windows\""); + headers = curl_slist_append(headers, "Accept-Language: en-GB,en;q=0.9"); + headers = curl_slist_append(headers, "Upgrade-Insecure-Requests: 1"); + headers = curl_slist_append(headers, "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36"); + headers = curl_slist_append(headers, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"); + headers = curl_slist_append(headers, "Sec-Fetch-Site: same-site"); + headers = curl_slist_append(headers, "Sec-Fetch-Mode: navigate"); + headers = curl_slist_append(headers, "Sec-Fetch-User: ?1"); + headers = curl_slist_append(headers, "Sec-Fetch-Dest: document"); + headers = curl_slist_append(headers, "Referer: https://vimm.net/vault/40297"); + headers = curl_slist_append(headers, "Accept-Encoding: gzip, deflate, br"); + headers = curl_slist_append(headers, "Priority: u=0, i"); + headers = curl_slist_append(headers, "Connection: keep-alive"); + + curl_easy_setopt(curl, 10023, headers); + curl_easy_setopt(curl, 10002, downloadUrl.c_str()); + curl_easy_setopt(curl, 64, 0L); // Disable SSL verification + curl_easy_setopt(curl, 81, 0L); // Disable host verification + curl_easy_setopt(curl, 10001, fp); + curl_easy_setopt(curl, 20011, NULL); + curl_easy_setopt(curl, 10029, &outputPath); // WRITEHHEADER + + curl_easy_setopt(curl, 20219, progressCallback); + curl_easy_setopt(curl, 43, 0L); + + + std::string filename; + curl_easy_setopt(curl, 20079, header_callback); + curl_easy_setopt(curl, 10029, &filename); + + int res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + fclose(fp); + dlclose(handle); + + if (!filename.empty()) { + std::string newOutputPath = "/mnt/SDCARD/Roms/" + romFolder + "/" + filename; + if (rename(outputPath.c_str(), newOutputPath.c_str()) == 0) { + std::cout << "File renamed to: " << filename << std::endl; + } else { + std::cerr << "Failed to rename file: " << strerror(errno) << std::endl; + } + } + + if (res != 0) { + std::cerr << "Failed to download game: " << curl_easy_strerror(res) << std::endl; + return -1; + } else { + std::cout << "Game downloaded successfully to " << outputPath << std::endl; + } + + } else { + dlclose(handle); + std::cerr << "Failed to initialize curl" << std::endl; + return -1; + } + + return 0; +} \ No newline at end of file