/* ✨ +------------------------------------------------------+ ____/ \____ ✨/| Open source game framework licensed to freely use, | ✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | +--\ . . /--+ | | | ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | | ~~~🌊~~~~🌊~ | +------------------------------------------------------+ | SPACE πŸͺπŸ…± OX | / | 🌊 ~ ~~~~ ~~ |/ +-------------*/ #include "Game.hpp" Game::Game(std::initializer_list configuration_merge) { #include #include /* Changing directory is necessary for loading external resources in a macOS app bundle. This may be useful on other platforms * too, so it may make more sense to be configurable. */ #if defined(__MACOS__) fs::current_path(SDL_GetBasePath()); #endif /* Set custom log function that prints to stdout/stderr and to file if enabled. Temporarily set to DEBUG priority before loading * user setting. */ SDL_LogSetOutputFunction(&Game::sdl_log_override, this); SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_DEBUG); /* Merge user configuration into the existing configuration and turn on auto refresh if it is enabled by the configuration. Merge * configuration file paths passed into the contructor as well. */ _configuration.merge(user_config_path); #ifdef __ANDROID__ _configuration.merge(configuration()("configuration", "android config path").get()); #elif defined(__EMSCRIPTEN__) _configuration.merge(configuration()("configuration", "wasm config path").get()); #endif for (const std::string& extra_configuration_path : configuration_merge) { _configuration.merge(extra_configuration_path); } if (configuration()("configuration", "auto refresh")) { _configuration.enable_auto_refresh(user_config_path); } /* Set the appropriate priority level for the default log category. Change it to VERBOSE if it is requested. Otherwise, * change it to INFO if the user does not want debug statements, or leave it at DEBUG if not. */ if (_configuration("log", "verbose to stdout")) { SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_VERBOSE); } else if (!configuration()["log"]["debug-to-file"] && !configuration()["log"]["debug-to-stdout"]) { SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_INFO); } /* If recording is enabled by configuration, activate it. */ if (configuration()("recording", "enabled")) { recorder.animation.play(); } /* Log the current working directory as seen by std::filesystem */ std::ostringstream log_message; log_message << "Current path as seen by std::filesystem is " << fs::current_path(); sb::Log::log(log_message); /* Load the key mappings into the input manager object */ input.load_key_map(); /* Log Android storage paths as determined by SDL */ #if defined(__ANDROID__) || defined(ANDROID) log_message = std::ostringstream(); log_message << "Using Android SDK version " << SDL_GetAndroidSDKVersion() << std::endl; log_message << "SDL_AndroidGetInternalStoragePath() is " << SDL_AndroidGetInternalStoragePath() << std::endl; log_message << "SDL_AndroidGetExternalStorageState() is " << SDL_AndroidGetExternalStorageState() << " (1=read, 2=write, 3=r/w)" << std::endl; log_message << "SDL_AndroidGetExternalStoragePath() is " << SDL_AndroidGetExternalStoragePath(); sb::Log::log(log_message); #endif /* Pretty print config JSON to debug log */ log_message = std::ostringstream(); log_message << std::setw(4) << configuration()() << std::endl; sb::Log::log(log_message, sb::Log::DEBUG); /* Tell SDL which render driver you will be requesting (not sure whether this only affects SDL_ GL */ SDL_SetHint(SDL_HINT_RENDER_DRIVER, configuration()["display"]["render driver"].get().c_str()); /* Initialize the buffer of frame lengths which will be used to calculate and display FPS */ frame_length_history.reserve(5000); /* Subscribe to events */ _delegate.subscribe(&Game::handle_quit_event, this, SDL_QUIT); _delegate.subscribe(&Game::respond, this); /* Needed for displaying fullscreen correctly on Linux (?) Also might need SDL_VIDEO_CENTERED (?) */ std::string fullscreen_env_assigment = "SDL_VIDEO_X11_LEGACY_FULLSCREEN=0"; putenv(const_cast(fullscreen_env_assigment.c_str())); /* log compiled and linked SDL versions */ SDL_version version; log_message = std::ostringstream(); SDL_VERSION(&version); log_message << "compiled against SDL " << static_cast(version.major) << "." << static_cast(version.minor) << "." << static_cast(version.patch) << std::endl; SDL_GetVersion(&version); log_message << "linked to SDL " << static_cast(version.major) << "." << static_cast(version.minor) << "." << static_cast(version.patch); sb::Log::log(log_message); /* allows use of our own main function (?) see SDL_SetMainReady.html */ SDL_SetMainReady(); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { sb::Log::sdl_error("SDL could not initialize"); flag_to_end(); } log_message = std::ostringstream(); /* Android and macOS do not use GLEW */ #if !defined(__ANDROID__) && !defined(ANDROID) && !defined(__MACOS__) log_message << "GLEW " << glewGetString(GLEW_VERSION); #endif sb::Log::log(log_message.str()); glm::ivec2 window_size = _configuration("display", "dimensions").get(); /* Set GL context attributes before creating a window (see SDL_GLattr.html). Don't ask Emscripten or Android for a specific GL * context version. */ #if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID) SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, _configuration("gl", "major version")); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, _configuration("gl", "minor version")); #endif SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, _configuration("gl", "depth size")); SDL_GL_SetAttribute(SDL_GL_RED_SIZE, _configuration("gl", "red size")); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, _configuration("gl", "green size")); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, _configuration("gl", "blue size")); SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, _configuration("gl", "share with current context")); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, _configuration("gl", "double buffer")); /* Use compatibility mode on Windows because it seemed to be required on one version of Windows tested */ #if defined(__MINGW32__) SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); #elif defined(__MACOS__) SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); #endif /* Create a window with dimensions set in the config, centered, and flagged to be usable in OpenGL context */ Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE; if (_configuration("display", "fullscreen")) { flags |= SDL_WINDOW_FULLSCREEN; } _window = SDL_CreateWindow(_configuration("display", "title").get_ref().c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, window_size.x, window_size.y, flags); if (_window == nullptr) { sb::Log::sdl_error("Could not create window"); flag_to_end(); } /* Create a single GL context for the game. This is restrictive since it forces the game to use only one context, so it should * be improved in the future. */ if ((glcontext = SDL_GL_CreateContext(window())) == nullptr) { sb::Log::sdl_error("Could not get GL context"); flag_to_end(); } /* Clear window to black */ glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); SDL_GL_SwapWindow(_window); /* Try setting vsync */ bool vsync_enabled = configuration()("display", "vsync").get(); if (SDL_GL_SetSwapInterval(static_cast(vsync_enabled)) == 0) { std::ostringstream message; message << "Set vsync to " << vsync_enabled; sb::Log::log(message); } else { sb::Log::log("Setting vysnc is not supported"); } /* Initialize GLEW for GL function discovery on all platforms except Android and macOS */ #if !defined(__ANDROID__) && !defined(ANDROID) && !defined(__MACOS__) GLenum error = glewInit(); if (error != GLEW_OK) { std::ostringstream message; message << "GLEW could not initialize " << glewGetErrorString(error); sb::Log::log(message, sb::Log::ERR); } #endif /* Log the GL profile that was chosen */ log_gl_properties(); log_display_mode(); /* Show or hide cursor depending on the configuration */ SDL_ShowCursor(_configuration("display", "show cursor")); /* Initialize SDL TTF font library */ if (TTF_Init() < 0) { sb::Log::sdl_error("Could not initialize SDL ttf"); flag_to_end(); } else { SDL_Log("initialized SDL ttf %d.%d.%d", SDL_TTF_MAJOR_VERSION, SDL_TTF_MINOR_VERSION, SDL_TTF_PATCHLEVEL); } /* Try to load the default font path. The font will be freed when the Game object is destroyed. */ _font = font(configuration()("display", "default font path").get(), configuration()("display", "default font size")); /* Initialize SDL mixer with OGG support (in addition to WAV) */ if (Mix_Init(MIX_INIT_OGG) == 0) { sb::Log::sdl_error("Could not initialize SDL mixer"); } else { SDL_Log("initialized SDL mixer %d.%d.%d", SDL_MIXER_MAJOR_VERSION, SDL_MIXER_MINOR_VERSION, SDL_MIXER_PATCHLEVEL); /* Log the available audio devices found on the system */ const int audio_device_count = SDL_GetNumAudioDevices(SDL_TRUE); for (int ii = 0; ii < audio_device_count; ii++) { std::ostringstream message; message << "Found audio capture device " << ii << ": " << SDL_GetAudioDeviceName(ii, SDL_TRUE); sb::Log::log(message); } /* Open the audio device chosen automatically by SDL mixer */ if (Mix_OpenAudio(_configuration("audio", "frequency"), MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, _configuration("audio", "chunk size")) < 0) { sb::Log::sdl_error("Could not set up audio"); } else { SDL_Log("Using audio driver: %s", SDL_GetCurrentAudioDriver()); } } #if SDL_BYTEORDER == SDL_BIG_ENDIAN sb::Log::log("big endian"); #else sb::Log::log("little endian"); #endif last_frame_timestamp = SDL_GetTicks(); } void Game::sdl_log_override(void* userdata, int category, SDL_LogPriority priority, const char* message) { Game* game = static_cast(userdata); std::ostream& out = (priority > SDL_LOG_PRIORITY_WARN) ? std::cerr : std::cout; /* Print to stdout/stderr if priority is higher than debug or debug statements are enabled */ if (priority > SDL_LOG_PRIORITY_DEBUG || game->configuration()["log"]["debug-to-stdout"]) { out << message << std::endl; /* If printing to stdout, print to Android log as well */ #if defined(__ANDROID__) || defined(ANDROID) __android_log_print(ANDROID_LOG_VERBOSE, game->configuration()["log"]["short-name"].get_ref().c_str(), "%s", message); #endif } /* Create a date + time timestamp */ std::ostringstream timestamp; std::time_t now = std::time(nullptr); timestamp << std::put_time(std::localtime(&now), "%F %T "); /* Handle writing to log file */ if (game->configuration()["log"]["enabled"]) { fs::path path = game->configuration()("log", "output-directory").get(); if (!fs::exists(path)) { fs::create_directories(path); } /* Prepend timestamp to the message */ std::stringstream stamped_message; stamped_message << timestamp.str() << message; /* If debug is enabled, append message to debug log file */ if (game->configuration()["log"]["debug-to-file"]) { fs::path debug_path = path / game->configuration()("log", "debug-file-name").get(); std::ofstream debug_stream(debug_path, std::ios_base::app); debug_stream << stamped_message.str() << std::endl; } /* Only append messages to the info log that are higher than debug priority */ if (priority > SDL_LOG_PRIORITY_DEBUG) { fs::path info_path = path / game->configuration()("log", "info-file-name").get(); std::ofstream info_stream(info_path, std::ios_base::app); info_stream << stamped_message.str() << std::endl; } } } void Game::print_frame_length_history() { for (float& frame_length : frame_length_history) { std::cout << frame_length << ", "; } std::cout << std::endl; } /* Create, compile and return the ID of a GL shader from the GLSL code at path */ GLuint Game::load_shader(const fs::path& path, GLenum type) const { GLuint shader = glCreateShader(type); std::ostringstream message; std::string contents = sb::file_to_string(path); const char *c_str = contents.c_str(); glShaderSource(shader, 1, &c_str, 0); glCompileShader(shader); GLint is_compiled; glGetShaderiv(shader, GL_COMPILE_STATUS, &is_compiled); if (is_compiled == GL_TRUE) { message << "compiled shader at " << path; sb::Log::log(message); return shader; } else { /* log error by allocating a string to copy the GL error message buffer into */ std::string error_info; GLint max_length; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length); error_info.resize(max_length, 0); glGetShaderInfoLog(shader, error_info.size(), nullptr, error_info.data()); message << "failed to compile " << path << ": " << error_info; sb::Log::log(message, sb::Log::Level::ERR); return -1; } } bool Game::link_shader(GLuint program) const { glLinkProgram(program); int is_linked; glGetProgramiv(program, GL_LINK_STATUS, (int *) &is_linked); std::ostringstream message; if (is_linked == GL_TRUE) { message << "linked shader program " << program; sb::Log::log(message); return true; } else { /* log error by allocating a string to copy the GL error message buffer into */ std::string error_info; GLint max_length; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &max_length); error_info.resize(max_length, 0); glGetProgramInfoLog(program, error_info.size(), nullptr, error_info.data()); message << "failed linking shader program " << program << ": " << error_info; sb::Log::log(message, sb::Log::Level::ERR); return false; } } void Game::log_display_mode() const { SDL_DisplayMode current; std::ostringstream message; for (int ii = 0; ii < SDL_GetNumVideoDisplays(); ii++) { int mode = SDL_GetCurrentDisplayMode(ii, ¤t); if (mode != 0) { message << "Could not get display mode for video display #" << ii; sb::Log::sdl_error(message.str()); } else { if (ii > 0) { message << ", "; } message << "Display #" << ii << ": display mode is " << current.w << "x" << current.h << "px @ " << current.refresh_rate << "hz " << SDL_GetPixelFormatName(current.format); } } sb::Log::log(message); } void Game::log_gl_properties() const { std::ostringstream message; message << "OpenGL " << glGetString(GL_VERSION) << ", renderer " << glGetString(GL_RENDERER) << ", shading language " << glGetString(GL_SHADING_LANGUAGE_VERSION) << ", vendor " << glGetString(GL_VENDOR); int attribute; int status = SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &attribute); if (!status) { message << ", SDL_GL_RED_SIZE: requested " << _configuration("gl", "red size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_RED_SIZE"); } status = SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &attribute); if (!status) { message << ", SDL_GL_GREEN_SIZE: requested " << _configuration("gl", "green size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_GREEN_SIZE"); } status = SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &attribute); if (!status) { message << ", SDL_GL_BLUE_SIZE: requested " << _configuration("gl", "blue size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_BLUE_SIZE"); } status = SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &attribute); if (!status) { message << ", SDL_GL_DEPTH_SIZE: requested " << _configuration("gl", "depth size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_DEPTH_SIZE"); } sb::Log::log(message); /* This block fails on macOS */ #if !defined(__MACOS__) std::ostringstream debug_message; debug_message << "OpenGL extensions: " << glGetString(GL_EXTENSIONS); sb::Log::log(message, sb::Log::DEBUG); #endif } void Game::log_surface_format(SDL_Surface* surface, std::string preface) { SDL_PixelFormat* format = surface->format; std::string pixel_format = SDL_GetPixelFormatName(format->format); SDL_Log("%s bpp: %i mask: %i %i %i %i format: %s", preface.c_str(), format->BytesPerPixel, format->Rmask, format->Gmask, format->Bmask, format->Amask, pixel_format.c_str()); } const Configuration& Game::configuration() const { return _configuration; } Configuration& Game::configuration() { return _configuration; } const sb::Delegate& sb::Game::delegate() const { return _delegate; } sb::Delegate& sb::Game::delegate() { return _delegate; } const SDL_Window* Game::window() const { return _window; } SDL_Window* Game::window() { return _window; } const Input& Game::get_input() const { return input; } Input& Game::get_input() { return input; } std::shared_ptr sb::Game::font() const { return _font; } std::shared_ptr sb::Game::font(const fs::path& path, int size) const { std::shared_ptr font = std::shared_ptr(TTF_OpenFont(path.string().c_str(), size), TTF_CloseFont); if (font.get() == nullptr) { std::ostringstream message; message << "Could not load " << path; sb::Log::log(message, sb::Log::ERR); if (path != configuration()("display", "default font path").get()) { return this->font(); } } else { std::ostringstream message; message << "Loaded " << path; sb::Log::log(message); } return font; } void Game::run() { /* Any events received before this discarded. This prevents input from being entered before the game starts. Input is also suspended * for a moment in case something is input as the screen is appearing. */ SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); suppress_input_temporarily(); /* In Emscripten builds, use the browser's requestAnimationFrame to call each frame until the game is quit. */ #if defined(__EMSCRIPTEN__) SDL_Log("using emscripten main loop"); emscripten_set_main_loop_arg(&loop, this, -1, true); /* Other builds request a frame repeatedly until the game quits, delaying for the configured amount of milliseconds between each * request. */ #else SDL_Log("using standard main loop"); while (!done) { frame(SDL_GetTicks()); SDL_Delay(configuration()("display", "sdl delay")); } #endif } void Game::respond(SDL_Event& event) { if (_delegate.compare(event, "screenshot")) { recorder.capture_screen(); } else if (_delegate.compare(event, "record")) { recorder.toggle(); } else if (_delegate.compare(event, "save current stash")) { recorder.grab_stash(); } else if (_delegate.compare(event, "log video memory size")) { recorder.log_video_memory_size(); } } void Game::frame(float timestamp) { /* Max framerate is the maximum number of frames to display per second. It is unlimited if the value is -1. */ int max_framerate = configuration()("display", "max framerate").get(); /* If max framerate is unlimited, always build a frame. Otherwise, check if enough milliseconds have passed since the last frame. */ if (max_framerate == -1 || timestamp - last_frame_timestamp >= 1000.0f / max_framerate) { /* Amount of time passed since the last timestamp was recorded in milliseconds. */ last_frame_length = timestamp - last_frame_timestamp; /* Constrain the size of the frame length history and add the milliseconds between this frame and the last. */ if (frame_length_history.size() == 5000) { frame_length_history.pop_back(); } frame_length_history.insert(frame_length_history.begin(), last_frame_length); /* Save the timestamp */ last_frame_timestamp = timestamp; /* Update game state and render frame (update and draw should be separated into different functions in a future revision to * allow for different timing intervals for each) */ float timestamp_seconds = timestamp / 1000.0f; recorder.update(timestamp_seconds); _delegate.dispatch(); input.unsuppress_animation.update(timestamp_seconds); update(timestamp_seconds); _configuration.update(timestamp_seconds); /* Update frame count per second for verbose log */ frame_count_this_second++; if (timestamp - last_frame_count_timestamp >= 1000) { current_frames_per_second = frame_count_this_second; last_frame_count_timestamp = timestamp; frame_count_this_second = 0; std::ostringstream message; message << "Counted " << current_frames_per_second << " frames last second"; sb::Log::log(message, sb::Log::VERBOSE); } } } #if defined(__EMSCRIPTEN__) void loop(void* context) { Game* game = static_cast(context); game->frame(emscripten_performance_now()); if (game->done) { emscripten_cancel_main_loop(); } } #endif void Game::flag_to_end() { done = true; } void Game::handle_quit_event(SDL_Event &event) { if (event.type == SDL_QUIT) { flag_to_end(); } } void Game::quit() { /* Unsubscribe all delegate object's subscribers. */ _delegate.unsubscribe(&display); _delegate.unsubscribe(&input); _delegate.unsubscribe(this); if (glcontext != nullptr) { SDL_GL_DeleteContext(glcontext); } if (_window != nullptr) { SDL_DestroyWindow(_window); } Mix_CloseAudio(); Mix_Quit(); SDL_Quit(); } Game::~Game() { /* Delete font before quitting the TTF library so the font's shared pointer will successfully call its delete * function in the TTF library. */ _font.reset(); if (TTF_WasInit()) { TTF_Quit(); } }