awesome!!!

This commit is contained in:
veclavtalica
2024-07-08 03:44:20 +03:00
commit 206a5b7cad
529 changed files with 148340 additions and 0 deletions

31
src/config.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef CONFIG_H
#define CONFIG_H
/*
* this file is for configuration values which are to be set at
* compile time. generally speaking, it's for things that would be unwise to
* change mid-development without considering the work it might take to
* adapt the game logic.
*
* if you're looking for distribution-related definitions like
* APP_NAME, you should know that they are set from CMake.
*/
#define TICKS_PER_SECOND 60
#define FIXED_DELTA_TIME (1.0 / TICKS_PER_SECOND)
#define RENDER_BASE_WIDTH 640
#define RENDER_BASE_HEIGHT 360
#define TEXTURE_ATLAS_SIZE 2048
#define TEXTURE_ATLAS_BIT_DEPTH 32
#define TEXTURE_ATLAS_FORMAT SDL_PIXELFORMAT_RGBA32
#define NUM_KEYBIND_SLOTS 8
/* 1024 * 1024 */
/* #define UMKA_STACK_SIZE 1048576 */
#endif

3
src/context.c Normal file
View File

@ -0,0 +1,3 @@
#include "context.h"
t_ctx ctx;

68
src/context.h Normal file
View File

@ -0,0 +1,68 @@
#ifndef CONTEXT_H
#define CONTEXT_H
#include "config.h"
#include "private/rendering.h"
#include "textures.h"
#include "input.h"
#include "game_api.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <stdint.h>
typedef struct context {
/* the program's actual argc and argv */
int argc;
char **argv;
struct texture_cache texture_cache;
struct input_state input;
struct sprite_primitive *render_queue_sprites;
struct rect_primitive *render_queue_rectangles;
struct circle_primitive *render_queue_circles;
struct audio_channel *audio_channels;
struct circle_radius_cache_item *circle_radius_hash;
/* main loop machinery */
int64_t clocks_per_second;
int64_t prev_frame_time;
int64_t desired_frametime; /* how long one tick should be */
int64_t frame_accumulator;
int64_t delta_averager_residual;
int64_t time_averager[4];
uint64_t tick_count;
uint64_t step_count;
/* set just once on startup */
uint64_t random_seed;
/* this should be a multiple of TICKS_PER_SECOND */
/* use it to simulate low framerate (e.g. at 60 tps, set to 2 for 30 fps) */
/* it can be changed at runtime; any resulting logic anomalies are bugs */
unsigned int update_multiplicity;
SDL_Renderer *renderer;
SDL_Window *window;
uint32_t window_id;
int window_w;
int window_h;
bool was_successful;
/* you may read from and write to these from game code */
void *udata;
bool debug;
bool is_running;
bool resync_flag;
} t_ctx;
extern t_ctx ctx;
#endif

70
src/game/game.c Normal file
View File

@ -0,0 +1,70 @@
#include "game.h"
#include "../textures.h"
#include "../game_api.h"
#include "state.h"
#include "scenes/scene.h"
#include "scenes/title.h"
#include <stdio.h>
#include <malloc.h>
#include <stdint.h>
void game_step(int64_t delta) {
(void)delta;
}
void game_tick(void) {
if (ctx.tick_count == 0) {
ctx.udata = ccalloc(1, sizeof (struct state));
struct state *state = ctx.udata;
state->ctx = &ctx;
state->scene = title_scene(state);
input_add_action(&ctx.input, "debug_dump_atlases");
input_bind_action_scancode(&ctx.input, "debug_dump_atlases", SDL_SCANCODE_HOME);
input_add_action(&ctx.input, "debug_toggle");
input_bind_action_scancode(&ctx.input, "debug_toggle", SDL_SCANCODE_BACKSPACE);
input_add_action(&ctx.input, "player_left");
input_bind_action_scancode(&ctx.input, "player_left", SDL_SCANCODE_LEFT);
input_add_action(&ctx.input, "player_right");
input_bind_action_scancode(&ctx.input, "player_right", SDL_SCANCODE_RIGHT);
input_add_action(&ctx.input, "player_jump");
input_bind_action_scancode(&ctx.input, "player_jump", SDL_SCANCODE_X);
input_add_action(&ctx.input, "ui_accept");
input_bind_action_scancode(&ctx.input, "ui_accept", SDL_SCANCODE_RETURN);
}
struct state *state = ctx.udata;
if (input_is_action_just_pressed(&ctx.input, "debug_dump_atlases")) {
textures_dump_atlases(&ctx.texture_cache);
}
if (input_is_action_just_pressed(&ctx.input, "debug_toggle")) {
ctx.debug = !ctx.debug;
}
state->scene->tick(state);
/* there's a scene switch pending, we can do it now that the tick is done */
if (state->next_scene != NULL) {
state->scene->end(state);
state->scene = state->next_scene;
state->is_scene_switching = false;
state->next_scene = NULL;
}
}
void game_end(void) {
struct state *state = ctx.udata;
state->scene->end(state);
free(state);
}

15
src/game/game.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef GAME_H
#define GAME_H
#include "../game_api.h"
#include <stdint.h>
void game_step(int64_t delta);
void game_tick(void);
void game_end(void);
#endif

303
src/game/player.c Normal file
View File

