Files
spacebox/src/sb.cpp
cocktailfrank 10993d10b7 Improve font and audio library initialization
Move initialization out of the Game object and into sb::init and
sb::quit. The font and audio libraries only need to be initialized once
when the library starts. The Game object opens an audio interface, so
open and close it within the game object. Continue opening and closing
the font itself within the game object.
2026-10-15 18:17:43 -04:00

345 lines
11 KiB
C++

/* +=======================================================+
____/ \____ /: Open source game framework licensed to freely use, :
\ / / : copy, and modify - created for dank.game :
+==\ ^__^ /==+ : :
: ~/ \~ : : Download at https://open.shampoo.ooo/shampoo/spacebox :
: ~~~~~~~~~~~~ : +=======================================================+
: SPACE ~~~~~ : /
: ~~~~~~~ BOX :/
+=============*/
#include "SDL_ttf.h"
#include "SDL_mixer.h"
#include "sb.hpp"
CLI::App sb::cli::_app {
"The options below can be used to configure the application at launch.\n"
};
#if defined(STEAM_ENABLED)
bool sb::initialize_steam = true;
#else
bool sb::initialize_steam = false;
#endif
/*!
* Use unnamed namespace to prevent data and functions from being used outside this file.
*/
namespace
{
std::vector<nlohmann::json> _cli_configs;
bool _cli_debug_config_load;
void load_config_json();
/* Store environment variable string until the program exits. The putenv function keeps a reference to the string
* data for the lifetime of the program. */
std::string fullscreen_env_assigment = "SDL_VIDEO_X11_LEGACY_FULLSCREEN=0";
}
std::vector<std::string> sb::cli::_reserved = {"--config", "--config-file", "--debug-config-load"};
std::vector<std::string> sb::cli::_config_strings {};
std::vector<fs::path> sb::cli::_config_files {};
const CLI::App& sb::cli::app()
{
return _app;
}
const std::vector<std::string> sb::cli::reserved()
{
return _reserved;
}
void sb::cli::init()
{
/* Always start with no data parsed */
_config_files = {};
_config_strings = {};
_cli_configs = {};
try
{
_add("--config", _config_strings,
"JSON configuration to be loaded into the program. Any existing key values will be overwritten by the "
"values in this string. Multiple strings may be passed and each will be merged into the "
"configuration.");
_add("--config-file", _config_files,
"JSON file of configuration data. Any existing key values will be overwritten by the values in this "
"file. Multiple files may be passed and each will be merged into the configuration.");
_add_flag("--debug-config-load", _cli_debug_config_load,
"If this flag is passed, log statements will be printed to stdout during configuration loading, "
"regardless of the log settings in the configuration files. This can be useful when loading "
"multiple configuration files with conflicting log settings.");
}
catch (const CLI::OptionAlreadyAdded& error)
{
/* If either option is already added, sb::cli::init must have already ran since the reserved names are not
* able to be added from outside the class, so it's OK to ignore this error. */
}
}
void sb::cli::parse(int argc, char** argv)
{
/* Handles Unicode support on Windows */
argv = _app.ensure_utf8(argv);
if (argc > 0)
{
std::vector<std::string> command_line {argv, argv + argc};
parse(command_line);
}
}
void sb::cli::parse(std::vector<std::string>& command_line)
{
if (command_line.size() > 0)
{
try
{
/* CLI::App::parse expects a reversed vector */
std::vector<std::string> reversed {command_line.rbegin(), command_line.rend()};
_app.parse(reversed);
}
catch (const CLI::ParseError& error)
{
_app.exit(error);
throw CLI::ParseError(error);
}
}
}
void sb::cli::parse(std::initializer_list<std::string> _command_line)
{
std::vector<std::string> command_line {_command_line};
parse(command_line);
}
void sb::cli::_add_flag(const std::string& name, bool& storage, const std::string& description)
{
try
{
_app.add_flag(name, storage, description);
}
catch (const CLI::OptionAlreadyAdded& error)
{
std::ostringstream message;
message << "Error while trying to create option " << name << ". This option has already been added.";
throw CLI::OptionAlreadyAdded(message.str());
}
}
const std::vector<std::string>& sb::cli::config_strings()
{
return _config_strings;
}
const std::vector<fs::path>& sb::cli::config_files()
{
return _config_files;
}
void sb::init()
{
init({});
}
void sb::init(int argc, char** argv)
{
if (argc > 0)
{
std::vector<std::string> command_line {argv + 1, argv + argc};
init(command_line);
}
else
{
init();
}
}
void sb::init(std::initializer_list<std::string> _command_line)
{
std::vector<std::string> command_line {_command_line};
init(command_line);
}
void sb::init(std::vector<std::string>& command_line)
{
/* Changing directory is necessary for loading external resources in a macOS app bundle. This may be useful on other
* platforms too, so it may become default on all platforms and/or configurable in the future. */
#if defined(__APPLE__)
fs::current_path(SDL_GetBasePath());
#endif
/* This will make the framework's reserved options available in the CLI */
cli::init();
/* Parse the command line, and throw an exception if the help or version commands were passed. */
cli::parse(command_line);
/* Combine all configuration submitted to the CLI into one JSON object. */
load_config_json();
/* If HTTP is enabled, the HTTP library will be initialized. If Steam is enabled, the Steam API will be initialized
* with callbacks checked every 0.5 seconds.
*
* sb::initialize_steam can be set to false to manually force Steam not to load. */
sb::cloud::init(0.5f, initialize_steam);
/* Allows use of custom main function instead of SDL's main function. It requires SDL_MAIN_HANDLED to be defined
* before SDL.h is included, which can be done by passing `-D SDL_MAIN_HANDLED` to the compiler. See
* SDL_SetMainReady.html */
SDL_SetMainReady();
/* Initialize SDL's video, event, joystick, and game controller sub-systems. SDL's font and audio sub-systems are
* initialized later, in the Game class constructor, though they should be moved here eventually. */
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
throw std::runtime_error("SDL could not be initialized");
}
/* Needed for displaying fullscreen correctly on Linux (?) Also might need SDL_VIDEO_CENTERED (?). The string data
* is not copied, so the pointer must remain valid for the lifetime of the program. */
putenv(const_cast<char*>(fullscreen_env_assigment.c_str()));
/* Initialize SDL TTF font library */
if (TTF_Init() < 0) {
sb::Log::sdl_error("Could not initialize SDL ttf");
throw std::runtime_error("SDL TTF library could not be initialized");
}
else {
sb::Log::Multi() << "SDL TTF v" << SDL_TTF_MAJOR_VERSION << "." << SDL_TTF_MINOR_VERSION << "." <<
SDL_TTF_PATCHLEVEL << sb::Log::end;
}
/* 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 {
sb::Log::Multi() << "SDL Mixer v" << SDL_MIXER_MAJOR_VERSION << "." << SDL_MIXER_MINOR_VERSION << "." <<
SDL_MIXER_PATCHLEVEL << sb::Log::end;
/* 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++) {
sb::Log::Multi() <<
"Found audio capture device " << ii << ": " << SDL_GetAudioDeviceName(ii, SDL_TRUE) << sb::Log::end;
}
}
}
const std::vector<nlohmann::json> sb::cli_configs()
{
return _cli_configs;
}
bool sb::cli_debug_config_load()
{
return _cli_debug_config_load;
}
namespace {
/*!
* Combine all configuration submitted to the CLI into one JSON object.
*/
void load_config_json()
{
/* Combine the --config and --config-file options into a single list of JSON objects. The --config flag takes
* priority. */
for (const fs::path& command_line_config_file : sb::cli::config_files())
{
_cli_configs.push_back(sb::json_from_file(command_line_config_file));
sb::Log::log("Parsed " + command_line_config_file.string(), sb::Log::DEBUG);
}
for (const std::string& command_line_config : sb::cli::config_strings())
{
try
{
_cli_configs.push_back(nlohmann::json::parse(command_line_config));
sb::Log::log("Parsed " + command_line_config, sb::Log::DEBUG);
}
catch (const nlohmann::json::parse_error& error)
{
std::ostringstream message;
message << "Error parsing config string, \"" << command_line_config << "\": " << error.what();
/* ID 101 indicates a syntax error. See `json.hpp`. */
throw nlohmann::json::parse_error::create(101, error.byte, message.str(), nullptr);
}
}
#if defined(__EMSCRIPTEN__)
/* Check URL for config overrides in web builds. */
char* config_from_url = (char*) EM_ASM_PTR({
var parsed_url = new URL(window.location.href);
var config = parsed_url.searchParams.get("config");
if (config)
{
return stringToNewUTF8(config);
}
else
{
return stringToNewUTF8("\0");
}
});
if (config_from_url[0] != '\0')
{
sb::Log::Multi() << "Read config string from URL: " << config_from_url << sb::Log::end;
try
{
_cli_configs.push_back(nlohmann::json::parse(config_from_url));
}
catch (const nlohmann::json::parse_error& error)
{
std::ostringstream message;
message << "Error parsing config string, \"" << config_from_url << "\": " << error.what();
/* ID 101 indicates a syntax error. See `json.hpp`. */
throw nlohmann::json::parse_error::create(101, error.byte, message.str(), nullptr);
}
}
free(config_from_url);
#endif
}
/*!
* Captures log messages to stdout.
*/
void _log_to_stdout([[maybe_unused]] void* userdata, [[maybe_unused]] int category, SDL_LogPriority priority,
const char* message)
{
std::ostream& out = (priority > SDL_LOG_PRIORITY_WARN) ? std::cerr : std::cout;
out << message << std::endl;
}
}
void sb::reset_log_function()
{
SDL_LogSetOutputFunction(&_log_to_stdout, nullptr);
SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_DEBUG);
}
void sb::quit()
{
sb::cloud::quit();
/* Unload font library */
if (TTF_WasInit()) {
TTF_Quit();
sb::Log::Line() << "Exited SDL TTF font library";
}
/* Quit audio library */
Mix_Quit();
sb::Log::Line() << "Exited SDL audio mixer library";
/* Clean up SDL */
sb::Log::Line() << "Quitting SDL";
SDL_Quit();
}