townengine/src/twn_loop.c

869 lines
29 KiB
C
Raw Normal View History

#include "twn_loop_c.h"
#include "twn_engine_context_c.h"
#include "twn_filewatch_c.h"
#include "twn_input_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_game_object_c.h"
#include "twn_textures_c.h"
#include "twn_timer_c.h"
2025-02-07 07:19:36 +00:00
#include "twn_workers_c.h"
2024-07-08 00:44:20 +00:00
#include <SDL2/SDL.h>
#include <physfs.h>
#include <stb_ds.h>
#include <toml.h>
2024-07-08 00:44:20 +00:00
#include <stdbool.h>
#include <limits.h>
#define TICKS_PER_SECOND_DEFAULT 60
#define PACKAGE_EXTENSION "btw"
static SDL_sem *opengl_load_semaphore;
2025-02-03 21:24:31 +00:00
/* note: it drives most of IO implicitly, such as audio callbacks */
2024-07-08 00:44:20 +00:00
static void poll_events(void) {
SDL_Event e;
ctx.window_size_has_changed = false;
2024-07-08 00:44:20 +00:00
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT:
ctx.is_running = false;
2024-07-08 00:44:20 +00:00
break;
2024-07-08 00:44:20 +00:00
case SDL_WINDOWEVENT:
if (e.window.windowID != ctx.window_id)
break;
switch (e.window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
2025-02-03 21:24:31 +00:00
ctx.window_dims.x = (float)e.window.data1;
ctx.window_dims.y = (float)e.window.data2;
ctx.resync_flag = true;
ctx.window_size_has_changed = true;
2024-07-08 00:44:20 +00:00
break;
2024-08-21 13:55:34 +00:00
2025-02-03 21:24:31 +00:00
case SDL_WINDOWEVENT_FOCUS_LOST: {
ctx.window_mouse_resident = false;
break;
}
case SDL_WINDOWEVENT_FOCUS_GAINED: {
ctx.window_mouse_resident = true;
break;
}
2024-08-21 13:55:34 +00:00
default:
break;
2024-07-08 00:44:20 +00:00
}
break;
2024-08-21 13:55:34 +00:00
default:
break;
2024-07-08 00:44:20 +00:00
}
}
}
2024-10-13 18:32:31 +00:00
static void preserve_persistent_ctx_fields(void) {
ctx.game.udata = ctx.game_copy.udata;
}
static void update_viewport(void) {
Rect new_viewport;
float new_scale;
if ((float)ctx.window_dims.x / (float)ctx.window_dims.y > (float)ctx.base_render_width / (float)ctx.base_render_height) {
float ratio = (float)ctx.window_dims.y / (float)ctx.base_render_height;
float w = ((float)ctx.base_render_width * ratio);
new_viewport.x = ctx.window_dims.x / 2 - w / 2;
new_viewport.y = 0;
new_viewport.w = w;
new_viewport.h = ctx.window_dims.y;
new_scale = ratio;
} else {
float ratio = (float)ctx.window_dims.x / (float)ctx.base_render_width;
float h = ((float)ctx.base_render_height * ratio);
new_viewport.x = 0;
new_viewport.y = ctx.window_dims.y / 2 - h / 2;
new_viewport.w = ctx.window_dims.x;
new_viewport.h = h;
new_scale = ratio;
}
ctx.viewport_rect = new_viewport;
ctx.viewport_scale = new_scale;
}
static void main_loop(void) {
2024-07-08 00:44:20 +00:00
/*
if (!ctx.is_running) {
end(ctx);
clean_up(ctx);
emscripten_cancel_main_loop();
}
*/
2024-07-08 00:44:20 +00:00
/* frame timer */
int64_t current_frame_time = SDL_GetPerformanceCounter();
int64_t delta_time = current_frame_time - ctx.prev_frame_time;
ctx.prev_frame_time = current_frame_time;
ctx.delta_time = delta_time;
2024-07-08 00:44:20 +00:00
/* handle unexpected timer anomalies (overflow, extra slow frames, etc) */
if (delta_time > ctx.desired_frametime * 8) { /* ignore extra-slow frames */
delta_time = ctx.desired_frametime;
}
delta_time = MAX(0, delta_time);
/* vsync time snapping */
/* get the refresh rate of the current display (it might not always be the same one) */
int display_framerate = 60; /* a reasonable guess */
SDL_DisplayMode current_display_mode;
int current_display_index = SDL_GetWindowDisplayIndex(ctx.window);
if (SDL_GetCurrentDisplayMode(current_display_index, &current_display_mode) == 0)
display_framerate = current_display_mode.refresh_rate;
int64_t snap_hz = display_framerate;
if (snap_hz <= 0)
snap_hz = 60;
/* these are to snap delta time to vsync values if it's close enough */
int64_t vsync_maxerror = (int64_t)((double)ctx.clocks_per_second * 0.0002);
size_t snap_frequency_count = 8;
for (size_t i = 0; i < snap_frequency_count; ++i) {
int64_t frequency = (ctx.clocks_per_second / snap_hz) * (i+1);
if (llabs(delta_time - frequency) < vsync_maxerror) {
delta_time = frequency;
break;
}
}
/* delta time averaging */
size_t time_averager_count = SDL_arraysize(ctx.time_averager);
for (size_t i = 0; i < time_averager_count - 1; ++i) {
ctx.time_averager[i] = ctx.time_averager[i+1];
}
ctx.time_averager[time_averager_count - 1] = delta_time;
int64_t averager_sum = 0;
for (size_t i = 0; i < time_averager_count; ++i) {
averager_sum += ctx.time_averager[i];
}
delta_time = averager_sum / time_averager_count;
ctx.delta_averager_residual += averager_sum % time_averager_count;
delta_time += ctx.delta_averager_residual / time_averager_count;
ctx.delta_averager_residual %= time_averager_count;
/* add to the accumulator */
ctx.frame_accumulator += delta_time;
/* spiral of death protection */
if (ctx.frame_accumulator > ctx.desired_frametime * 8) {
2024-07-08 00:44:20 +00:00
ctx.resync_flag = true;
}
/* timer resync if requested */
if (ctx.resync_flag) {
ctx.frame_accumulator = 0;
delta_time = ctx.desired_frametime;
ctx.resync_flag = false;
}
ctx.game_copy = ctx.game;
2024-07-08 00:44:20 +00:00
/* finally, let's get to work */
int frames = 0;
2025-01-14 21:52:42 +00:00
while (ctx.frame_accumulator >= ctx.desired_frametime) {
frames += 1;
2025-01-14 21:52:42 +00:00
/* TODO: disable rendering pushes on not-last ? */
2025-02-01 22:41:02 +00:00
render_clear();
2025-01-14 21:52:42 +00:00
poll_events();
if (ctx.window_size_has_changed)
update_viewport();
input_state_update(&ctx.input);
profile_start("game tick");
start_sanity_timer(2000);
2025-01-14 21:52:42 +00:00
game_object_tick();
end_sanity_timer();
profile_end("game tick");
2025-01-14 21:52:42 +00:00
input_state_update_postframe(&ctx.input);
/* TODO: make it works when ctx.ticks_per_second != 60 */
if (ctx.push_audio_model) {
uint64_t const queued_frames = SDL_GetQueuedAudioSize(ctx.audio_device) / sizeof (float) / 2;
/* note: this is here to reduce drift which sometimes appears */
if (queued_frames >= 4096 * 2)
SDL_ClearQueuedAudio(ctx.audio_device);
static uint8_t audio_buffer[(AUDIO_FREQUENCY / 60) * sizeof (float) * 2];
if (ctx.audio_initialized) {
audio_callback(NULL, audio_buffer, sizeof audio_buffer);
if (SDL_QueueAudio(ctx.audio_device, audio_buffer, sizeof audio_buffer))
CRY_SDL("Error queueing audio: ");
}
}
2025-01-14 21:52:42 +00:00
2025-01-14 22:01:16 +00:00
/* TODO: ctx.game_copy = ctx.game should be placed after it, but it messes with state used in render() */
2025-01-14 21:52:42 +00:00
preserve_persistent_ctx_fields();
ctx.frame_accumulator -= ctx.desired_frametime;
ctx.game.frame_number++;
/* TODO: should we ask for reinitialization in such case? */
if (ctx.game.frame_number > 16777216)
ctx.game.frame_number = 0;
ctx.game.initialization_needed = false;
2024-07-08 00:44:20 +00:00
}
2024-07-31 15:38:32 +00:00
/* TODO: in some cases machine might want to assume frames will be fed as much as possible */
/* which for now is broken as glBufferData with NULL is used all over right after render */
if (frames != 0)
render();
else if (ctx.desired_frametime - ctx.frame_accumulator > 1000000)
/* don't waste clock cycles on useless work */
/* TODO: make it adjustable from config */
SDL_Delay((uint32_t)(ctx.desired_frametime - ctx.frame_accumulator) / 1250000);
2024-07-08 00:44:20 +00:00
}
/* TODO: cache and deny duplicates */
/* TODO: ability to redefine mounting point for a dependency */
/* TODO: ability to load external sources over HTTP if url is given */
static void resolve_pack_dependencies(const char *pack_name) {
char *path;
if (SDL_asprintf(&path, "/packs/%s.toml", pack_name) == -1) {
CRY("resolve_pack_dependencies()", "Allocation error");
goto ERR_PACK_MANIFEST_PATH_ALLOC_FAIL;
}
if (!PHYSFS_exists(path))
/* no package manifest provided, abort */
goto OK_NO_MANIFEST;
char *manifest_file = file_to_str(path);
if (!manifest_file) {
CRY_PHYSFS("Pack manifest file loading failed");
goto ERR_PACK_MANIFEST_FILE_LOADING;
}
char errbuf[256]; /* tomlc99 example implies that this is enough... */
toml_table_t *manifest = toml_parse(manifest_file, errbuf, sizeof errbuf);
SDL_free(manifest_file);
if (!manifest) {
CRY("Pack manifest decoding failed", errbuf);
goto ERR_PACK_MANIFEST_DECODING;
}
toml_array_t *deps = toml_array_in(manifest, "deps");
if (!deps)
goto OK_NO_DEPS;
/* iterate over entries in [[deps]] array */
/* TODO: use sub procedures for failable loops? so that error mechanism cleans well */
toml_table_t *cur_dep = toml_table_at(deps, 0);
for (int idx = 0; cur_dep; ++idx, cur_dep = toml_table_at(deps, idx)) {
toml_datum_t dep_name = toml_string_in(cur_dep, "name");
if (!dep_name.ok) {
log_warn("No 'name' is present in pack dependency, skipping it");
continue;
}
char *dep_pack_name;
if (SDL_asprintf(&dep_pack_name, "%s%s.btw", ctx.base_dir, dep_name.u.s) == -1) {
CRY("resolve_pack_dependencies()", "Allocation error");
SDL_free(dep_name.u.s);
goto ERR_DEP_PATH_ALLOC_FAIL;
}
/* try loading pack */
if (!PHYSFS_mount(dep_pack_name, "/", true)) {
/* if it failes, try going for source */
toml_datum_t dep_source = toml_string_in(cur_dep, "source");
if (!dep_source.ok) {
log_warn("No 'source' is present in pack %s, while %s.btw isn't present, skipping it", dep_name.u.s, dep_name.u.s);
SDL_free(dep_name.u.s);
SDL_free(dep_pack_name);
continue;
}
char *dep_source_path;
if (SDL_asprintf(&dep_source_path, "%s%s", ctx.base_dir, dep_source.u.s) == -1) {
CRY("resolve_pack_dependencies()", "Allocation error");
SDL_free(dep_name.u.s);
SDL_free(dep_pack_name);
goto ERR_DEP_PATH_ALLOC_FAIL;
}
if (!PHYSFS_mount(dep_source_path, "/", true))
CRY("Cannot load pack", "Nothing is given to work with");
log_info("Pack loaded: %s\n", dep_source.u.s);
SDL_free(dep_source.u.s);
SDL_free(dep_source_path);
} else
log_info("Pack loaded: %s\n", dep_pack_name);
SDL_free(dep_pack_name);
/* recursive resolution */
resolve_pack_dependencies(dep_name.u.s);
SDL_free(dep_name.u.s);
}
ERR_DEP_PATH_ALLOC_FAIL:
OK_NO_DEPS:
toml_free(manifest);
ERR_PACK_MANIFEST_DECODING:
ERR_PACK_MANIFEST_FILE_LOADING:
OK_NO_MANIFEST:
SDL_free(path);
ERR_PACK_MANIFEST_PATH_ALLOC_FAIL:
return;
}
static int opengl_load_thread_fn(void *sem) {
SDL_assert_always(!SDL_GL_LoadLibrary(NULL));
2024-07-08 00:44:20 +00:00
/* signal success */
SDL_assert_always(!SDL_SemPost(sem));
return 0;
}
static bool initialize(void) {
/* first things first, most things here will be loaded from the config file */
/* it's expected to be present in the data directory, no matter what */
/* that is why PhysicsFS is initialized before anything else */
toml_set_memutil(SDL_malloc, SDL_free);
profile_start("pack dependency resolution");
/* time to orderly resolve any dependencies present */
resolve_pack_dependencies("data");
profile_end("pack dependency resolution");
/* load the config file into an opaque table */
{
char *config_file = file_to_str("/twn.toml");
if (config_file == NULL) {
CRY("Configuration file loading failed", "Cannot access /twn.toml");
goto fail;
}
char errbuf[256]; /* tomlc99 example implies that this is enough... */
ctx.config_table = toml_parse(config_file, errbuf, sizeof errbuf);
SDL_free(config_file);
if (ctx.config_table == NULL) {
CRY("Configuration file pasing failed", errbuf);
goto fail;
}
}
/* at this point we can save references to the tables within the parent one for later */
toml_table_t *about = toml_table_in(ctx.config_table, "about");
if (about == NULL) {
CRY("Initialization failed", "[about] table expected in configuration file");
goto fail;
}
toml_table_t *game = toml_table_in(ctx.config_table, "game");
if (game == NULL) {
CRY("Initialization failed", "[game] table expected in configuration file");
goto fail;
}
toml_table_t *engine = toml_table_in(ctx.config_table, "engine");
if (engine == NULL) {
CRY("Initialization failed", "[engine] table expected in configuration file");
goto fail;
}
/* configure physicsfs write dir */
{
toml_datum_t datum_dev_id = toml_string_in(about, "dev_id");
if (!datum_dev_id.ok) {
CRY("Initialization failed", "Valid about.dev_id expected in configuration file");
goto fail;
}
toml_datum_t datum_app_id = toml_string_in(about, "app_id");
if (!datum_app_id.ok) {
CRY("Initialization failed", "Valid about.app_id expected in configuration file");
goto fail;
}
if (!PHYSFS_setSaneConfig(datum_dev_id.u.s, datum_app_id.u.s, PACKAGE_EXTENSION, false, true)) {
CRY_PHYSFS("Filesystem initialization failed");
goto fail;
}
SDL_free(datum_dev_id.u.s);
SDL_free(datum_app_id.u.s);
}
/* debug mode defaults to being enabled */
/* pass --debug or --release to force a mode, ignoring configuration */
toml_datum_t datum_debug = toml_bool_in(game, "debug");
ctx.game.debug = datum_debug.ok ? datum_debug.u.b : true;
#ifdef EMSCRIPTEN
/* emscripten interpretes those as GL ES version against WebGL */
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
if (ctx.game.debug)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_NO_ERROR);
#endif
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);
toml_datum_t datum_title = toml_string_in(about, "title");
if (!datum_title.ok)
datum_title.u.s = SDL_strdup("townengine project");
ctx.title = datum_title.u.s;
/* not yet used
toml_datum_t datum_developer = toml_string_in(about, "developer");
if (!datum_developer.ok) {
CRY("Initialization failed", "Valid about.developer expected in configuration file");
2024-07-08 00:44:20 +00:00
goto fail;
}
*/
2024-07-08 00:44:20 +00:00
toml_array_t *datum_resolution = toml_array_in(game, "resolution");
if (datum_resolution) {
toml_datum_t datum_base_render_width = toml_int_at(datum_resolution, 0);
if (!datum_base_render_width.ok) {
CRY("Initialization failed", "Valid game.resolution expected in configuration file");
goto fail;
}
toml_datum_t datum_base_render_height = toml_int_at(datum_resolution, 1);
if (!datum_base_render_height.ok) {
CRY("Initialization failed", "Valid game.resolution expected in configuration file");
goto fail;
}
ctx.base_render_width = datum_base_render_width.u.i;
ctx.base_render_height = datum_base_render_height.u.i;
} else {
ctx.base_render_width = 640;
ctx.base_render_height = 360;
}
ctx.game.resolution.x = (float)ctx.base_render_width;
ctx.game.resolution.y = (float)ctx.base_render_height;
//SDL_free(datum_developer.u.s);
/* TODO: */
// SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
ctx.window_dims.x = (float)ctx.base_render_width;
ctx.window_dims.y = (float)ctx.base_render_height;
2024-07-08 00:44:20 +00:00
ctx.background_color = (Color){ 230, 230, 230, 255 };
toml_array_t *datum_background_color = toml_array_in(game, "background_color");
if (datum_background_color) do {
toml_datum_t datum_background_color_red = toml_int_at(datum_background_color, 0);
if (!datum_background_color_red.ok || datum_background_color_red.u.i < 0 || datum_background_color_red.u.i >= 256) {
log_warn("Invalid value for red channel of game.background_color");
break;
}
toml_datum_t datum_background_color_green = toml_int_at(datum_background_color, 1);
if (!datum_background_color_green.ok || datum_background_color_green.u.i < 0 || datum_background_color_green.u.i >= 256) {
log_warn("Invalid value for green channel of game.background_color");
break;
}
toml_datum_t datum_background_color_blue = toml_int_at(datum_background_color, 2);
if (!datum_background_color_blue.ok || datum_background_color_blue.u.i < 0 || datum_background_color_blue.u.i >= 256) {
log_warn("Invalid value for blue channel of game.background_color");
break;
}
toml_datum_t datum_background_color_alpha = toml_int_at(datum_background_color, 3);
if (!datum_background_color_alpha.ok || datum_background_color_alpha.u.i < 0 || datum_background_color_alpha.u.i >= 256) {
log_warn("Invalid value for alpha channel of game.background_color");
break;
}
ctx.background_color = (Color){
(uint8_t)datum_background_color_red.u.i,
(uint8_t)datum_background_color_green.u.i,
(uint8_t)datum_background_color_blue.u.i,
(uint8_t)datum_background_color_alpha.u.i,
};
} while (0);
ctx.game.resolution.x = (float)ctx.base_render_width;
ctx.game.resolution.y = (float)ctx.base_render_height;
2024-07-08 00:44:20 +00:00
/* random seeding */
/* SDL_GetPerformanceCounter returns some platform-dependent number. */
/* it should vary between game instances. i checked! random enough for me. */
ctx.game.random_seed = (float)(SDL_GetPerformanceCounter() % 16777216);
srand((unsigned int)(SDL_GetPerformanceCounter()));
stbds_rand_seed((size_t)(SDL_GetPerformanceCounter()));
2024-07-08 00:44:20 +00:00
/* main loop machinery */
toml_datum_t datum_ticks_per_second = toml_int_in(engine, "ticks_per_second");
if (!datum_ticks_per_second.ok) {
ctx.ticks_per_second = TICKS_PER_SECOND_DEFAULT;
} else {
if (datum_ticks_per_second.u.i < 8) {
ctx.ticks_per_second = TICKS_PER_SECOND_DEFAULT;
} else {
ctx.ticks_per_second = datum_ticks_per_second.u.i;
}
}
ctx.is_running = true;
2024-07-08 00:44:20 +00:00
ctx.resync_flag = true;
ctx.clocks_per_second = SDL_GetPerformanceFrequency();
ctx.prev_frame_time = SDL_GetPerformanceCounter();
ctx.desired_frametime = ctx.clocks_per_second / ctx.ticks_per_second;
2024-07-08 00:44:20 +00:00
ctx.frame_accumulator = 0;
ctx.game.frame_number = 0;
2025-01-26 06:09:21 +00:00
ctx.game.frame_duration = 1.0f / (float)ctx.ticks_per_second;
2024-07-08 00:44:20 +00:00
/* delta time averaging */
ctx.delta_averager_residual = 0;
for (size_t i = 0; i < SDL_arraysize(ctx.time_averager); ++i) {
ctx.time_averager[i] = ctx.desired_frametime;
}
2025-02-03 21:24:31 +00:00
/* engine configuration */
{
toml_datum_t datum_texture_atlas_size = toml_int_in(engine, "texture_atlas_size");
2025-02-10 11:36:57 +00:00
if (!datum_texture_atlas_size.ok || datum_texture_atlas_size.u.i < 32) {
ctx.texture_atlas_size = TEXTURE_ATLAS_SIZE_DEFAULT;
2025-02-10 11:36:57 +00:00
} else
ctx.texture_atlas_size = datum_texture_atlas_size.u.i;
toml_datum_t datum_font_texture_size = toml_int_in(engine, "font_texture_size");
2025-02-10 11:36:57 +00:00
if (!datum_font_texture_size.ok || datum_font_texture_size.u.i < 1024) {
ctx.font_texture_size = TEXT_FONT_TEXTURE_SIZE_DEFAULT;
2025-02-10 11:36:57 +00:00
} else
ctx.font_texture_size = datum_font_texture_size.u.i;
toml_datum_t datum_font_oversampling = toml_int_in(engine, "font_oversampling");
2025-02-10 11:36:57 +00:00
if (!datum_font_oversampling.ok || datum_font_oversampling.u.i < 0) {
ctx.font_oversampling = TEXT_FONT_OVERSAMPLING_DEFAULT;
2025-02-10 11:36:57 +00:00
} else
ctx.font_oversampling = datum_font_oversampling.u.i;
toml_datum_t datum_font_filtering = toml_string_in(engine, "font_filtering");
if (!datum_font_filtering.ok) {
ctx.font_filtering = TEXT_FONT_FILTERING_DEFAULT;
} else {
2025-01-15 03:50:19 +00:00
if (SDL_strncmp(datum_font_filtering.u.s, "nearest", sizeof "nearest" - 1) == 0) {
ctx.font_filtering = TEXTURE_FILTER_NEAREAST;
2025-01-15 03:50:19 +00:00
} else if (SDL_strncmp(datum_font_filtering.u.s, "linear", sizeof "linear" - 1) == 0) {
ctx.font_filtering = TEXTURE_FILTER_LINEAR;
} else {
ctx.font_filtering = TEXT_FONT_FILTERING_DEFAULT;
}
}
SDL_free(datum_font_filtering.u.s);
2025-01-27 01:15:46 +00:00
toml_datum_t datum_cull_faces = toml_bool_in(engine, "cull_faces");
if (!datum_cull_faces.ok) {
ctx.cull_faces = true;
} else {
ctx.cull_faces = datum_cull_faces.u.b;
}
toml_datum_t datum_audio_model = toml_string_in(engine, "audio_model");
ctx.push_audio_model = datum_audio_model.ok ? SDL_strncmp(datum_audio_model.u.s, "push", sizeof "push" - 1) == 0 : false;
SDL_free(datum_audio_model.u.s);
2024-07-08 00:44:20 +00:00
/* input */
toml_datum_t datum_keybind_slots = toml_int_in(engine, "keybind_slots");
2025-02-10 11:36:57 +00:00
if (!datum_keybind_slots.ok || datum_keybind_slots.u.i < 1) {
ctx.keybind_slots = KEYBIND_SLOTS_DEFAULT;
2025-02-10 11:36:57 +00:00
} else
ctx.keybind_slots = datum_keybind_slots.u.i;
}
input_state_init(&ctx.input);
2024-07-08 00:44:20 +00:00
ctx.render_double_buffered = true;
ctx.window_mouse_resident = true;
ctx.game.fog_color = (Color){ 255, 255, 255, 255 }; /* TODO: pick some grey? */
update_viewport();
profile_start("game object load");
/* now we can actually start doing stuff */
game_object_load();
profile_end("game object load");
/* delayed as further as possible so that more work is done before we have to wait */
SDL_SemWait(opengl_load_semaphore);
SDL_DestroySemaphore(opengl_load_semaphore);
profile_end("opengl loading");
2025-02-01 21:50:26 +00:00
SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
2025-02-03 22:04:34 +00:00
SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, "1");
SDL_SetHint(SDL_HINT_WAVE_FACT_CHUNK, "ignorezero");
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
if (ctx.game.debug)
SDL_SetHint(SDL_HINT_SHUTDOWN_DBUS_ON_QUIT, "1");
2025-02-01 21:50:26 +00:00
/* TODO: investigate viability of detached thread driver and window creation, as it's the worst load time offender */
profile_start("window creation");
ctx.window = SDL_CreateWindow(ctx.title,
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
(int)ctx.base_render_width,
(int)ctx.base_render_height,
//SDL_WINDOW_ALLOW_HIGHDPI |
SDL_WINDOW_RESIZABLE |
SDL_WINDOW_OPENGL);
profile_end("window creation");
if (ctx.window == NULL) {
CRY_SDL("Window creation failed.");
goto fail;
}
profile_start("opengl context creation");
ctx.gl_context = SDL_GL_CreateContext(ctx.window);
if (!ctx.gl_context) {
CRY_SDL("GL context creation failed.");
goto fail;
}
if (SDL_GL_MakeCurrent(ctx.window, ctx.gl_context)) {
CRY_SDL("GL context binding failed.");
goto fail;
}
/* TODO: figure out what we ultimately prefer */
SDL_GL_SetSwapInterval(0);
if (!render_init())
goto fail;
setup_viewport(0, 0, (int)ctx.base_render_width, (int)ctx.base_render_height);
profile_end("opengl context creation");
/* might need this to have multiple windows */
ctx.window_id = SDL_GetWindowID(ctx.window);
/* post background color until we're doing other initialization */
render();
if (ctx.render_double_buffered)
render();
profile_start("texture and text cache initialization");
textures_cache_init(&ctx.texture_cache, ctx.window);
text_cache_init(&ctx.text_cache);
profile_end("texture and text cache initialization");
2024-07-08 00:44:20 +00:00
return true;
fail:
SDL_Quit();
return false;
}
/* will not be called on an abnormal exit */
static void clean_up(void) {
input_state_deinit(&ctx.input);
2024-08-23 02:41:52 +00:00
text_cache_deinit(&ctx.text_cache);
2024-07-08 00:44:20 +00:00
textures_cache_deinit(&ctx.texture_cache);
arrfree(ctx.render_queue_2d);
toml_free(ctx.config_table);
2024-07-08 00:44:20 +00:00
PHYSFS_deinit();
2025-02-07 07:19:36 +00:00
workers_deinit();
2025-02-07 10:42:01 +00:00
model_state_deinit();
SDL_free(ctx.base_dir);
SDL_free(ctx.title);
SDL_GL_DeleteContext(ctx.gl_context);
SDL_GL_UnloadLibrary();
2025-02-07 21:22:34 +00:00
SDL_QuitSubSystem(SDL_INIT_EVENTS);
if (ctx.audio_initialized)
SDL_QuitSubSystem(SDL_INIT_AUDIO);
2024-07-08 00:44:20 +00:00
SDL_Quit();
}
void reset_state(void) {
ctx.game.initialization_needed = true;
input_reset_state(&ctx.input);
2024-08-21 15:00:27 +00:00
textures_reset_state();
2024-08-21 13:55:34 +00:00
}
static void pack_contents_modified(char const *path, enum FilewatchAction action) {
static uint32_t last_reset_tick;
if (last_reset_tick != (uint32_t)ctx.game.frame_number) {
log_info("Pack contents invalidated: %s, action: %i", path, action);
reset_state();
last_reset_tick = (uint32_t)ctx.game.frame_number;
}
}
/* TODO: handle .btw packs */
static bool try_mounting_root_pack(char *path) {
log_info("Mounting %s", path);
if (!PHYSFS_mount(path, NULL, true))
return false;
filewatch_add_directory(path, &pack_contents_modified);
return true;
}
int enter_loop(int argc, char **argv) {
profile_start("startup");
profile_start("SDL initialization");
if (SDL_Init(SDL_INIT_VIDEO) == -1) {
CRY_SDL("SDL initialization failed.");
return EXIT_FAILURE;
}
profile_end("SDL initialization");
profile_start("opengl loading");
opengl_load_semaphore = SDL_CreateSemaphore(0);
SDL_Thread *opengl_load_thread = SDL_CreateThread(opengl_load_thread_fn, "opengl loader", opengl_load_semaphore);
if (!opengl_load_thread) {
CRY_SDL("Cannot create opengl loading thread: ");
return EXIT_FAILURE;
}
SDL_DetachThread(opengl_load_thread);
opengl_load_thread = NULL;
2024-07-08 00:44:20 +00:00
ctx.argc = argc;
ctx.argv = argv;
ctx.base_dir = SDL_GetBasePath();
2024-07-08 00:44:20 +00:00
/* needs to be done before anything else so config can be loaded */
/* TODO: ANDROID: see the warning in physicsfs PHYSFS_init docs/header */
if (!PHYSFS_init(ctx.argv[0])) {
CRY_PHYSFS("Filesystem initialization failed");
2024-07-08 00:44:20 +00:00
return EXIT_FAILURE;
}
2024-07-08 00:44:20 +00:00
/* process arguments */
bool force_debug = false;
bool force_release = false;
bool data_dir_mounted = false;
for (int i = 1; i < argc; ++i) {
/* override data directory */
if (SDL_strncmp(argv[i], "--data-dir", sizeof "--data-dir" - 1) == 0) {
if (argv[i+1] == NULL || SDL_strncmp(argv[i+1], "--", 2) == 0) {
CRY("Data dir mount override failed.", "No arguments passed (expected 1).");
return EXIT_FAILURE;
}
if (!try_mounting_root_pack(argv[i+1]))
2024-07-08 00:44:20 +00:00
return EXIT_FAILURE;
data_dir_mounted = true;
continue;
}
/* force debug mode */
if (SDL_strncmp(argv[i], "--debug", sizeof "--debug" - 1) == 0) {
force_debug = true;
continue;
}
/* force release mode */
if (SDL_strncmp(argv[i], "--release", sizeof "--release" - 1) == 0) {
2025-01-15 04:58:42 +00:00
force_release = true;
continue;
2024-07-08 00:44:20 +00:00
}
}
/* data path not explicitly specified, look into convention defined places */
if (!data_dir_mounted) {
/* try mouning data folder first, relative to executable root */
char *full_path;
SDL_asprintf(&full_path, "%sdata", ctx.base_dir);
if (!try_mounting_root_pack(full_path)) {
SDL_free(full_path);
SDL_asprintf(&full_path, "%sdata.btw", ctx.base_dir);
if (!try_mounting_root_pack(full_path)) {
SDL_free(full_path);
CRY_PHYSFS("Cannot find data.btw or data directory in root. Please create them or specify with --data-dir parameter.");
return EXIT_FAILURE;
2024-10-08 07:13:07 +00:00
}
}
SDL_free(full_path);
}
if (!initialize())
return EXIT_FAILURE;
/* good time to override anything that was set in initialize() */
if (force_debug) {
ctx.game.debug = true;
}
if (force_release) {
ctx.game.debug = false;
}
2024-07-08 00:44:20 +00:00
ctx.was_successful = true;
ctx.game.initialization_needed = true;
2024-08-21 13:55:34 +00:00
SDL_InitSubSystem(SDL_INIT_EVENTS);
2025-02-07 07:19:36 +00:00
workers_init(SDL_GetCPUCount());
profile_end("startup");
while (ctx.is_running) {
/* dispatch all filewatch driven events, such as game object and asset pack reload */
filewatch_poll();
2024-07-08 00:44:20 +00:00
main_loop();
2024-08-21 13:55:34 +00:00
}
2024-07-08 00:44:20 +00:00
if (ctx.game.debug)
profile_list_stats();
/* loop is over */
game_object_unload();
2024-08-21 13:55:34 +00:00
2024-07-08 00:44:20 +00:00
clean_up();
return ctx.was_successful ? EXIT_SUCCESS : EXIT_FAILURE;
}