@ -0,0 +1,303 @@
#include "player.h"
#include "world.h"
#include "../game_api.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <stdlib.h>
#include <tgmath.h>
static void update_timers(struct player *player) {
tick_timer(&player->jump_air_timer);
tick_timer(&player->jump_coyote_timer);
tick_timer(&player->jump_buffer_timer);
}
static void input_move(struct input_state *input, struct player *player) {
/* apply horizontal damping when the player stops moving */
/* in other words, make it decelerate to a standstill */
if (!input_is_action_pressed(input, "player_left") &&
!input_is_action_pressed(input, "player_right"))
{
player->dx *= player->horizontal_damping;
}
int input_dir = 0;
if (input_is_action_pressed(input, "player_left"))
input_dir = -1;
if (input_is_action_pressed(input, "player_right"))
input_dir = 1;
if (input_is_action_pressed(input, "player_left") &&
input_is_action_pressed(input, "player_right"))
input_dir = 0;
player->dx += (float)input_dir * player->run_horizontal_speed;
player->dx = SDL_clamp(player->dx, -player->max_dx, player->max_dx);
if (fabs(player->dx) < player->run_horizontal_speed) {
player->dx = 0;
}
}
static void jump(struct player *player) {
player->jump_coyote_timer = 0;
player->jump_buffer_timer = 0;
player->dy = player->jump_force_initial;
player->action = PLAYER_ACTION_JUMP;
player->jump_air_timer = player->jump_air_ticks;
}
static void input_jump(struct input_state *input, struct player *player) {
player->current_gravity_multiplier = player->jump_default_multiplier;
if (input_is_action_just_pressed(input, "player_jump")) {
player->jump_air_timer = 0;
player->jump_buffer_timer = player->jump_buffer_ticks;
if (player->action == PLAYER_ACTION_GROUND || player->jump_coyote_timer > 0) {
jump(player);
}
}
if (input_is_action_pressed(input, "player_jump")) {
if (player->action != PLAYER_ACTION_GROUND && player->jump_air_timer > 0) {
player->current_gravity_multiplier = player->jump_boosted_multiplier;
player->dy += player->jump_force_increase;
}
}
}
static void update_collider_x(struct player *player) {
player->collider_x.w = player->rect.w;
player->collider_x.h = player->rect.h - 8;
player->collider_x.x = player->rect.x;
player->collider_x.y = player->rect.y + ((player->rect.h - player->collider_x.h) / 2);
}
static void update_collider_y(struct player *player) {
player->collider_y.w = player->rect.w;
player->collider_y.h = player->rect.h;
player->collider_y.x = player->rect.x + ((player->rect.w - player->collider_y.w) / 2);
player->collider_y.y = player->rect.y;
}
static void apply_gravity(struct player *player, float gravity) {
player->dy -= gravity * player->current_gravity_multiplier;
player->dy = fmax(player->dy, -player->terminal_velocity);
if (player->dy < 0) {
/* just started falling */
if (player->action == PLAYER_ACTION_GROUND) {
player->jump_coyote_timer = player->jump_coyote_ticks;
}
player->action = PLAYER_ACTION_FALL;
}
}
/* returns whether or not a correction was applied */
static bool corner_correct(struct player *player, t_frect collision) {
/*
* somewhat of a hack here. we only want to do corner correction
* if the corner in question really is the corner of a "platform,"
* and not simply the edge of a single tile in a row of many
* tiles, as that would briefly clip the player into the ceiling,
* halting movement. the dumbest way i could think of to fix this
* is to simply ensure that there's no tile to possibly clip into
* before correcting, by checking if there's a tile right above
* the center of the player
*/
if (world_is_tile_at(player->world,
player->rect.x + (player->rect.w / 2),
player->rect.y - 1))
{
return false;
}
float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
float collision_center_x = collision.x + (collision.w / 2);
float abs_difference = fabs(player_center_x - collision_center_x);
/* we're good, no correction needed */
if (abs_difference < player->jump_corner_correction_offset)
return false;
/* collision was on player's right side */
if (player_center_x < collision_center_x) {
player->rect.x -= collision.w / 2;
}
/* collision was on player's left side */
else if (player_center_x > collision_center_x) {
player->rect.x += collision.w / 2;
}
return true;
}
static void calc_collisions_x(struct player *player) {
t_frect collision;
bool is_colliding = world_find_intersect_frect(player->world, player->collider_x, &collision);
if (!is_colliding) return;
float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
float collision_center_x = collision.x + (collision.w / 2);
enum collision_direction { COLLISION_LEFT = -1, COLLISION_RIGHT = 1 };
enum collision_direction dir_x =
player_center_x > collision_center_x ? COLLISION_LEFT : COLLISION_RIGHT;
player->rect.x -= collision.w * (float)dir_x;
player->dx = 0;
}
static void calc_collisions_y(struct player *player) {
t_frect collision;
bool is_colliding = world_find_intersect_frect(player->world, player->collider_y, &collision);
if (!is_colliding) return;
float player_center_y = player->collider_y.y + (player->collider_y.h / 2);
float collision_center_y = collision.y + (collision.h / 2);
enum collision_direction { COLLISION_ABOVE = -1, COLLISION_BELOW = 1 };
enum collision_direction dir_y =
player_center_y > collision_center_y ? COLLISION_ABOVE : COLLISION_BELOW;
/* before the resolution */
if (dir_y == COLLISION_ABOVE) {
if (corner_correct(player, collision)) {
return;
}
}
/* resolution */
player->rect.y -= collision.h * (float)dir_y;
player->dy = 0;
/* after the resolution */
if (dir_y == COLLISION_BELOW) {
/* bandaid fix for float precision-related jittering */
player->rect.y = ceilf(player->rect.y);
player->action = PLAYER_ACTION_GROUND;
if (player->jump_buffer_timer > 0) {
jump(player);
apply_gravity(player, player->world->gravity);
}
} else if (dir_y == COLLISION_ABOVE) {
player->jump_air_timer = 0;
}
}
struct player *player_create(struct world *world) {
struct player *player = cmalloc(sizeof *player);
*player = (struct player) {
.world = world,
.sprite_w = 48,
.sprite_h = 48,
.rect = (t_frect) {
.x = 92,
.y = 200,
.w = 16,
.h = 32,
},
.action = PLAYER_ACTION_GROUND,
.collider_thickness = 42,
.max_dx = 8,
.run_horizontal_speed = 0.5f,
.horizontal_damping = 0.9f,
.current_gravity_multiplier = 1,
.terminal_velocity = 30,
.jump_force_initial = 10,
.jump_force_increase = 1,
.jump_air_ticks = 18,
.jump_coyote_ticks = 8,
.jump_buffer_ticks = 8,
.jump_default_multiplier = 1.4f,
.jump_boosted_multiplier = 1,
.jump_corner_correction_offset = 16.0f,
};
return player;
}
static void drawdef(struct player *player) {
push_sprite("/assets/player/baron-walk.png",
(t_frect) {
.x = player->rect.x + ((player->rect.w - player->sprite_w) / 2),
.y = player->rect.y - 8,
.w = player->sprite_w,
.h = player->sprite_h,
});
push_circle((t_fvec2) { 256, 128 },
24,
(t_color) { 255, 0, 0, 255 });
}
static void drawdef_debug(struct player *player) {
if (!ctx.debug)
return;
/* const int info_separation = 24; */
/* const struct RectPrimitive info_theme = { */
/* .x = 8, */
/* .r = 0, .g = 0, .b = 0, .a = 255, */
/* }; */
push_rectangle(player->collider_x,
(t_color){ 0, 0, 255, 128 });
push_rectangle(player->collider_y,
(t_color){ 0, 0, 255, 128 });
}
void player_destroy(struct player *player) {
free(player);
}
void player_calc(struct player *player) {
update_timers(player);
input_move(&ctx.input, player);
input_jump(&ctx.input, player);
player->rect.x += player->dx;
update_collider_x(player);
calc_collisions_x(player);
apply_gravity(player, player->world->gravity);
player->rect.y -= player->dy;
update_collider_y(player);
calc_collisions_y(player);
update_collider_x(player);
update_collider_y(player);
drawdef(player);
drawdef_debug(player);
}

66
src/game/player.h Normal file
View File

@ -0,0 +1,66 @@
#ifndef PLAYER_H
#define PLAYER_H
#include "../game_api.h"
struct world;
enum player_action {
PLAYER_ACTION_GROUND,
PLAYER_ACTION_FALL,
PLAYER_ACTION_JUMP,
};
struct player {
struct world *world;
/* visual */
float sprite_w;
float sprite_h;
/* body */
t_frect rect;
/* state */
enum player_action action;
/* physics */
t_frect collider_x;
t_frect collider_y;
int collider_thickness;
float dx;
float dy;
float max_dx;
float run_horizontal_speed;
float horizontal_damping;
float current_gravity_multiplier;
float terminal_velocity;
/* jumping */
float jump_force_initial;
float jump_force_increase; /* aka "jump power", increment of dy per tick */
int jump_air_ticks;
int jump_air_timer;
int jump_coyote_ticks;
int jump_coyote_timer;
int jump_buffer_ticks;
int jump_buffer_timer;
/* gravity multipliers */
float jump_default_multiplier;
float jump_boosted_multiplier;
float jump_corner_correction_offset; /* from center */
};
struct player *player_create(struct world *world);
void player_destroy(struct player *player);
void player_calc(struct player *player);
#endif

32
src/game/scenes/ingame.c Normal file
View File

@ -0,0 +1,32 @@
#include "ingame.h"
#include "title.h"
#include "scene.h"
static void ingame_tick(struct state *state) {
struct scene_ingame *scn = (struct scene_ingame *)state->scene;
world_drawdef(scn->world);
player_calc(scn->player);
}
static void ingame_end(struct state *state) {
struct scene_ingame *scn = (struct scene_ingame *)state->scene;
player_destroy(scn->player);
world_destroy(scn->world);
}
struct scene *ingame_scene(struct state *state) {
(void)state;
struct scene_ingame *new_scene = ccalloc(1, sizeof *new_scene);
new_scene->base.tick = ingame_tick;
new_scene->base.end = ingame_end;
new_scene->world = world_create();
new_scene->player = player_create(new_scene->world);
return (struct scene *)new_scene;
}

23
src/game/scenes/ingame.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef INGAME_H
#define INGAME_H
#include "../../game_api.h"
#include "../state.h"
#include "scene.h"
#include "../player.h"
#include "../world.h"
struct scene_ingame {
struct scene base;
struct world *world;
struct player *player;
};
struct scene *ingame_scene(struct state *state);
#endif

8
src/game/scenes/scene.c Normal file
View File

@ -0,0 +1,8 @@
#include "scene.h"
#include "../state.h"
void switch_to(struct state *state, struct scene *(*scene_func)(struct state *)) {
state->next_scene = scene_func(state);
state->is_scene_switching = true;
}

16
src/game/scenes/scene.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef SCENE_H
#define SCENE_H
struct state;
struct scene {
char *id;
void (*tick)(struct state *);
void (*end)(struct state *);
};
void switch_to(struct state *state, struct scene *(*scene_func)(struct state *));
#endif

41
src/game/scenes/title.c Normal file
View File

@ -0,0 +1,41 @@
#include "title.h"
#include "ingame.h"
#include "../world.h"
#include "../player.h"
#include "../../game_api.h"
static void title_tick(struct state *state) {
struct scene_title *scn = (struct scene_title *)state->scene;
(void)scn;
if (input_is_action_just_pressed(&state->ctx->input, "ui_accept")) {
switch_to(state, ingame_scene);
}
push_sprite("/assets/title.png", (t_frect) {
(RENDER_BASE_WIDTH / 2) - (320 / 2), 64, 320, 128
});
}
static void title_end(struct state *state) {
struct scene_title *scn = (struct scene_title *)state->scene;
player_destroy(scn->player);
world_destroy(scn->world);
}
struct scene *title_scene(struct state *state) {
(void)state;
struct scene_title *new_scene = ccalloc(1, sizeof *new_scene);
new_scene->base.tick = title_tick;
new_scene->base.end = title_end;
new_scene->world = world_create();
new_scene->player = player_create(new_scene->world);
return (struct scene *)new_scene;
}

23
src/game/scenes/title.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef TITLE_H
#define TITLE_H
#include "../../game_api.h"
#include "../state.h"
#include "scene.h"
#include "../player.h"
#include "../world.h"
struct scene_title {
struct scene base;
struct world *world;
struct player *player;
};
struct scene *title_scene(struct state *state);
#endif

16
src/game/state.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef STATE_H
#define STATE_H
#include "../game_api.h"
struct state {
t_ctx *ctx;
struct scene *scene;
struct scene *next_scene;
bool is_scene_switching;
};
#endif

187
src/game/world.c Normal file
View File

