#include "Game.hpp" Game::Game() { /* Set the appropriate priority level for the default log category so either info level messages * and higher are enabled or debug level messages are enabled, depending on the global configuration */ SDL_LogPriority default_log_category_priority; if (configuration()["log"]["debug-to-file"] || configuration()["log"]["debug-to-stdout"]) { default_log_category_priority = SDL_LOG_PRIORITY_DEBUG; } else { default_log_category_priority = SDL_LOG_PRIORITY_INFO; } SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, default_log_category_priority); /* set custom log function that prints to stdout/stderr and to file if enabled */ SDL_LogSetOutputFunction(&Game::sdl_log_override, this); /* pretty print config to debug log */ std::ostringstream log_message; 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 when calling SDL_CreateRenderer */ 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 FPS */ frame_length_history.reserve(5000); set_framerate(configuration()["display"]["framerate"]); delegate.subscribe(&Game::handle_quit_event, this, SDL_QUIT); /* 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_AUDIO) < 0) { sb::Log::sdl_error("SDL could not initialize"); flag_to_end(); } log_message = std::ostringstream(); log_message << "GLEW " << glewGetString(GLEW_VERSION); sb::Log::log(log_message.str()); glm::ivec2 window_size = configuration()["display"]["dimensions"].get(); /* Create a window with dimensions set in the config, centered, and flagged to be usable in OpenGL context */ _window = SDL_CreateWindow( configuration()["display"]["title"].get_ref().c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, window_size.x, window_size.y, SDL_WINDOW_OPENGL); if (_window == nullptr) { sb::Log::sdl_error("Could not create window"); flag_to_end(); } /* Create an SDL renderer for clearing the screen to black and for logging renderer properties. Destroy renderer when finished. * Skip this in emscripten because it causes a mouse event bug. */ #ifndef __EMSCRIPTEN__ if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr) { sb::Log::sdl_error("Could not create renderer"); flag_to_end(); } else { int w, h; /* log the renderer resolution */ SDL_GetRendererOutputSize(renderer, &w, &h); log_message = std::ostringstream(); log_message << "renderer output size is " << w << "x" << h; sb::Log::log(log_message); /* clear screen to black */ SDL_SetRenderTarget(renderer, nullptr); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); SDL_RenderPresent(renderer); SDL_RenderFlush(renderer); SDL_DestroyRenderer(renderer); } #endif SDL_ShowCursor(configuration()["display"]["show-cursor"]); 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); } if ((bp_mono_font = TTF_OpenFont("BPmono.ttf", 14)) == nullptr) { sb::Log::log("Could not load BPmono.ttf", sb::Log::ERROR); } if (Mix_Init(MIX_INIT_OGG) == 0) { sb::Log::sdl_error("Could not initialize SDL mixer"); // flag_to_end(); } else { SDL_Log("initialized SDL mixer %d.%d.%d", SDL_MIXER_MAJOR_VERSION, SDL_MIXER_MINOR_VERSION, SDL_MIXER_PATCHLEVEL); } // if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, // MIX_DEFAULT_CHANNELS, 1024) < 0) if (Mix_OpenAudio(11025, AUDIO_U8, MIX_DEFAULT_CHANNELS, 2048) < 0) { sb::Log::sdl_error("Could not set up audio"); } SDL_Log("Using audio driver: %s", SDL_GetCurrentAudioDriver()); 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); } audio.load_sfx(); audio.load_bgm(); #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::load_sdl_context() { if (glcontext != nullptr) { SDL_GL_DeleteContext(glcontext); glcontext = nullptr; } SDL_RendererInfo renderer_info; int render_driver_count = SDL_GetNumRenderDrivers(); SDL_Log("Render drivers:"); for (int ii = 0; ii < render_driver_count; ii++) { SDL_GetRenderDriverInfo(ii, &renderer_info); log_renderer_info(renderer_info); } if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr) { sb::Log::sdl_error("Could not create renderer"); flag_to_end(); } else { SDL_Log("Current renderer:"); SDL_GetRendererInfo(renderer, &renderer_info); log_renderer_info(renderer_info); SDL_Log("Renderer supports the use of render targets? %d", SDL_RenderTargetSupported(renderer)); } is_gl_context = false; log_display_mode(); } void Game::load_gl_context() { if (renderer != nullptr) { SDL_DestroyRenderer(renderer); renderer = nullptr; } #ifndef __EMSCRIPTEN__ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); #endif SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); if ((glcontext = SDL_GL_CreateContext(_window)) == nullptr) { sb::Log::sdl_error("Could not get GL context"); flag_to_end(); } /* try enabling vsync */ if (SDL_GL_SetSwapInterval(1) == 0) { sb::Log::log("enabled vsync"); } else { sb::Log::log("vsync not supported"); } GLenum error = glewInit(); std::ostringstream message; if (error != GLEW_OK) { message << "GLEW could not initialize " << glewGetErrorString(error); sb::Log::log(message, sb::Log::ERROR); } message << "OpenGL " << glGetString(GL_VERSION) << ", renderer " << glGetString(GL_RENDERER) << ", shading language " << glGetString(GL_SHADING_LANGUAGE_VERSION); sb::Log::log(message); is_gl_context = true; log_display_mode(); } /* Overrides SDL's default log function to log a message to stdout/stderr and, if log is enabled in the * global configuration, to a file. Debug level statements may be suppressed, printed to stdout, or printed to * both stdout and file, depending on the global configuration. */ 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; } /* handle writing to log file */ if (game->configuration()["log"]["enabled"]) { fs::path path = game->configuration()["log"]["output-directory"]; if (!fs::exists(path)) { fs::create_directories(path); } /* prepend a timestamp to the message */ std::time_t now = std::time(nullptr); std::stringstream stamped_message; stamped_message << std::put_time(std::localtime(&now), "%F %T ") << 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"]; 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"]; 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::fstream file = std::fstream(path); std::ostringstream message; std::string contents = sb::file_to_string(path); glShaderSource(shader, 1, reinterpret_cast(&contents), 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::ERROR); 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::ERROR); return false; } } void Game::log_renderer_info(SDL_RendererInfo& info) { std::ostringstream message; message << "renderer name: " << info.name << ", flags: " << info.flags << ", texture formats: " << info.num_texture_formats << ", max texture w: " << info.max_texture_width << ", max texture h: " << info.max_texture_height; sb::Log::log(message); } /* Write resolution, monitor refresh rate, and pixel format to the log. Code taken from SDL_GetCurrentDisplayMode.html * on the SDL wiki */ void Game::log_display_mode() { 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 { message << "Display #" << ii << ": display mode is " << current.w << "x" << current.h << "px @ " << current.refresh_rate << "hz " << get_pixel_format_string(current.format); sb::Log::log(message); } } } void Game::log_surface_format(SDL_Surface* surface, std::string preface) { SDL_PixelFormat* format = surface->format; std::string pixel_format = get_pixel_format_string(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()); } std::string Game::get_pixel_format_string(Uint32 format) { std::string pixel_format; if (format == SDL_PIXELFORMAT_UNKNOWN) { pixel_format = "SDL_PIXELFORMAT_UNKNOWN"; } else if (format == SDL_PIXELFORMAT_INDEX1LSB) { pixel_format = "SDL_PIXELFORMAT_INDEX1LSB"; } else if (format == SDL_PIXELFORMAT_INDEX1MSB) { pixel_format = "SDL_PIXELFORMAT_INDEX1MSB"; } else if (format == SDL_PIXELFORMAT_INDEX4LSB) { pixel_format = "SDL_PIXELFORMAT_INDEX4LSB"; } else if (format == SDL_PIXELFORMAT_INDEX4MSB) { pixel_format = "SDL_PIXELFORMAT_INDEX4MSB"; } else if (format == SDL_PIXELFORMAT_INDEX8) { pixel_format = "SDL_PIXELFORMAT_INDEX8"; } else if (format == SDL_PIXELFORMAT_RGB332) { pixel_format = "SDL_PIXELFORMAT_RGB332"; } else if (format == SDL_PIXELFORMAT_RGB444) { pixel_format = "SDL_PIXELFORMAT_RGB444"; } else if (format == SDL_PIXELFORMAT_RGB555) { pixel_format = "SDL_PIXELFORMAT_RGB555"; } else if (format == SDL_PIXELFORMAT_BGR555) { pixel_format = "SDL_PIXELFORMAT_BGR555"; } else if (format == SDL_PIXELFORMAT_ARGB4444) { pixel_format = "SDL_PIXELFORMAT_ARGB4444"; } else if (format == SDL_PIXELFORMAT_RGBA4444) { pixel_format = "SDL_PIXELFORMAT_RGBA4444"; } else if (format == SDL_PIXELFORMAT_ABGR4444) { pixel_format = "SDL_PIXELFORMAT_ABGR4444"; } else if (format == SDL_PIXELFORMAT_BGRA4444) { pixel_format = "SDL_PIXELFORMAT_BGRA4444"; } else if (format == SDL_PIXELFORMAT_ARGB1555) { pixel_format = "SDL_PIXELFORMAT_ARGB1555"; } else if (format == SDL_PIXELFORMAT_RGBA5551) { pixel_format = "SDL_PIXELFORMAT_RGBA5551"; } else if (format == SDL_PIXELFORMAT_ABGR1555) { pixel_format = "SDL_PIXELFORMAT_ABGR1555"; } else if (format == SDL_PIXELFORMAT_BGRA5551) { pixel_format = "SDL_PIXELFORMAT_BGRA5551"; } else if (format == SDL_PIXELFORMAT_RGB565) { pixel_format = "SDL_PIXELFORMAT_RGB565"; } else if (format == SDL_PIXELFORMAT_BGR565) { pixel_format = "SDL_PIXELFORMAT_BGR565"; } else if (format == SDL_PIXELFORMAT_RGB24) { pixel_format = "SDL_PIXELFORMAT_RGB24"; } else if (format == SDL_PIXELFORMAT_BGR24) { pixel_format = "SDL_PIXELFORMAT_BGR24"; } else if (format == SDL_PIXELFORMAT_RGB888) { pixel_format = "SDL_PIXELFORMAT_RGB888"; } else if (format == SDL_PIXELFORMAT_RGBX8888) { pixel_format = "SDL_PIXELFORMAT_RGBX8888"; } else if (format == SDL_PIXELFORMAT_BGR888) { pixel_format = "SDL_PIXELFORMAT_BGR888"; } else if (format == SDL_PIXELFORMAT_BGRX8888) { pixel_format = "SDL_PIXELFORMAT_BGRX8888"; } else if (format == SDL_PIXELFORMAT_ARGB8888) { pixel_format = "SDL_PIXELFORMAT_ARGB8888"; } else if (format == SDL_PIXELFORMAT_RGBA8888) { pixel_format = "SDL_PIXELFORMAT_RGBA8888"; } else if (format == SDL_PIXELFORMAT_ABGR8888) { pixel_format = "SDL_PIXELFORMAT_ABGR8888"; } else if (format == SDL_PIXELFORMAT_BGRA8888) { pixel_format = "SDL_PIXELFORMAT_BGRA8888"; } else if (format == SDL_PIXELFORMAT_ARGB2101010) { pixel_format = "SDL_PIXELFORMAT_ARGB2101010"; } return pixel_format; } const nlohmann::json& Game::configuration() const { return _configuration.config; } nlohmann::json& Game::configuration() { return _configuration.config; } const SDL_Window* Game::window() const { return _window; } SDL_Window* Game::window() { return _window; } const SDL_Renderer* Game::get_renderer() const { return renderer; } SDL_Renderer* Game::get_renderer() { return renderer; } const Input& Game::get_input() const { return input; } Input& Game::get_input() { return input; } Audio& Game::get_audio() { return audio; } /* Applies delta timing to a vector: returns the passed vector as weighted by the amount of time passed since the * last frame update, allowing for vector values to change the same amount over time independent of the frame rate */ glm::vec2 Game::weight(glm::vec2 motion) { return {weight(motion.x), weight(motion.y)}; } void Game::run() { SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); suppress_input_temporarily(); #if defined(__EMSCRIPTEN__) SDL_Log("using emscripten main loop"); emscripten_set_main_loop_arg(&loop, this, -1, true); #else SDL_Log("using standard main loop"); while (!done) { frame(SDL_GetTicks()); SDL_Delay(8); } #endif } void Game::frame(float ticks) { if (ticks - last_frame_timestamp + frame_time_overflow >= frame_length) { last_frame_length = ticks - last_frame_timestamp; if (frame_length_history.size() == 5000) { frame_length_history.pop_back(); } frame_length_history.insert(frame_length_history.begin(), last_frame_length); frame_time_overflow = last_frame_length + frame_time_overflow - frame_length; last_frame_timestamp = ticks; if (last_frame_length < 1000) { recorder.update(); delegate.dispatch(); audio.update(); input.unsuppress_animation.update(); update(); framerate_indicator.update(); _configuration.update(); if (!is_gl_context) { SDL_SetRenderTarget(renderer, nullptr); SDL_RenderPresent(renderer); } } // if (frame_time_overflow > frame_length) // { // SDL_Log("%i frame(s) dropped", ((int) (frame_time_overflow / frame_length))); // frame_time_overflow = 0; // } frame_count_this_second++; if (ticks - last_frame_count_timestamp >= 1000) { framerate_indicator.refresh(); last_frame_count_timestamp = ticks; frame_count_this_second = 0; } } // std::cout << std::endl; } #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; } /* Set the length of a frame in seconds by passing the framerate, the amount of frames to display * per second */ void Game::set_framerate(int framerate) { if (framerate < 1) { framerate = 1; } frame_length = 1000.0 / framerate; } /* Return the length of a frame in seconds */ float Game::get_frame_length() const { return frame_length; } void Game::handle_quit_event(SDL_Event &event) { if (event.type == SDL_QUIT) { flag_to_end(); } } void Game::quit() { if (glcontext != nullptr) { SDL_GL_DeleteContext(glcontext); } if (renderer != nullptr) { SDL_DestroyRenderer(renderer); } if (_window != nullptr) { SDL_DestroyWindow(_window); } if (TTF_WasInit()) { TTF_CloseFont(bp_mono_font); TTF_Quit(); } Mix_CloseAudio(); Mix_Quit(); SDL_Quit(); } Game::~Game() { get_delegate().unsubscribe(this); } FramerateIndicator::FramerateIndicator(Node* parent) : Sprite(parent) { get_delegate().subscribe(&FramerateIndicator::respond, this); hide(); } void FramerateIndicator::respond(SDL_Event& event) { if (get_delegate().compare(event, "toggle-framerate")) { toggle_hidden(); } } SDL_Surface* FramerateIndicator::get_surface() { std::string padded = sb::pad(get_root()->frame_count_this_second, 2); SDL_Surface* shaded = TTF_RenderText_Shaded( get_root()->bp_mono_font, padded.c_str(), {0, 0, 0, 255}, {255, 255, 255, 255}); if (!shaded) { sb::Log::sdl_error("Could not create text"); } return shaded; } void FramerateIndicator::refresh() { if (!is_hidden() && get_root()->bp_mono_font != nullptr) { unload(); SDL_Surface* surface = get_surface(); SDL_Texture* texture = SDL_CreateTextureFromSurface(get_root()->get_renderer(), surface); add_frames(texture); SDL_FreeSurface(surface); set_ne(get_display().window_box().ne()); } }