2024-07-08 00:44:20 +00:00
|
|
|
#include "context.h"
|
|
|
|
#include "rendering.h"
|
|
|
|
#include "input.h"
|
|
|
|
#include "util.h"
|
|
|
|
#include "textures.h"
|
|
|
|
#include "game/game.h"
|
2024-07-08 06:46:12 +00:00
|
|
|
#include "private/audio.h"
|
2024-07-08 00:44:20 +00:00
|
|
|
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
#include <SDL2/SDL_ttf.h>
|
2024-07-08 06:46:12 +00:00
|
|
|
#include <SDL2/SDL_image.h>
|
2024-07-08 00:44:20 +00:00
|
|
|
#include <physfs.h>
|
|
|
|
#include <stb_ds.h>
|
|
|
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <tgmath.h>
|
|
|
|
#include <limits.h>
|
|
|
|
|
|
|
|
|
|
|
|
static void poll_events(void) {
|
|
|
|
SDL_Event e;
|
|
|
|
|
|
|
|
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_RESIZED:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2024-07-08 20:47:22 +00:00
|
|
|
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, ¤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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* finally, let's get to work */
|
2024-07-08 20:16:24 +00:00
|
|
|
while (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
|
|
|
|
for (size_t i = 0; i < ctx.update_multiplicity; ++i) {
|
|
|
|
render_queue_clear();
|
2024-07-08 00:44:20 +00:00
|
|
|
|
2024-07-08 20:16:24 +00:00
|
|
|
poll_events();
|
|
|
|
input_state_update(&ctx.input);
|
|
|
|
game_tick();
|
2024-07-08 00:44:20 +00:00
|
|
|
|
2024-07-08 20:16:24 +00:00
|
|
|
ctx.frame_accumulator -= ctx.desired_frametime;
|
|
|
|
ctx.tick_count = (ctx.tick_count % ULLONG_MAX) + 1;
|
2024-07-08 00:44:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
render();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static bool initialize(void) {
|
|
|
|
if (SDL_Init(SDL_INIT_EVERYTHING) == -1) {
|
|
|
|
CRY_SDL("SDL initialization failed.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* init got far enough to create a window */
|
|
|
|
ctx.window = SDL_CreateWindow("emerald",
|
|
|
|
SDL_WINDOWPOS_CENTERED,
|
|
|
|
SDL_WINDOWPOS_CENTERED,
|
|
|
|
RENDER_BASE_WIDTH,
|
|
|
|
RENDER_BASE_HEIGHT,
|
|
|
|
SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE);
|
|
|
|
if (ctx.window == NULL) {
|
|
|
|
CRY_SDL("Window creation failed.");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* might need this to have multiple windows */
|
|
|
|
ctx.window_id = SDL_GetWindowID(ctx.window);
|
|
|
|
|
|
|
|
/* now that we have a window, we know a renderer can be created */
|
|
|
|
ctx.renderer = SDL_CreateRenderer(ctx.window, -1, SDL_RENDERER_PRESENTVSYNC);
|
|
|
|
|
|
|
|
/* SDL_SetHint(SDL_HINT_RENDER_LOGICAL_SIZE_MODE, "overscan"); */
|
|
|
|
/* SDL_RenderSetIntegerScale(ctx.renderer, SDL_TRUE); */
|
|
|
|
SDL_RenderSetLogicalSize(ctx.renderer, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT);
|
|
|
|
SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
|
|
|
|
|
2024-07-08 06:46:12 +00:00
|
|
|
/* audio initialization */
|
|
|
|
{
|
|
|
|
|
|
|
|
SDL_AudioSpec request, got;
|
|
|
|
SDL_zero(request);
|
|
|
|
|
|
|
|
request.freq = AUDIO_FREQUENCY;
|
|
|
|
request.format = AUDIO_S16;
|
|
|
|
request.channels = AUDIO_N_CHANNELS;
|
|
|
|
request.callback = audio_callback;
|
|
|
|
/* TODO: check for errors */
|
|
|
|
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
|
|
|
|
ctx.audio_stream_format = got.format;
|
|
|
|
ctx.audio_stream_frequency = got.freq;
|
|
|
|
ctx.audio_stream_channel_count = got.channels;
|
|
|
|
|
|
|
|
SDL_PauseAudioDevice(ctx.audio_device, 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-07-08 00:44:20 +00:00
|
|
|
/* images */
|
|
|
|
if (IMG_Init(IMG_INIT_PNG) == 0) {
|
|
|
|
CRY_SDL("SDL_image initialization failed.");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* filesystem time */
|
|
|
|
/* TODO: ANDROID: see the warning in physicsfs PHYSFS_init docs/header */
|
|
|
|
if (!PHYSFS_init(ctx.argv[0]) ||
|
|
|
|
!PHYSFS_setSaneConfig(ORGANIZATION_NAME, APP_NAME, PACKAGE_EXTENSION, false, true))
|
|
|
|
{
|
|
|
|
CRY_PHYSFS("Filesystem initialization failed.");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* you could change this at runtime if you wanted */
|
|
|
|
ctx.update_multiplicity = 1;
|
|
|
|
|
|
|
|
/* debug mode _defaults_ to being enabled on debug builds. */
|
|
|
|
/* you should be able to enable it at runtime on any build */
|
|
|
|
#ifndef NDEBUG
|
|
|
|
ctx.debug = true;
|
|
|
|
#else
|
|
|
|
ctx.debug = false;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* random seeding */
|
|
|
|
/* SDL_GetPerformanceCounter returns some platform-dependent number. */
|
|
|
|
/* it should vary between game instances. i checked! random enough for me. */
|
|
|
|
ctx.random_seed = SDL_GetPerformanceCounter();
|
|
|
|
srand((unsigned int)ctx.random_seed);
|
|
|
|
stbds_rand_seed(ctx.random_seed);
|
|
|
|
|
|
|
|
/* main loop machinery */
|
|
|
|
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 / TICKS_PER_SECOND;
|
|
|
|
ctx.frame_accumulator = 0;
|
|
|
|
ctx.tick_count = 0;
|
|
|
|
|
|
|
|
/* 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* rendering */
|
|
|
|
/* these are dynamic arrays and will be allocated lazily by stb_ds */
|
|
|
|
ctx.render_queue_sprites = NULL;
|
|
|
|
ctx.render_queue_rectangles = NULL;
|
|
|
|
ctx.render_queue_circles = NULL;
|
|
|
|
ctx.circle_radius_hash = NULL;
|
|
|
|
|
|
|
|
textures_cache_init(&ctx.texture_cache, ctx.window);
|
|
|
|
if (TTF_Init() < 0) {
|
|
|
|
CRY_SDL("SDL_ttf initialization failed.");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* input */
|
|
|
|
input_state_init(&ctx.input, ctx.renderer);
|
|
|
|
|
|
|
|
/* scripting */
|
|
|
|
/*
|
|
|
|
if (!scripting_init(ctx)) {
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
fail:
|
|
|
|
SDL_Quit();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* will not be called on an abnormal exit */
|
|
|
|
static void clean_up(void) {
|
|
|
|
/*
|
|
|
|
scripting_deinit(ctx);
|
|
|
|
*/
|
|
|
|
|
|
|
|
input_state_deinit(&ctx.input);
|
|
|
|
|
|
|
|
arrfree(ctx.render_queue_sprites);
|
|
|
|
arrfree(ctx.render_queue_rectangles);
|
|
|
|
textures_cache_deinit(&ctx.texture_cache);
|
|
|
|
|
|
|
|
PHYSFS_deinit();
|
|
|
|
TTF_Quit();
|
|
|
|
IMG_Quit();
|
|
|
|
SDL_Quit();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
|
|
ctx.argc = argc;
|
|
|
|
ctx.argv = argv;
|
|
|
|
|
|
|
|
|
|
|
|
if (!initialize())
|
|
|
|
return EXIT_FAILURE;
|
|
|
|
|
|
|
|
for (int i = 1; i < (argc - 1); ++i) {
|
|
|
|
if (strcmp(argv[i], "--data-dir") == 0) {
|
|
|
|
if (!PHYSFS_mount(argv[i+1], NULL, true)) {
|
|
|
|
CRY_PHYSFS("Data dir mount override failed.");
|
|
|
|
return EXIT_FAILURE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.was_successful = true;
|
|
|
|
while (ctx.is_running)
|
|
|
|
main_loop();
|
|
|
|
|
|
|
|
game_end();
|
|
|
|
clean_up();
|
|
|
|
|
|
|
|
return ctx.was_successful ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
|
|
}
|