@ -0,0 +1,187 @@
#include "world.h"
#include "../game_api.h"
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include <tgmath.h>
static void update_tiles(struct world *world) {
for (size_t row = 0; row < world->tilemap_height; ++row) {
for (size_t col = 0; col < world->tilemap_width; ++col) {
world->tiles[(row * world->tilemap_width) + col] = (struct tile) {
.rect = (t_rect) {
.x = (int)col * world->tile_size,
.y = (int)row * world->tile_size,
.w = world->tile_size,
.h = world->tile_size,
},
.type = world->tilemap[row][col],
};
}
}
}
static t_vec2 to_grid_location(struct world *world, float x, float y) {
return (t_vec2) {
.x = (int)floor(x / (float)world->tile_size),
.y = (int)floor(y / (float)world->tile_size),
};
}
static void drawdef_debug(struct world *world) {
if (!ctx.debug) return;
for (size_t i = 0; i < world->tilemap_height * world->tilemap_width; ++i) {
if (world->tiles[i].type == TILE_TYPE_VOID) continue;
push_rectangle(to_frect(world->tiles[i].rect),
(t_color) { 255, 0, 255, 128 });
}
}
struct world *world_create(void) {
struct world *world = cmalloc(sizeof *world);
*world = (struct world) {
.tiles = NULL,
.tile_size = 42,
.tilemap_width = 20,
.tilemap_height = 12,
.gravity = 1,
};
/* create the tilemap */
/* it simply stores what's in each tile as a 2d array */
/* on its own, it's entirely unrelated to drawing or logic */
world->tilemap = cmalloc(sizeof *world->tilemap * world->tilemap_height);
for (size_t i = 0; i < world->tilemap_height; ++i) {
world->tilemap[i] = cmalloc(sizeof **world->tilemap * world->tilemap_width);
for (size_t j = 0; j < world->tilemap_width; ++j) {
world->tilemap[i][j] = TILE_TYPE_VOID;
}
}
for (size_t i = 0; i < 12; ++i) {
world->tilemap[world->tilemap_height-2][2+i] = TILE_TYPE_SOLID;
}
world->tilemap[world->tilemap_height-3][8] = TILE_TYPE_SOLID;
world->tilemap[world->tilemap_height-4][8] = TILE_TYPE_SOLID;
world->tilemap[world->tilemap_height-5][10] = TILE_TYPE_SOLID;
world->tilemap[world->tilemap_height-6][10] = TILE_TYPE_SOLID;
for (size_t i = 0; i < 7; ++i) {
world->tilemap[world->tilemap_height-6][12+i] = TILE_TYPE_SOLID;
}
/* the tiles array contains data meant to be used by other logic */
/* most importantly, it is used to draw the tiles */
const size_t tile_count = world->tilemap_height * world->tilemap_width;
world->tiles = cmalloc(sizeof *world->tiles * tile_count);
update_tiles(world);
return world;
}
void world_destroy(struct world *world) {
free(world->tiles);
for (size_t i = 0; i < world->tilemap_height; ++i) {
free(world->tilemap[i]);
}
free(world->tilemap);
free(world);
}
void world_drawdef(struct world *world) {
for (size_t i = 0; i < world->tilemap_height * world->tilemap_width; ++i) {
if (world->tiles[i].type == TILE_TYPE_VOID)
continue;
push_sprite("/assets/white.png", to_frect(world->tiles[i].rect));
}
drawdef_debug(world);
}
bool world_find_intersect_frect(struct world *world, t_frect rect, t_frect *intersection) {
bool is_intersecting = false;
const size_t tile_count = world->tilemap_height * world->tilemap_width;
for (size_t i = 0; i < tile_count; ++i) {
if (world->tiles[i].type == TILE_TYPE_VOID)
continue;
t_frect tile_frect = {
.x = (float)(world->tiles[i].rect.x),
.y = (float)(world->tiles[i].rect.y),
.w = (float)(world->tiles[i].rect.w),
.h = (float)(world->tiles[i].rect.h),
};
if (intersection == NULL) {
t_frect temp;
is_intersecting = intersect_frect(&rect, &tile_frect, &temp);
} else {
is_intersecting = intersect_frect(&rect, &tile_frect, intersection);
}
if (is_intersecting)
break;
}
return is_intersecting;
}
bool world_find_intersect_rect(struct world *world, t_rect rect, t_rect *intersection) {
bool is_intersecting = false;
const size_t tile_count = world->tilemap_height * world->tilemap_width;
for (size_t i = 0; i < tile_count; ++i) {
if (world->tiles[i].type == TILE_TYPE_VOID)
continue;
t_rect *tile_rect = &world->tiles[i].rect;
if (intersection == NULL) {
t_rect temp;
is_intersecting = intersect_rect(&rect, tile_rect, &temp);
} else {
is_intersecting = intersect_rect(&rect, tile_rect, intersection);
}
if (is_intersecting)
break;
}
return is_intersecting;
}
bool world_is_tile_at(struct world *world, float x, float y) {
t_vec2 position_in_grid = to_grid_location(world, x, y);
return world->tilemap[position_in_grid.y][position_in_grid.x] != TILE_TYPE_VOID;
}
void world_place_tile(struct world *world, float x, float y) {
t_vec2 position_in_grid = to_grid_location(world, x, y);
world->tilemap[position_in_grid.y][position_in_grid.x] = TILE_TYPE_SOLID;
update_tiles(world);
}
void world_remove_tile(struct world *world, float x, float y) {
t_vec2 position_in_grid = to_grid_location(world, x, y);
world->tilemap[position_in_grid.y][position_in_grid.x] = TILE_TYPE_VOID;
update_tiles(world);
}

44
src/game/world.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef WORLD_H
#define WORLD_H
#include "../game_api.h"
#include <stdint.h>
#include <stdbool.h>
enum tile_type {
TILE_TYPE_VOID,
TILE_TYPE_SOLID,
};
struct tile {
t_rect rect;
enum tile_type type;
};
struct world {
enum tile_type **tilemap;
struct tile *tiles;
int tile_size;
unsigned int tilemap_width;
unsigned int tilemap_height;
size_t tile_nonvoid_count;
float gravity;
};
struct world *world_create(void);
void world_destroy(struct world *world);
void world_drawdef(struct world *world);
bool world_find_intersect_frect(struct world *world, t_frect rect, t_frect *intersection);
bool world_find_intersect_rect(struct world *world, t_rect rect, t_rect *intersection);
bool world_is_tile_at(struct world *world, float x, float y);
void world_place_tile(struct world *world, float x, float y);
void world_remove_tile(struct world *world, float x, float y);
#endif

13
src/game_api.h Normal file
View File

@ -0,0 +1,13 @@
/* include this header in game code to get the usable parts of the engine */
#ifndef GAME_API_H
#define GAME_API_H
#include "context.h"
#include "rendering.h"
#include "util.h"
#include <SDL2/SDL.h>
#endif

296
src/input.c Normal file
View File

