#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" #include "twn_workers_c.h" #include #include #include #include #include #include #define TICKS_PER_SECOND_DEFAULT 60 #define PACKAGE_EXTENSION "btw" static void pack_contents_modified(char const *path, enum FilewatchAction action); static SDL_sem *opengl_load_semaphore; /* note: it drives most of IO implicitly, such as audio callbacks */ static void poll_events(void) { SDL_Event e; ctx.window_size_has_changed = false; while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_QUIT: ctx.is_running = false; break; case SDL_WINDOWEVENT: if (e.window.windowID != ctx.window_id) break; switch (e.window.event) { case SDL_WINDOWEVENT_SIZE_CHANGED: 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; break; case SDL_WINDOWEVENT_FOCUS_LOST: { ctx.window_mouse_resident = false; break; } case SDL_WINDOWEVENT_FOCUS_GAINED: { ctx.window_mouse_resident = true; break; } default: break; } break; default: break; } } } 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) { /* if (!ctx.is_running) { end(ctx); clean_up(ctx); emscripten_cancel_main_loop(); } */ /* 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; /* 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, ¤t_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) { 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; /* finally, let's get to work */ int frames = 0; while (ctx.frame_accumulator >= ctx.desired_frametime) { frames += 1; /* TODO: disable rendering pushes on not-last ? */ render_clear(); poll_events(); if (ctx.window_size_has_changed) update_viewport(); input_state_update(&ctx.input); profile_start("game tick"); if (!ctx.no_sanity_timer) start_sanity_timer(2000); game_object_tick(); if (!ctx.no_sanity_timer) end_sanity_timer(); profile_end("game tick"); 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: "); } } /* TODO: ctx.game_copy = ctx.game should be placed after it, but it messes with state used in render() */ 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; } /* 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); } /* 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"); filewatch_add_directory(dep_source_path, &pack_contents_modified); 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)); /* 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"); goto fail; } */ 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; 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; /* 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())); /* 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; 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; ctx.frame_accumulator = 0; ctx.game.frame_number = 0; ctx.game.frame_duration = 1.0f / (float)ctx.ticks_per_second; /* 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; } /* engine configuration */ { toml_datum_t datum_texture_atlas_size = toml_int_in(engine, "texture_atlas_size"); if (!datum_texture_atlas_size.ok || datum_texture_atlas_size.u.i < 32) { ctx.texture_atlas_size = TEXTURE_ATLAS_SIZE_DEFAULT; } 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"); if (!datum_font_texture_size.ok || datum_font_texture_size.u.i < 1024) { ctx.font_texture_size = TEXT_FONT_TEXTURE_SIZE_DEFAULT; } else ctx.font_texture_size = datum_font_texture_size.u.i; toml_datum_t datum_font_oversampling = toml_int_in(engine, "font_oversampling"); if (!datum_font_oversampling.ok || datum_font_oversampling.u.i < 0) { ctx.font_oversampling = TEXT_FONT_OVERSAMPLING_DEFAULT; } 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 { if (SDL_strncmp(datum_font_filtering.u.s, "nearest", sizeof "nearest" - 1) == 0) { ctx.font_filtering = TEXTURE_FILTER_NEAREAST; } 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); 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); /* input */ toml_datum_t datum_keybind_slots = toml_int_in(engine, "keybind_slots"); if (!datum_keybind_slots.ok || datum_keybind_slots.u.i < 1) { ctx.keybind_slots = KEYBIND_SLOTS_DEFAULT; } else ctx.keybind_slots = datum_keybind_slots.u.i; } input_state_init(&ctx.input); 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"); SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); 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"); /* 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); models_state_init(); profile_end("texture and text cache initialization"); 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); text_cache_deinit(&ctx.text_cache); textures_cache_deinit(&ctx.texture_cache); arrfree(ctx.render_queue_2d); toml_free(ctx.config_table); PHYSFS_deinit(); workers_deinit(); models_state_deinit(); SDL_free(ctx.base_dir); SDL_free(ctx.title); SDL_GL_DeleteContext(ctx.gl_context); SDL_GL_UnloadLibrary(); SDL_QuitSubSystem(SDL_INIT_EVENTS); if (ctx.audio_initialized) SDL_QuitSubSystem(SDL_INIT_AUDIO); SDL_Quit(); } void reset_state(void) { ctx.game.initialization_needed = true; input_reset_state(&ctx.input); textures_reset_state(); } 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; ctx.argc = argc; ctx.argv = argv; ctx.base_dir = SDL_GetBasePath(); /* 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"); return EXIT_FAILURE; } /* 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])) 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) { force_release = true; continue; } /* do not use sanity timer, useful with attached debugger */ if (SDL_strncmp(argv[i], "--no-sanity-timer", sizeof "--no-sanity-timer" - 1) == 0) { ctx.no_sanity_timer = true; continue; } } /* 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; } } 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; } ctx.was_successful = true; ctx.game.initialization_needed = true; SDL_InitSubSystem(SDL_INIT_EVENTS); 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(); main_loop(); } if (ctx.game.debug) profile_list_stats(); /* loop is over */ game_object_unload(); clean_up(); return ctx.was_successful ? EXIT_SUCCESS : EXIT_FAILURE; }