494 lines
16 KiB
C++
494 lines
16 KiB
C++
#include "Game.h"
|
|
#include "Environment.h"
|
|
#include "utils/Utils.h"
|
|
#include <iostream>
|
|
#ifdef __ANDROID__
|
|
#include <android/log.h>
|
|
#endif
|
|
|
|
#ifdef EMSCRIPTEN
|
|
#include <emscripten.h>
|
|
#include <emscripten/html5.h>
|
|
#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<bool>();
|
|
}
|
|
|
|
if (settingsRoot.contains("isHighDPIEnabled")) {
|
|
FRG::Environment::isHighDPIEnabled = settingsRoot["isHighDPIEnabled"].get<bool>();
|
|
}
|
|
|
|
if (settingsRoot.contains("enableLogging")) {
|
|
FRG::Environment::enableLogging = settingsRoot["enableLogging"].get<bool>();
|
|
}
|
|
|
|
}
|
|
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<int>(logicalW * dpr);
|
|
int physicalH = static_cast<int>(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<int>(w), static_cast<int>(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 <jni.h>
|
|
#include <android/log.h>
|
|
|
|
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 |