@ -0,0 +1,296 @@
#include "input.h"
#include "util.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <stdbool.h>
#include <stdlib.h>
static void update_action_pressed_state(struct input_state *input, struct action *action) {
for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
switch (action->bindings[i].source) {
case BUTTON_SOURCE_NOT_SET:
break;
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
/* not pressed */
if (input->keyboard_state[action->bindings[i].code.scancode] == 0) {
action->just_changed = action->is_pressed;
action->is_pressed = false;
}
/* pressed */
else {
action->just_changed = !action->is_pressed;
action->is_pressed = true;
return;
}
break;
case BUTTON_SOURCE_MOUSE:
/* not pressed */
if ((input->mouse_state & action->bindings[i].code.mouse_button) == 0) {
action->just_changed = action->is_pressed;
action->is_pressed = false;
}
/* pressed */
else {
action->just_changed = !action->is_pressed;
action->is_pressed = true;
/*
* SDL_RenderWindowToLogical will turn window mouse
* coords into a position inside the logical render
* area. this has to be done to get an accurate point
* that can actually be used in game logic
*/
SDL_RenderWindowToLogical(input->renderer,
input->mouse_window_position.x,
input->mouse_window_position.y,
&action->position.x,
&action->position.y);
return;
}
break;
default:
break;
}
}
}
static void input_bind_code_to_action(struct input_state *input,
char *action_name,
enum button_source source,
union button_code code)
{
struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
if (action_item == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return;
}
struct action *action = &action_item->value;
/* check every binding to make sure this code isn't already bound */
for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
struct button *binding = &action->bindings[i];
if (binding->source != source)
break;
bool is_already_bound = false;
switch (binding->source) {
case BUTTON_SOURCE_NOT_SET:
break;
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
is_already_bound = binding->code.scancode == code.scancode;
break;
case BUTTON_SOURCE_KEYBOARD_CHARACTER:
is_already_bound = binding->code.keycode == code.keycode;
break;
case BUTTON_SOURCE_GAMEPAD:
is_already_bound = binding->code.gamepad_button == code.gamepad_button;
break;
case BUTTON_SOURCE_MOUSE:
is_already_bound = binding->code.mouse_button == code.mouse_button;
break;
}
if (is_already_bound) {
log_warn("(%s) Code already bound to action \"%s\".", __func__, action_name);
return;
}
}
/* if we're at max bindings, forget the first element and shift the rest */
if (action->num_bindings == SDL_arraysize(action->bindings)) {
--action->num_bindings;
size_t shifted_size = (sizeof action->bindings) - (sizeof action->bindings[0]);
memmove(action->bindings, action->bindings + 1, shifted_size);
}
action->bindings[action->num_bindings++] = (struct button) {
.source = source,
.code = code,
};
}
static void input_unbind_code_from_action(struct input_state *input,
char *action_name,
enum button_source source,
union button_code code)
{
struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
if (action_item == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return;
}
struct action *action = &action_item->value;
/* check every binding to make sure this code is bound */
size_t index = 0;
bool is_bound = false;
for (index = 0; index < SDL_arraysize(action->bindings); ++index) {
struct button *binding = &action->bindings[index];
if (binding->source != source)
continue;
switch (binding->source) {
case BUTTON_SOURCE_NOT_SET:
break;
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
is_bound = binding->code.scancode == code.scancode;
break;
case BUTTON_SOURCE_KEYBOARD_CHARACTER:
is_bound = binding->code.keycode == code.keycode;
break;
case BUTTON_SOURCE_GAMEPAD:
is_bound = binding->code.gamepad_button == code.gamepad_button;
break;
case BUTTON_SOURCE_MOUSE:
is_bound = binding->code.mouse_button == code.mouse_button;
break;
}
if (is_bound)
break;
}
if (!is_bound) {
log_warn("(%s) Code is not bound to action \"%s\".", __func__, action_name);
return;
}
/* remove the element to unbind and shift the rest so there isn't a gap */
if (action->num_bindings == SDL_arraysize(action->bindings)) {
size_t elements_after_index = action->num_bindings - index;
size_t shifted_size = elements_after_index * (sizeof action->bindings[0]);
memmove(action->bindings + index, action->bindings + index + 1, shifted_size);
--action->num_bindings;
}
}
void input_state_init(struct input_state *input, SDL_Renderer *renderer) {
sh_new_strdup(input->action_hash);
input->renderer = renderer;
}
void input_state_deinit(struct input_state *input) {
shfree(input->action_hash);
}
void input_state_update(struct input_state *input) {
input->keyboard_state = SDL_GetKeyboardState(NULL);
input->mouse_state = SDL_GetMouseState(&input->mouse_window_position.x,
&input->mouse_window_position.y);
for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
struct action *action = &input->action_hash[i].value;
update_action_pressed_state(input, action);
}
}
void input_bind_action_scancode(struct input_state *input,
char *action_name,
SDL_Scancode scancode)
{
input_bind_code_to_action(input,
action_name,
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
(union button_code) { .scancode = scancode });
}
void input_unbind_action_scancode(struct input_state *input,
char *action_name,
SDL_Scancode scancode)
{
input_unbind_code_from_action(input,
action_name,
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
(union button_code) { .scancode = scancode });
}
void input_bind_action_mouse(struct input_state *input,
char *action_name,
uint8_t mouse_button)
{
input_bind_code_to_action(input,
action_name,
BUTTON_SOURCE_MOUSE,
(union button_code) { .mouse_button = mouse_button});
}
void input_unbind_action_mouse(struct input_state *input,
char *action_name,
uint8_t mouse_button)
{
input_unbind_code_from_action(input,
action_name,
BUTTON_SOURCE_MOUSE,
(union button_code) { .mouse_button = mouse_button});
}
void input_add_action(struct input_state *input, char *action_name) {
if (shgeti(input->action_hash, action_name) >= 0) {
log_warn("(%s) Action \"%s\" is already registered.", __func__, action_name);
return;
}
shput(input->action_hash, action_name, (struct action) { 0 });
}
void input_delete_action(struct input_state *input, char *action_name) {
if (shdel(input->action_hash, action_name) == 0)
log_warn("(%s) Action \"%s\" is not registered.", __func__, action_name);
}
bool input_is_action_pressed(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return false;
}
return action->value.is_pressed;
}
bool input_is_action_just_pressed(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return false;
}
return action->value.is_pressed && action->value.just_changed;
}
bool input_is_action_just_released(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return false;
}
return !action->value.is_pressed && action->value.just_changed;
}
t_fvec2 input_get_action_position(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return (t_fvec2) { 0 };
}
return action->value.position;
}

92
src/input.h Normal file
View File

@ -0,0 +1,92 @@
#ifndef INPUT_H
#define INPUT_H
#include "config.h"
#include "util.h"
#include <SDL2/SDL.h>
#include <stdint.h>
#include <stdbool.h>
enum button_source {
BUTTON_SOURCE_NOT_SET,
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
BUTTON_SOURCE_KEYBOARD_CHARACTER,
BUTTON_SOURCE_GAMEPAD,
BUTTON_SOURCE_MOUSE,
};
union button_code {
SDL_Scancode scancode;
SDL_Keycode keycode;
SDL_GameControllerButton gamepad_button;
uint8_t mouse_button; /* SDL_BUTTON_ enum */
};
/* an input to which an action is bound */
/* it is not limited to literal buttons */
struct button {
enum button_source source;
union button_code code;
};
/* represents the collective state of a group of buttons */
/* that is, changes in the states of any of the bound buttons will affect it */
struct action {
size_t num_bindings;
struct button bindings[NUM_KEYBIND_SLOTS];
t_fvec2 position; /* set if applicable */
bool is_pressed;
bool just_changed;
};
struct action_hash_item {
char *key;
struct action value;
};
struct input_state {
struct action_hash_item *action_hash;
SDL_Renderer *renderer; /* some input relates to the screen in some way */
const uint8_t *keyboard_state; /* array of booleans indexed by scancode */
uint32_t mouse_state; /* SDL mouse button bitmask */
t_vec2 mouse_window_position;
enum button_source last_active_source;
bool is_anything_just_pressed;
};
void input_state_init(struct input_state *input, SDL_Renderer *renderer);
void input_state_deinit(struct input_state *input);
void input_state_update(struct input_state *input);
void input_bind_action_scancode(struct input_state *input,
char *action_name,
SDL_Scancode scancode);
void input_unbind_action_scancode(struct input_state *input,
char *action_name,
SDL_Scancode scancode);
void input_bind_action_mouse(struct input_state *input,
char *action_name,
uint8_t mouse_button);
void input_unbind_action_mouse(struct input_state *input,
char *action_name,
uint8_t mouse_button);
void input_add_action(struct input_state *input, char *action_name);
void input_delete_action(struct input_state *input, char *action_name);
bool input_is_action_pressed(struct input_state *input, char *action_name);
bool input_is_action_just_pressed(struct input_state *input, char *action_name);
bool input_is_action_just_released(struct input_state *input, char *action_name);
t_fvec2 input_get_action_position(struct input_state *input, char *action_name);
#endif

306
src/main.c Normal file
View File

