#include "Game.h" #include "Environment.h" #include "utils/Utils.h" #include #ifdef __ANDROID__ #include #endif #ifdef EMSCRIPTEN #include #include #endif #ifdef STEAMSDK #include "steam_api.h" #endif // Единый указатель на объект игры для всех платформ FRG::Game* g_game = nullptr; // Read the persisted fullscreen preference (if any) before creating the // window, so it opens directly in the right mode instead of flashing // windowed -> fullscreen once settings are loaded later during setupPart2(). void readSettings() { const std::string settingsContent = FRG::readSavedTextFile("settings.json"); if (!settingsContent.empty()) { try { const nlohmann::json settingsRoot = nlohmann::json::parse(settingsContent); if (settingsRoot.contains("fullscreen")) { FRG::Environment::isFullscreen = settingsRoot["fullscreen"].get(); } if (settingsRoot.contains("isHighDPIEnabled")) { FRG::Environment::isHighDPIEnabled = settingsRoot["isHighDPIEnabled"].get(); } if (settingsRoot.contains("enableLogging")) { FRG::Environment::enableLogging = settingsRoot["enableLogging"].get(); } } catch (const std::exception& e) { std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl; } } } #if defined(EMSCRIPTEN) static SDL_GLContext glContext_x; static bool g_isFsLoaded = false; // Флаг готовности static bool g_pendingFullscreenResize = false; // Макрос EMSCRIPTEN_KEEPALIVE экспортирует функцию в глобальный объект Module extern "C" void EMSCRIPTEN_KEEPALIVE onFileSystemLoaded() { // 1. Файлы загружены. Теперь можно читать настройки и логи // Если функции readSettings() нет для Emscripten, форсируем логгер: //FRG::Environment::enableLogging = true; //readSettings(); FRG::initLogger(); FRG::logger() << "[boot] File system is ready. Setting up game..." << std::endl; // 2. Инициализируем игру ТОЛЬКО когда файлы доступны g_game = new FRG::Game(); g_game->setup(); // 3. Даем отмашку главному циклу g_isFsLoaded = true; } static void applyResize(int logicalW, int logicalH) { // Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung) double dpr = emscripten_get_device_pixel_ratio(); //double dpr = 1; // low quality // Вычисляем реальные физические пиксели int physicalW = static_cast(logicalW * dpr); int physicalH = static_cast(logicalH * dpr); // Устанавливаем размер внутреннего буфера канваса emscripten_set_canvas_element_size("#canvas", physicalW, physicalH); // Сообщаем SDL о новом размере. // ВАЖНО: SDL2 в Emscripten ожидает здесь именно физические пиксели // для корректной работы последующих вызовов glViewport. if (FRG::Environment::window) { SDL_SetWindowSize(FRG::Environment::window, physicalW, physicalH); } // Пушим событие, чтобы движок пересчитал матрицы проекции SDL_Event e = {}; e.type = SDL_WINDOWEVENT; e.window.event = SDL_WINDOWEVENT_RESIZED; e.window.data1 = physicalW; e.window.data2 = physicalH; SDL_PushEvent(&e); FRG::logger() << "Resized, new size: " << logicalW << "x" << logicalH << " (physical: " << physicalW << "x" << physicalH << ", DPR: " << dpr << ")" << std::endl; } #endif void MainLoop() { #if defined(EMSCRIPTEN) // Крутим цикл вхолостую, пока JS читает IndexedDB if (!g_isFsLoaded) return; if (g_pendingFullscreenResize) { g_pendingFullscreenResize = false; // Теперь DOM перестроен, можно забирать актуальные размеры. // Если canvas должен занимать все окно браузера: int canvasW = EM_ASM_INT({ return window.innerWidth; }); int canvasH = EM_ASM_INT({ return window.innerHeight; }); applyResize(canvasW, canvasH); } #endif g_game->update(); } #ifdef EMSCRIPTEN EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { return EM_TRUE; } EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) { return EM_TRUE; } EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) { double w, h; emscripten_get_element_css_size("window", &w, &h); // Если "window" не поддерживается вашей версией SDK, используем данные из e: // int logicalW = e->windowInnerWidth; // int logicalH = e->windowInnerHeight; applyResize(static_cast(w), static_cast(h)); return EM_FALSE; } EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) { FRG::Environment::isFullscreen = e->isFullscreen; FRG::logger() << "onFullscreenChanged: isFullscreen = " << e->isFullscreen << std::endl; // Браузер еще не обновил DOM. Ставим флаг, чтобы применить размеры в следующем кадре. g_pendingFullscreenResize = true; return EM_FALSE; } int main(int argc, char* argv[]) { SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); FRG::Environment::window = SDL_CreateWindow("Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); glContext_x = SDL_GL_CreateContext(FRG::Environment::window); SDL_GL_MakeCurrent(FRG::Environment::window, glContext_x); emscripten_set_webglcontextlost_callback("#canvas", nullptr, EM_TRUE, onWebGLContextLost); emscripten_set_webglcontextrestored_callback("#canvas", nullptr, EM_TRUE, onWebGLContextRestored); emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized); emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onFullscreenChanged); int canvasW = EM_ASM_INT({ return window.innerWidth; }); int canvasH = EM_ASM_INT({ return window.innerHeight; }); applyResize(canvasW, canvasH); SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); // Запускаем асинхронное монтирование ФС FRG::emscriptenInitFileSystem(); // Начинаем крутить пустой цикл. Он активируется, когда отработает onFileSystemLoaded emscripten_set_main_loop(MainLoop, 0, 1); return 0; } #endif #ifdef __ANDROID__ #include #include void Android_InitTouchDevices() { JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); if (!env) { __android_log_print(ANDROID_LOG_ERROR, "Game", "[Android] Failed to get JNIEnv for touch init"); return; } jclass activityClass = env->FindClass("org/libsdl/app/SDLActivity"); if (!activityClass) { __android_log_print(ANDROID_LOG_ERROR, "Game", "[Android] Failed to find SDLActivity class"); return; } jmethodID initTouchMethod = env->GetStaticMethodID(activityClass, "initTouch", "()V"); if (!initTouchMethod) { __android_log_print(ANDROID_LOG_ERROR, "Game", "[Android] Failed to find initTouch method"); return; } env->CallStaticVoidMethod(activityClass, initTouchMethod); __android_log_print(ANDROID_LOG_INFO, "Game", "[Android] JNI initTouch() successfully invoked"); } // Переменная для отслеживания фокуса приложения на Android bool isAppActive = true; int main(int argc, char* argv[]) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { __android_log_print(ANDROID_LOG_ERROR, "Game", "SDL init failed: %s", SDL_GetError()); return 1; } SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); // Передаем 0, 0 — на Android SDL автоматически растянет окно // на весь экран с учетом системной ориентации Активити FRG::Environment::window = SDL_CreateWindow( "Shadow Over Bishkek", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE ); if (!FRG::Environment::window) { __android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError()); SDL_Quit(); return 1; } SDL_GLContext ctx = SDL_GL_CreateContext(FRG::Environment::window); if (!ctx) { __android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError()); SDL_DestroyWindow(FRG::Environment::window); SDL_Quit(); return 1; } if (SDL_GL_MakeCurrent(FRG::Environment::window, ctx) != 0) { __android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_MakeCurrent failed: %s", SDL_GetError()); SDL_GL_DeleteContext(ctx); SDL_DestroyWindow(FRG::Environment::window); SDL_Quit(); return 1; } // СТРОГО ТУТ: Запрашиваем у созданного окна реальный размер буфера // Это исключит баг перевернутых координат ширины и высоты int realWidth, realHeight; SDL_GL_GetDrawableSize(FRG::Environment::window, &realWidth, &realHeight); FRG::Environment::width = realWidth; FRG::Environment::height = realHeight; __android_log_print(ANDROID_LOG_INFO, "Game", "Actual Render Resolution: %dx%d", FRG::Environment::width, FRG::Environment::height); // Теперь, когда окно гарантированно создано в правильной ориентации, // привязываем тач-устройства на стороне Java Android_InitTouchDevices(); if (SDL_GL_SetSwapInterval(1) < 0) { __android_log_print(ANDROID_LOG_WARN, "Game", "Unable to set VSync: %s", SDL_GetError()); } try { g_game = new FRG::Game(); g_game->setup(); } catch (const std::exception& e) { __android_log_print(ANDROID_LOG_ERROR, "Game", "Game setup failed: %s", e.what()); if (g_game) { delete g_game; g_game = nullptr; } SDL_GL_DeleteContext(ctx); SDL_DestroyWindow(FRG::Environment::window); SDL_Quit(); return 1; } while (!g_game->shouldExit()) { g_game->update(); if (!isAppActive) { // Если игра свернута — спим, чтобы не нагружать CPU/GPU SDL_Delay(100); } } delete g_game; g_game = nullptr; SDL_GL_DeleteContext(ctx); SDL_DestroyWindow(FRG::Environment::window); SDL_Quit(); return 0; } #endif #ifndef __ANDROID__ #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__linux__) int main(int argc, char* argv[]) { SDL_GLContext ctx = nullptr; try { readSettings(); if (FRG::Environment::enableLogging) { FRG::initLogger(); } FRG::logger() << "Log started!" << std::endl; #ifdef STEAMSDK // Передаем явный App ID демо-версии if (SteamAPI_RestartAppIfNecessary(4945840)) { return 0; } if (!SteamAPI_Init()) { std::cerr << "SteamAPI_Init() failed! Steam client might not be running.\n"; return 1; } #endif SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); constexpr int CONST_WIDTH = FRG::Environment::CONST_DEFAULT_WIDTH; constexpr int CONST_HEIGHT = FRG::Environment::CONST_DEFAULT_HEIGHT; FRG::Environment::width = CONST_WIDTH; FRG::Environment::height = CONST_HEIGHT; if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { SDL_Log("SDL init failed: %s", SDL_GetError()); return 1; } SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); Uint32 windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; if (FRG::Environment::isFullscreen) { windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP; } SDL_DisplayMode dm; if (SDL_GetCurrentDisplayMode(0, &dm) == 0) { FRG::logger() << "Desktop resolution: " << dm.w << "x" << dm.h << " @ " << dm.refresh_rate << "Hz\n"; } else { SDL_Log("SDL_GetCurrentDisplayMode failed: %s", SDL_GetError()); } int windowWidth = FRG::Environment::width; int windowHeight = FRG::Environment::height; if (dm.w <= 1280 && dm.h <= 720) { windowWidth = round(dm.w / 1.5); windowHeight = round(dm.h / 1.5); } FRG::Environment::window = SDL_CreateWindow( "Shadow Over Bishkek Demo", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, windowWidth, windowHeight, windowFlags ); ctx = SDL_GL_CreateContext(FRG::Environment::window); SDL_GL_MakeCurrent(FRG::Environment::window, ctx); if (FRG::Environment::isHighDPIEnabled) { int drawW, drawH; SDL_GL_GetDrawableSize(FRG::Environment::window, &drawW, &drawH); FRG::Environment::width = drawW; FRG::Environment::height = drawH; FRG::logger() << "HiDPI enabled, drawable size: " << drawW << "x" << drawH << std::endl; } else { int winW, winH; SDL_GetWindowSize(FRG::Environment::window, &winW, &winH); FRG::Environment::width = winW; FRG::Environment::height = winH; FRG::logger() << "HiDPI disabled, window size: " << winW << "x" << winH << std::endl; } // Динамическое создание объекта игры g_game = new FRG::Game(); g_game->setup(); while (!g_game->shouldExit()) { g_game->update(); SDL_Delay(2); #ifdef STEAMSDK SteamAPI_RunCallbacks(); #endif } //FRG::logger() << "Quitting step 1: " << std::endl; // Явное уничтожение объекта игры ДО завершения работы Steamworks delete g_game; //FRG::logger() << "Quitting step 2: " << std::endl; g_game = nullptr; } catch (const std::exception& e) { std::cerr << "Exception caught: " << e.what() << std::endl; if (g_game) { delete g_game; g_game = nullptr; } } catch (...) { std::cerr << "Unknown exception happened: " << std::endl; if (g_game) { delete g_game; g_game = nullptr; } } //FRG::logger() << "Quitting step 3: " << std::endl; #ifdef STEAMSDK // Деинициализация Steamworks происходит только тогда, когда все объекты игры гарантированно уничтожены SteamAPI_Shutdown(); #endif //FRG::logger() << "Quitting step 4: " << std::endl; // Очистка ресурсов SDL if (ctx) { //FRG::logger() << "Quitting step 5: " << std::endl; SDL_GL_DeleteContext(ctx); } //FRG::logger() << "Quitting step 6: " << std::endl; if (FRG::Environment::window) { //FRG::logger() << "Quitting step 7: " << std::endl; SDL_DestroyWindow(FRG::Environment::window); } //FRG::logger() << "Quitting step 8: " << std::endl; #ifndef EMSCRIPTEN // In Emscripten, SDL must stay alive across context loss/restore cycles // so the window remains valid when the game object is re-created. SDL_Quit(); #endif //FRG::logger() << "Quitting step 9: " << std::endl; return 0; } #endif #endif