@ -0,0 +1,306 @@
#include "context.h"
#include "rendering.h"
#include "input.h"
#include "util.h"
#include "textures.h"
#include "game/game.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <physfs.h>
#include <stb_ds.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <inttypes.h>
#include <tgmath.h>
#include <limits.h>
#include <math.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;
/* 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) {
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 */
poll_events();
input_state_update(&ctx.input);
/* do we _always_ have to be dry? */
if (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
while (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
for (size_t i = 0; i < ctx.update_multiplicity; ++i) {
render_queue_clear();
game_tick();
ctx.frame_accumulator -= ctx.desired_frametime;
ctx.tick_count = (ctx.tick_count % ULLONG_MAX) + 1;
}
}
} else {
render_queue_clear();
}
/* the "step" will always run as long as there is time to spare. */
/* without interpolation, this is preferable to fixed ticks if the additional */
/* delta time calculations (like in physics code) are acceptable */
game_step(delta_time);
ctx.step_count = (ctx.step_count % ULLONG_MAX) + 1;
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);
/* 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;
ctx.step_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;
}

34
src/private/rendering.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef PRIVATE_RENDERING_H
#define PRIVATE_RENDERING_H
#include "../rendering.h"
#include "../util.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
struct sprite_primitive {
t_frect rect;
t_color color;
double rotation;
char *path;
SDL_BlendMode blend_mode;
int atlas_index;
int layer;
bool flip_x;
bool flip_y;
};
struct rect_primitive {
t_frect rect;
t_color color;
};
struct circle_primitive {
float radius;
t_color color;
t_fvec2 position;
};
#endif

367
src/rendering.c Normal file
View File

@ -0,0 +1,367 @@
#include "private/rendering.h"
#include "context.h"
#include "config.h"
#include "textures.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <stdlib.h>
#include <tgmath.h>
void render_queue_clear(void) {
/* since i don't intend to free the queues, */
/* it's faster and simpler to just "start over" */
/* and start overwriting the existing data */
arrsetlen(ctx.render_queue_sprites, 0);
arrsetlen(ctx.render_queue_rectangles, 0);
arrsetlen(ctx.render_queue_circles, 0);
}
/*
* an implementation note:
* try to avoid doing expensive work in the push functions,
* because they will be called multiple times in the main loop
* before anything is really rendered
*/
/* sprite */
void push_sprite(char *path, t_frect rect) {
textures_load(&ctx.texture_cache, path);
struct sprite_primitive sprite = {
.rect = rect,
.color = (t_color) { 255, 255, 255, 255 },
.path = path,
.rotation = 0.0,
.blend_mode = SDL_BLENDMODE_BLEND,
.atlas_index =
textures_get_atlas_index(&ctx.texture_cache, path),
.layer = 0,
.flip_x = false,
.flip_y = false,
};
arrput(ctx.render_queue_sprites, sprite);
}
void push_sprite_ex(t_frect rect, t_push_sprite_args args) {
textures_load(&ctx.texture_cache, args.path);
struct sprite_primitive sprite = {
.rect = rect,
.color = args.color,
.path = args.path,
.rotation = args.rotation,
.blend_mode = args.blend_mode,
.atlas_index =
textures_get_atlas_index(&ctx.texture_cache, args.path),
.layer = args.layer,
.flip_x = args.flip_x,
.flip_y = args.flip_y,
};
arrput(ctx.render_queue_sprites, sprite);
}
/* rectangle */
void push_rectangle(t_frect rect, t_color color) {
struct rect_primitive rectangle = {
.rect = rect,
.color = color,
};
arrput(ctx.render_queue_rectangles, rectangle);
}
/* circle */
void push_circle(t_fvec2 position, float radius, t_color color) {
struct circle_primitive circle = {
.radius = radius,
.color = color,
.position = position,
};
arrput(ctx.render_queue_circles, circle);
}
/* rendering */
static void render_background(void) {
SDL_SetRenderDrawColor(ctx.renderer, 230, 230, 230, 255);
SDL_Rect rect = {
0,
0,
ctx.window_w,
ctx.window_h
};
SDL_RenderFillRect(ctx.renderer, &rect);
SDL_SetRenderDrawColor(ctx.renderer, 255, 255, 255, 255);
}
/* compare functions for the sort in render_sprites */
static int cmp_atlases(const void *a, const void *b) {
int index_a = ((const struct sprite_primitive *)a)->atlas_index;
int index_b = ((const struct sprite_primitive *)b)->atlas_index;
return (index_a > index_b) - (index_a < index_b);
}
static int cmp_blend_modes(const void *a, const void *b) {
SDL_BlendMode mode_a = ((const struct sprite_primitive *)a)->blend_mode;
SDL_BlendMode mode_b = ((const struct sprite_primitive *)b)->blend_mode;
return (mode_a > mode_b) - (mode_a < mode_b);
}
static int cmp_colors(const void *a, const void *b) {
t_color color_a = ((const struct sprite_primitive *)a)->color;
t_color color_b = ((const struct sprite_primitive *)b)->color;
/* check reds */
if (color_a.r < color_b.r)
return -1;
else if (color_a.r > color_b.r)
return 1;
/* reds were equal, check greens */
else if (color_a.g < color_b.g)
return -1;
else if (color_a.g > color_b.g)
return 1;
/* greens were equal, check blues */
else if (color_a.b < color_b.b)
return -1;
else if (color_a.b > color_b.b)
return 1;
/* blues were equal, check alphas */
else if (color_a.a < color_b.a)
return -1;
else if (color_a.a > color_b.a)
return 1;
/* entirely equal */
else
return 0;
}
static int cmp_layers(const void *a, const void *b) {
int layer_a = ((const struct sprite_primitive *)a)->layer;
int layer_b = ((const struct sprite_primitive *)b)->layer;
return (layer_a > layer_b) - (layer_a < layer_b);
}
/* necessary to allow the renderer to draw in batches in the best case */
static void sort_sprites(struct sprite_primitive *sprites) {
size_t sprites_len = arrlenu(sprites);
qsort(sprites, sprites_len, sizeof *sprites, cmp_atlases);
qsort(sprites, sprites_len, sizeof *sprites, cmp_blend_modes);
qsort(sprites, sprites_len, sizeof *sprites, cmp_colors);
qsort(sprites, sprites_len, sizeof *sprites, cmp_layers);
}
static void render_sprite(struct sprite_primitive *sprite) {
SDL_Rect srcrect_value = { 0 };
SDL_Rect *srcrect = &srcrect_value;
SDL_Texture *texture = NULL;
/* loner */
if (sprite->atlas_index == -1) {
srcrect = NULL;
texture = textures_get_loner(&ctx.texture_cache, sprite->path);
} else {
*srcrect = textures_get_srcrect(&ctx.texture_cache, sprite->path);
texture = textures_get_atlas(&ctx.texture_cache, sprite->atlas_index);
}
SDL_RendererFlip flip = SDL_FLIP_NONE;
if (sprite->flip_x)
flip |= SDL_FLIP_HORIZONTAL;
if (sprite->flip_y)
flip |= SDL_FLIP_VERTICAL;
SDL_SetTextureColorMod(texture,
sprite->color.r,
sprite->color.g,
sprite->color.b);
SDL_SetTextureAlphaMod(texture, sprite->color.a);
SDL_SetTextureBlendMode(texture, sprite->blend_mode);
SDL_Rect rect = {
(int)sprite->rect.x,
(int)sprite->rect.y,
(int)sprite->rect.w,
(int)sprite->rect.h
};
SDL_RenderCopyEx(ctx.renderer,
texture,
srcrect,
&rect,
sprite->rotation,
NULL,
flip);
}
static void render_rectangle(struct rect_primitive *rectangle) {
SDL_SetRenderDrawColor(ctx.renderer,
rectangle->color.r,
rectangle->color.g,
rectangle->color.b,
rectangle->color.a);
SDL_Rect rect = {
(int)rectangle->rect.x,
(int)rectangle->rect.y,
(int)rectangle->rect.w,
(int)rectangle->rect.h
};
SDL_RenderFillRect(ctx.renderer, &rect);
SDL_SetRenderDrawColor(ctx.renderer, 255, 255, 255, 255);
}
/* vertices_out and indices_out MUST BE FREED */
static void create_circle_geometry(t_fvec2 position,
t_color color,
float radius,
size_t num_vertices,
SDL_Vertex **vertices_out,
int **indices_out)
{
SDL_Vertex *vertices = cmalloc(sizeof *vertices * (num_vertices + 1));
int *indices = cmalloc(sizeof *indices * (num_vertices * 3));
/* the angle (in radians) to rotate by on each iteration */
float seg_rotation_angle = (360.0f / (float)num_vertices) * ((float)M_PI / 180);
vertices[0].position.x = (float)position.x;
vertices[0].position.y = (float)position.y;
vertices[0].color.r = color.r;
vertices[0].color.g = color.g;
vertices[0].color.b = color.b;
vertices[0].color.a = color.a;
vertices[0].tex_coord = (SDL_FPoint){ 0, 0 };
/* this point will rotate around the center */
float start_x = 0.0f - radius;
float start_y = 0.0f;
for (size_t i = 1; i < num_vertices + 1; ++i) {
float final_seg_rotation_angle = (float)i * seg_rotation_angle;
vertices[i].position.x =
cos(final_seg_rotation_angle) * start_x -
sin(final_seg_rotation_angle) * start_y;
vertices[i].position.y =
cos(final_seg_rotation_angle) * start_y +
sin(final_seg_rotation_angle) * start_x;
vertices[i].position.x += position.x;
vertices[i].position.y += position.y;
vertices[i].color.r = color.r;
vertices[i].color.g = color.g;
vertices[i].color.b = color.b;
vertices[i].color.a = color.a;
vertices[i].tex_coord = (SDL_FPoint){ 0, 0 };
size_t triangle_offset = 3 * (i - 1);
/* center point index */
indices[triangle_offset] = 0;
/* generated point index */
indices[triangle_offset + 1] = (int)i;
size_t index = (i + 1) % num_vertices;
if (index == 0)
index = num_vertices;
indices[triangle_offset + 2] = (int)index;
}
*vertices_out = vertices;
*indices_out = indices;
}
static void render_circle(struct circle_primitive *circle) {
SDL_Vertex *vertices = NULL;
int *indices = NULL;
int num_vertices = (int)circle->radius;
create_circle_geometry(circle->position,
circle->color,
circle->radius,
num_vertices,
&vertices,
&indices);
SDL_RenderGeometry(ctx.renderer, NULL,
vertices,
num_vertices + 1, /* vertices + center vertex */
indices,
num_vertices * 3);
free(vertices);
free(indices);
}
static void render_sprites(void) {
if (ctx.texture_cache.is_dirty)
textures_update_current_atlas(&ctx.texture_cache);
sort_sprites(ctx.render_queue_sprites);
for (size_t i = 0; i < arrlenu(ctx.render_queue_sprites); ++i) {
render_sprite(&ctx.render_queue_sprites[i]);
}
}
static void render_rectangles(void) {
for (size_t i = 0; i < arrlenu(ctx.render_queue_rectangles); ++i) {
render_rectangle(&ctx.render_queue_rectangles[i]);
}
}
static void render_circles(void) {
for (size_t i = 0; i < arrlenu(ctx.render_queue_circles); ++i) {
render_circle(&ctx.render_queue_circles[i]);
}
}
void render(void) {
SDL_SetRenderDrawBlendMode(ctx.renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(ctx.renderer, 0, 0, 0, 255);
SDL_RenderClear(ctx.renderer);
SDL_SetRenderDrawColor(ctx.renderer, 255, 255, 255, 255);
render_background();
render_sprites();
render_rectangles();
render_circles();
SDL_RenderPresent(ctx.renderer);
}

41
src/rendering.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef RENDERING_H
#define RENDERING_H
#include "util.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
typedef struct push_sprite_args {
char *path;
int layer;
t_color color;
double rotation;
SDL_BlendMode blend_mode;
bool flip_x;
bool flip_y;
} t_push_sprite_args;
/* clears all render queues */
void render_queue_clear(void);
/* pushes a sprite onto the sprite render queue */
/* this is a simplified version of push_sprite_ex for the most common case. */
/* it assumes you want no color modulation, no rotation, no flip, layer 0, and alpha blending */
void push_sprite(char *path, t_frect rect);
/* pushes a sprite onto the sprite render queue */
/* note that if args is zeroed out, the color will be transparent black */
void push_sprite_ex(t_frect rect, t_push_sprite_args args);
/* pushes a filled rectangle onto the rectangle render queue */
void push_rectangle(t_frect rect, t_color color);
/* pushes a filled circle onto the circle render queue */
void push_circle(t_fvec2 position, float radius, t_color color);
/* renders the background, then the primitives in all render queues */
void render(void);
#endif

139
src/scripting.c Normal file
View File

@ -0,0 +1,139 @@
#include "scripting.h"
#include "util.h"
#include "context.h"
#include <SDL2/SDL.h>
#include <umka_api.h>
#include <physfs.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
static void msgbox(UmkaStackSlot *params, UmkaStackSlot *result) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION,
params[1].ptrVal, params[0].ptrVal, NULL);
}
static void umka_log_info(UmkaStackSlot *params, UmkaStackSlot *result) {
log_info(params[0].ptrVal);
}
static void umka_log_critical(UmkaStackSlot *params, UmkaStackSlot *result) {
log_critical(params[0].ptrVal);
}
static void umka_log_warn(UmkaStackSlot *params, UmkaStackSlot *result) {
log_warn(params[0].ptrVal);
}
static void is_action_pressed(UmkaStackSlot *params, UmkaStackSlot *result) {
struct state *state = params[1].ptrVal;
t_ctx *ctx = state->hidden_ptr;
bool value = input_is_action_pressed(&ctx->input, params[0].ptrVal);
result->uintVal = value;
}
static void is_action_just_pressed(UmkaStackSlot *params, UmkaStackSlot *result) {
struct state *state = params[1].ptrVal;
t_ctx *ctx = state->hidden_ptr;
bool value = input_is_action_just_pressed(&ctx->input, params[0].ptrVal);
result->uintVal = value;
}
static void is_action_just_released(UmkaStackSlot *params, UmkaStackSlot *result) {
struct state *state = params[1].ptrVal;
t_ctx *ctx = state->hidden_ptr;
bool value = input_is_action_just_released(&ctx->input, params[0].ptrVal);
result->uintVal = value;
}
static void get_action_position(UmkaStackSlot *params, UmkaStackSlot *result) {
struct state *state = params[2].ptrVal;
t_ctx *ctx = state->hidden_ptr;
t_fvec2 *position = params[0].ptrVal;
*position = input_get_action_position(&ctx->input, params[1].ptrVal);
// the result is in a hidden result pointer allocated by Umka
result->ptrVal = params[0].ptrVal;
}
static void register_api(void *umka) {
umkaAddFunc(umka, "msgbox", msgbox);
umkaAddFunc(umka, "logInfo", umka_log_info);
umkaAddFunc(umka, "logCritical", umka_log_critical);
umkaAddFunc(umka, "logWarn", umka_log_warn);
umkaAddFunc(umka, "cImplIsActionPressed", is_action_pressed);
umkaAddFunc(umka, "cImplIsActionJustPressed", is_action_just_pressed);
umkaAddFunc(umka, "cImplIsActionJustReleased", is_action_just_released);
umkaAddFunc(umka, "getActionPosition", get_action_position);
}
bool scripting_init(t_ctx *ctx) {
if (!PHYSFS_exists("/scripts/main.um")) {
CRY("Failed to initialize scripting", "Could not find a main.um (we need it)");
return false;
}
ctx->umka = umkaAlloc();
char *main_script = file_to_str("/scripts/main.um");
bool umka_ok = umkaInit(ctx->umka,
"main.um",
main_script,
UMKA_STACK_SIZE,
NULL,
ctx->argc,
ctx->argv,
false,
false,
NULL);
free(main_script);
if (!umka_ok) {
CRY("Failed to initialize scripting", "Unknown Umka error");
return false;
}
/* all Umka files are compiled even if they're never used */
char **dir_file_names = PHYSFS_enumerateFiles("/scripts");
for (char **i = dir_file_names; *i != NULL; ++i) {
char *file_name = *i;
if (!strends(file_name, ".um"))
continue;
/* got this one already */
if (strcmp(file_name, "main.um") == 0)
continue;
/* need to figure out the actual path (as opposed to the lone file name) */
const char *path_prefix = "/scripts/";
size_t path_size = snprintf(NULL, 0, "%s%s", path_prefix, file_name) + 1;
char *path = cmalloc(path_size);
snprintf(path, path_size, "%s%s", path_prefix, file_name);
char *contents = file_to_str(path);
umkaAddModule(ctx->umka, file_name, contents);
free(path);
free(contents);
}
PHYSFS_freeList(dir_file_names);
register_api(ctx->umka);
if (!umkaCompile(ctx->umka)) {
cry_umka(ctx->umka);
return false;
}
return true;
}
void scripting_deinit(t_ctx *ctx) {
umkaFree(ctx->umka);
}

18
src/scripting.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef SCRIPTING_H
#define SCRIPTING_H
#include <umka_api.h>
#include <stdint.h>
#include <stdbool.h>
typedef struct context t_ctx;
struct state {
t_ctx *hidden_ptr;
uint64_t tick_count;
};
bool scripting_init(void);
#endif

5
src/text.c Normal file
View File

@ -0,0 +1,5 @@
#include "text.h"
#include "SDL_ttf.h"

29
src/text.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef TEXT_H
#define TEXT_H
#include "util.h"
#include "SDL.h"
#include "SDL_ttf.h"
struct text {
char *text;
t_color color;
int ptsize;
};
struct text_cache_item {
char *key;
struct text *value;
};
struct text_cache {
struct text_cache_item *hash;
};
#endif

357
src/textures.c Normal file
View File

@ -0,0 +1,357 @@
#include "textures.h"
#include "config.h"
#include "util.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <physfs.h>
#include <physfsrwops.h>
#include <stb_ds.h>
#include <stb_rect_pack.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
static SDL_Surface *image_to_surface(char *path) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL)
goto fail;
SDL_Surface *result = IMG_Load_RW(handle, true);
if (result == NULL)
goto fail;
SDL_SetSurfaceBlendMode(result, SDL_BLENDMODE_NONE);
SDL_SetSurfaceRLE(result, true);
return result;
fail:
CRY(path, "Failed to load image. Aborting...");
die_abruptly();
}
/* adds a new, blank atlas surface to the cache */
static void add_new_atlas(struct texture_cache *cache) {
SDL_PixelFormat *native_format =
SDL_AllocFormat(SDL_GetWindowPixelFormat(cache->window));
/* the window format won't have an alpha channel, so we figure this out */
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
uint32_t a_mask = 0x000000FF;
#else
uint32_t a_mask = 0xFF000000;
#endif
SDL_Surface *new_atlas = SDL_CreateRGBSurface(0,
TEXTURE_ATLAS_SIZE,
TEXTURE_ATLAS_SIZE,
TEXTURE_ATLAS_BIT_DEPTH,
native_format->Rmask,
native_format->Gmask,
native_format->Bmask,
a_mask);
SDL_FreeFormat(native_format);
SDL_SetSurfaceRLE(new_atlas, true);
arrput(cache->atlas_surfaces, new_atlas);
SDL_Texture *new_atlas_texture =
SDL_CreateTextureFromSurface(cache->renderer, new_atlas);
arrput(cache->atlas_textures, new_atlas_texture);
}
static void recreate_current_atlas_texture(struct texture_cache *cache) {
/* TODO: figure out if SDL_UpdateTexture alone is faster than blitting */
SDL_Surface *atlas_surface = cache->atlas_surfaces[cache->atlas_index];
/* clear */
SDL_FillRect(atlas_surface, NULL, 0);
/* blit the texture surfaces onto the atlas */
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
if (cache->hash[i].value.atlas_index != cache->atlas_index)
continue;
SDL_BlitSurface(cache->hash[i].value.data,
NULL,
atlas_surface,
&cache->hash[i].value.srcrect);
}
/* texturize it! */
SDL_LockSurface(atlas_surface);
SDL_UpdateTexture(cache->atlas_textures[cache->atlas_index],
NULL,
atlas_surface->pixels,
atlas_surface->pitch);
SDL_UnlockSurface(atlas_surface);
}
/* uses the textures currently in the cache to create an array of stbrp_rects */
static stbrp_rect *create_rects_from_cache(struct texture_cache *cache) {
stbrp_rect *rects = NULL;
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
SDL_Surface *surface_data = cache->hash[i].value.data;
stbrp_rect new_rect = {
.w = surface_data->w,
.h = surface_data->h,
};
arrput(rects, new_rect);
}
return rects;
}
/* returns an array which contains a _copy_ of every unpacked rect in rects. */
/* each of these copies will have their original index in rects saved in */
/* their `id` field, which is an int. */
static stbrp_rect *filter_unpacked_rects(stbrp_rect *rects) {
stbrp_rect *unpacked_rects = NULL;
for (size_t i = 0; i < arrlenu(rects); ++i) {
/* already packed */
if (rects[i].was_packed)
continue;
arrput(unpacked_rects, rects[i]);
/* stb_rect_pack mercifully gives you a free userdata int */
/* the index is saved there so the original array can be updated later */
unpacked_rects[arrlenu(unpacked_rects)-1].id = (int)i;
}
return unpacked_rects;
}
/* updates the original rects array with the data from packed_rects */
/* returns true if all rects were packed successfully */
static bool update_rects(struct texture_cache *cache, stbrp_rect *rects, stbrp_rect *packed_rects) {
/* !!! do not grow either of the arrays !!! */
/* the reallocation will try to reassign the array pointer, to no effect. */
/* see stb_ds.h */
bool packed_all = true;
for (size_t i = 0; i < arrlenu(packed_rects); ++i) {
/* we can check if any rects failed to be packed right here */
/* it's not ideal, but it avoids another iteration */
if (!packed_rects[i].was_packed) {
packed_all = false;
continue;
}
rects[packed_rects[i].id] = packed_rects[i];
/* while the order of the elements in the hash map is unknown to us, */
/* their equivalents in `rects` are in that same (unknown) order, which means */
/* we can use the index we had saved to find the original texture struct */
cache->hash[packed_rects[i].id].value.atlas_index = cache->atlas_index;
}
return packed_all;
}
/* updates the atlas location of every rect in the cache */
static void update_texture_rects_in_atlas(struct texture_cache *cache, stbrp_rect *rects) {
for (size_t i = 0; i < arrlenu(rects); ++i) {
cache->hash[i].value.srcrect = (SDL_Rect) {
.x = rects[i].x,
.y = rects[i].y,
.w = rects[i].w,
.h = rects[i].h,
};
}
}
void textures_cache_init(struct texture_cache *cache, SDL_Window *window) {
cache->window = window;
cache->renderer = SDL_GetRenderer(window);
sh_new_arena(cache->hash);
sh_new_arena(cache->loner_hash);
cache->node_buffer = cmalloc(sizeof *cache->node_buffer * TEXTURE_ATLAS_SIZE);
add_new_atlas(cache);
recreate_current_atlas_texture(cache);
}
void textures_cache_deinit(struct texture_cache *cache) {
/* free atlas textures */
for (size_t i = 0; i < arrlenu(cache->atlas_textures); ++i) {
SDL_DestroyTexture(cache->atlas_textures[i]);
}
arrfree(cache->atlas_textures);
/* free atlas surfaces */
for (size_t i = 0; i < arrlenu(cache->atlas_surfaces); ++i) {
SDL_FreeSurface(cache->atlas_surfaces[i]);
}
arrfree(cache->atlas_surfaces);
/* free cache hashes */
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
SDL_FreeSurface(cache->hash[i].value.data);
}
shfree(cache->hash);
for (size_t i = 0; i < shlenu(cache->loner_hash); ++i) {
SDL_FreeSurface(cache->loner_hash[i].value.data);
}
shfree(cache->loner_hash);
free(cache->node_buffer);
}
void textures_dump_atlases(struct texture_cache *cache) {
PHYSFS_mkdir("/dump");
const char string_template[] = "/dump/atlas%zd.png";
char buf[2048]; /* larger than will ever be necessary */
size_t i = 0;
for (; i < arrlenu(cache->atlas_surfaces); ++i) {
snprintf(buf, sizeof buf, string_template, i);
SDL_RWops *handle = PHYSFSRWOPS_openWrite(buf);
if (handle == NULL) {
CRY("Texture atlas dump failed.", "File could not be opened");
return;
}
IMG_SavePNG_RW(cache->atlas_surfaces[i], handle, true);
log_info("Dumped atlas %s", buf);
}
size_t num_loners = shlenu(cache->loner_hash);
log_info("%zd atlases dumped. %zd loners left undumped.", i, num_loners);
}
void textures_load(struct texture_cache *cache, char *path) {
/* no need to do anything if it was loaded already */
if (shgeti(cache->hash, path) >= 0 || shgeti(cache->loner_hash, path) >= 0)
return;
SDL_Surface *surface = image_to_surface(path);
struct texture new_texture;
new_texture.data = surface;
/* it's a "loner texture," it doesn't fit in an atlas so it's not in one */
if (surface->w > TEXTURE_ATLAS_SIZE || surface->h > TEXTURE_ATLAS_SIZE) {
new_texture.loner_data = SDL_CreateTextureFromSurface(cache->renderer, surface);
new_texture.atlas_index = -1;
shput(cache->loner_hash, path, new_texture);
} else {
new_texture.atlas_index = cache->atlas_index;
shput(cache->hash, path, new_texture);
cache->is_dirty = true;
}
}
void textures_update_current_atlas(struct texture_cache *cache) {
/* this function makes a lot more sense if you read stb_rect_pack.h */
stbrp_context pack_ctx; /* target info */
stbrp_init_target(&pack_ctx,
TEXTURE_ATLAS_SIZE,
TEXTURE_ATLAS_SIZE,
cache->node_buffer,
TEXTURE_ATLAS_SIZE);
stbrp_rect *rects = create_rects_from_cache(cache);
/* we have to keep packing, and creating atlases if necessary, */
/* until all rects have been packed. */
/* ideally, this will not iterate more than once. */
bool textures_remaining = true;
while (textures_remaining) {
stbrp_rect *rects_to_pack = filter_unpacked_rects(rects);
stbrp_pack_rects(&pack_ctx, rects_to_pack, (int)arrlen(rects_to_pack));
textures_remaining = !update_rects(cache, rects, rects_to_pack);
arrfree(rects_to_pack); /* got what we needed */
/* some textures couldn't be packed */
if (textures_remaining) {
update_texture_rects_in_atlas(cache, rects);
recreate_current_atlas_texture(cache);
/* need a new atlas for next time */
add_new_atlas(cache);
++cache->atlas_index;
}
};
update_texture_rects_in_atlas(cache, rects);
recreate_current_atlas_texture(cache);
cache->is_dirty = false;
arrfree(rects);
}
SDL_Rect textures_get_srcrect(struct texture_cache *cache, char *path) {
struct texture_cache_item *texture = shgetp_null(cache->hash, path);
if (texture == NULL) {
CRY("Texture lookup failed.",
"Tried to get texture that isn't loaded.");
return (SDL_Rect){ 0, 0, 0, 0 };
}
return texture->value.srcrect;
}
int textures_get_atlas_index(struct texture_cache *cache, char *path) {
struct texture_cache_item *texture = shgetp_null(cache->hash, path);
/* it might be a loner texture */
if (texture == NULL) {
texture = shgetp_null(cache->loner_hash, path);
/* never mind it's just not there at all */
if (texture == NULL) {
CRY("Texture atlas index lookup failed.",
"Tried to get atlas index of texture that isn't loaded.");
return INT_MIN;
}
}
return texture->value.atlas_index;
}
SDL_Texture *textures_get_atlas(struct texture_cache *cache, int index) {
/* out of bounds */
if (arrlen(cache->atlas_textures) < index + 1 || index < 0)
return NULL;
return cache->atlas_textures[index];
}
SDL_Texture *textures_get_loner(struct texture_cache *cache, char *path) {
struct texture_cache_item *texture = shgetp_null(cache->loner_hash, path);
if (texture == NULL) {
CRY("Loner texture lookup failed.",
"Tried to get texture that isn't loaded.");
return NULL;
}
return texture->value.loner_data;
}
size_t textures_get_num_atlases(struct texture_cache *cache) {
return cache->atlas_index + 1;
}

78
src/textures.h Normal file
View File

@ -0,0 +1,78 @@
#ifndef TEXTURES_H
#define TEXTURES_H
#include <SDL2/SDL.h>
#include <stb_rect_pack.h>
#include <stdbool.h>
struct texture {
SDL_Rect srcrect; /* position in atlas */
SDL_Surface *data; /* original image data */
SDL_Texture *loner_data; /* loner textures store their data directly */
int atlas_index; /* which atlas the texture is in */
int8_t layer;
};
struct texture_cache_item {
char *key;
struct texture value;
};
/* use the public API to create and manipulate instances of this structure */
struct texture_cache {
/* from context */
SDL_Window *window;
SDL_Renderer *renderer;
struct texture_cache_item *hash;
struct texture_cache_item *loner_hash;
stbrp_node *node_buffer; /* used internally by stb_rect_pack */
SDL_Surface **atlas_surfaces;
SDL_Texture **atlas_textures;
int atlas_index;
bool is_dirty; /* current atlas needs to be recreated */
};
void textures_cache_init(struct texture_cache *cache, SDL_Window *window);
void textures_cache_deinit(struct texture_cache *cache);
/* for debugging */
void textures_dump_atlases(struct texture_cache *cache);
/* loads an image if it isn't in the cache, otherwise a no-op. */
/* can be called from anywhere at any time after init, useful if you want to */
/* preload textures you know will definitely be used */
void textures_load(struct texture_cache *cache, char *path);
/* repacks the current texture atlas based on the texture cache */
void textures_update_current_atlas(struct texture_cache *cache);
/* returns a rect in a texture cache atlas based on a path, for drawing */
/* if the texture is not found, returns a zero-filled rect (so check w or h) */
SDL_Rect textures_get_srcrect(struct texture_cache *cache, char *path);
/* returns which atlas the texture in the path is in, starting from 0 */
/* if the texture is not found, returns INT_MIN */
int textures_get_atlas_index(struct texture_cache *cache, char *path);
/* returns a pointer to the atlas at `index` */
/* if the index is out of bounds, returns NULL. */
/* you can get the index via texture_get_atlas_index */
SDL_Texture *textures_get_atlas(struct texture_cache *cache, int index);
SDL_Texture *textures_get_loner(struct texture_cache *cache, char *path);
/* returns the number of atlases in the cache */
size_t textures_get_num_atlases(struct texture_cache *cache);
#endif

191
src/util.c Normal file
View File

@ -0,0 +1,191 @@
#include "util.h"
#include <SDL2/SDL.h>
#include <physfsrwops.h>
#define STB_DS_IMPLEMENTATION
#define STBDS_ASSERT SDL_assert
#define STBDS_REALLOC(c,p,s) crealloc(p, s)
#define STBDS_FREE(c,p) free(p)
#include <stb_ds.h>
#define STB_RECT_PACK_IMPLEMENTATION
#define STBRP_ASSERT SDL_assert
#include <stb_rect_pack.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdnoreturn.h>
#include <limits.h>
#include <string.h>
void cry_impl(const char *file, const int line, const char *title, const char *text) {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"TEARS AT %s:%d: %s ... %s", file, line, title, text);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, NULL);
}
static void log_impl(const char *restrict format, va_list args, SDL_LogPriority priority) {
SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION,
priority,
format,
args);
}
void log_info(const char *restrict format, ...) {
va_list args;
va_start(args, format);
log_impl(format, args, SDL_LOG_PRIORITY_INFO);
va_end(args);
}
void log_critical(const char *restrict format, ...) {
va_list args;
va_start(args, format);
log_impl(format, args, SDL_LOG_PRIORITY_CRITICAL);
va_end(args);
}
void log_warn(const char *restrict format, ...) {
va_list args;
va_start(args, format);
log_impl(format, args, SDL_LOG_PRIORITY_WARN);
va_end(args);
}
noreturn static void alloc_failure_death(void) {
log_critical("Allocation failure. Aborting NOW.");
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
"MEMORY ALLOCATION FAILURE.",
"FAILED TO ALLOCATE MEMORY. "
"YOU MIGHT BE UNLUCKY. "
"THE GAME WILL EXIT NOW.",
NULL);
die_abruptly();
}
noreturn void die_abruptly(void) {
/* a zombie window will linger if we don't at least try to quit SDL */
SDL_Quit();
abort();
}
void *cmalloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL)
alloc_failure_death();
return ptr;
}
void *crealloc(void *ptr, size_t size) {
void *out = realloc(ptr, size);
if (out == NULL)
alloc_failure_death();
return out;
}
void *ccalloc(size_t num, size_t size) {
void *ptr = calloc(num, size);
if (ptr == NULL)
alloc_failure_death();
return ptr;
}
int64_t file_to_bytes(char *path, unsigned char **buf_out) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL) {
return -1;
}
int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */
*buf_out = cmalloc(data_size);
SDL_RWread(handle, *buf_out, sizeof **buf_out, data_size / sizeof **buf_out);
SDL_RWclose(handle); /* we got all we needed from the stream */
return data_size;
}
char *file_to_str(char *path) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL) {
return NULL;
}
int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */
char *str_out = cmalloc(data_size + 1); /* data plus final null */
size_t len = data_size / sizeof *str_out;
SDL_RWread(handle, str_out, sizeof *str_out, len);
SDL_RWclose(handle); /* we got all we needed from the stream */
str_out[len] = '\0';
return str_out;
}
bool strends(const char *str, const char *suffix) {
size_t str_length = strlen(str);
size_t suffix_length = strlen(suffix);
if (suffix_length > str_length)
return false;
return memcmp((str + str_length) - suffix_length, suffix, suffix_length) == 0;
}
bool intersect_rect(const t_rect *a, const t_rect *b, t_rect *result) {
SDL_Rect a_sdl = { a->x, a->y, a->w, a->h };
SDL_Rect b_sdl = { b->x, b->y, b->w, b->h };
SDL_Rect result_sdl = { 0 };
bool intersection = SDL_IntersectRect(&a_sdl, &b_sdl, &result_sdl);
*result = (t_rect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
return intersection;
}
bool intersect_frect(const t_frect *a, const t_frect *b, t_frect *result) {
SDL_FRect a_sdl = { a->x, a->y, a->w, a->h };
SDL_FRect b_sdl = { b->x, b->y, b->w, b->h };
SDL_FRect result_sdl = { 0 };
bool intersection = SDL_IntersectFRect(&a_sdl, &b_sdl, &result_sdl);
*result = (t_frect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
return intersection;
}
t_frect to_frect(t_rect rect) {
return (t_frect) {
.h = (float)rect.h,
.w = (float)rect.w,
.x = (float)rect.x,
.y = (float)rect.y,
};
}
void tick_timer(int *value) {
*value = MAX(*value - 1, 0);
}

120
src/util.h Normal file
View File

@ -0,0 +1,120 @@
#ifndef UTIL_H
#define UTIL_H
#include <SDL2/SDL.h>
#include <physfs.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#include <stdnoreturn.h>
/* */
/* GENERAL UTILITIES */
/* */
void cry_impl(const char *file, const int line, const char *title, const char *text);
#define CRY(title, text) cry_impl(__FILE__, __LINE__, title, text)
#define CRY_SDL(title) cry_impl(__FILE__, __LINE__, title, SDL_GetError())
#define CRY_PHYSFS(title) \
cry_impl(__FILE__, __LINE__, title, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()))
void log_info(const char *restrict format, ...);
void log_critical(const char *restrict format, ...);
void log_warn(const char *restrict format, ...);
/* for when there's absolutely no way to continue */
noreturn void die_abruptly(void);
/* "critical" allocation functions which will log and abort() on failure. */
/* if it is reasonable to handle this gracefully, use the standard versions. */
/* the stb implementations will be configured to use these */
void *cmalloc(size_t size);
void *crealloc(void *ptr, size_t size);
void *ccalloc(size_t num, size_t size);
/* DON'T FORGET ABOUT DOUBLE EVALUATION */
/* YOU THINK YOU WON'T, AND THEN YOU DO */
/* C23's typeof could fix it, so i will */
/* leave them as macros to be replaced. */
/* use the macro in tgmath.h for floats */
#define MAX SDL_max
#define MIN SDL_min
/* sets buf_out to a pointer to a byte buffer which must be freed. */
/* returns the size of this buffer. */
int64_t file_to_bytes(char *path, unsigned char **buf_out);
/* returns a pointer to a string which must be freed */
char *file_to_str(char *path);
/* returns true if str ends with suffix */
bool strends(const char *str, const char *suffix);
/* */
/* GAME LOGIC UTILITIES */
/* */
/* 32-bit color data */
typedef struct color {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
} t_color;
/* a rectangle with the origin at the upper left (integer) */
typedef struct rect {
int x, y;
int w, h;
} t_rect;
bool intersect_rect(const t_rect *a, const t_rect *b, t_rect *result);
/* a rectangle with the origin at the upper left (floating point) */
typedef struct frect {
float x, y;
float w, h;
} t_frect;
bool intersect_frect(const t_frect *a, const t_frect *b, t_frect *result);
t_frect to_frect(t_rect rect);
/* a point in some space (integer) */
typedef struct vec2 {
int x, y;
} t_vec2;
/* a point in some space (floating point) */
typedef struct fvec2 {
float x, y;
} t_fvec2;
/* decrements an lvalue (which should be an int), stopping at 0 */
/* meant for tick-based timers in game logic */
/*
* example:
* tick_timer(&player->jump_air_timer);
*/
void tick_timer(int *value);
#endif