Jelajahi Sumber

First commit

Aaro Saila 1 bulan lalu
melakukan
68f4f9a532

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+build

TEMPAT SAMPAH
assets/LiberationSans-Regular.ttf


+ 55 - 0
meson.build

@@ -0,0 +1,55 @@
+project(
+  'scufpong',
+  'c',
+  default_options: [
+    'force_fallback_for=scufeng'
+  ]
+)
+
+c_args = [
+  '-std=c23',
+  '-Wall',
+  '-Wextra',
+  '-pedantic'
+]
+
+scufeng_dep = dependency(
+  'scufeng', 
+  allow_fallback: true
+)
+
+game = executable(
+  'game',
+  'src/common/game.c',
+  'src/game/MainMenu.c',
+  'src/game/MultiPlayer.c',
+  'src/game/SinglePlayer.c',
+  'src/game/pong.c',
+  'src/game/screen.c',
+  include_directories: include_directories(
+    'src/common/'
+  ),
+  c_args: [
+    c_args,
+    '-DFONT_PATH="../assets/LiberationSans-Regular.ttf"',
+    '-D_DEFAULT_SOURCE' # for inet_aton
+  ],
+  link_args: [
+    '-lm'
+  ],
+  dependencies: [
+    scufeng_dep
+  ]
+)
+
+server = executable(
+  'server',
+  'src/server/server.c',
+  c_args: [
+    c_args,
+    '-D_DEFAULT_SOURCE' # for inet_aton
+  ],
+  dependencies: [
+    scufeng_dep
+  ]
+)

+ 271 - 0
src/common/game.c

@@ -0,0 +1,271 @@
+#include "cglm/call/vec2.h"
+
+#include "game.h"
+#include "scufeng/engine.h"
+#include "scufeng/util.h"
+
+typedef enum {
+    BCR_PADDLE_L_GOAL,
+    BCR_PADDLE_R_GOAL,
+} BALL_COLLISION_RESULT;
+
+GameState GameState_new(
+    const float paddles_w,
+    const float paddles_h,
+    const float paddle_l_x,
+    const float paddle_speed,
+    const float ball_w,
+    const vec2 ball_pos,
+    const float ball_speed
+) {
+
+#define BALL_START_DIR { -1.0f, 0.0f }
+
+    // clang-format off
+    GameState state = {
+        .paddle_l = {
+            .pos = { paddle_l_x, 0.5f },
+            .w = paddles_w,
+            .h = paddles_h,
+            .dirv = GLM_VEC2_ZERO_INIT,
+            .speed = paddle_speed
+        },
+        .paddle_r = {
+            .pos = { 1.0f - paddle_l_x, 0.5f },
+            .w = paddles_w,
+            .h = paddles_h,
+            .dirv = GLM_VEC2_ZERO_INIT,
+            .speed = paddle_speed
+        },
+        .ball = {
+            // .pos = { 0.5f, 0.5f },
+            .pos = { ball_pos[0], ball_pos[1] },
+            .w = ball_w,
+            .h = ball_w,
+            .dirv = BALL_START_DIR,
+            .speed = ball_speed
+        },
+        .ball_start_pos = { ball_pos[0], ball_pos[1] },
+        .ball_start_dir = BALL_START_DIR
+    };
+    // clang-format on
+
+    return state;
+}
+
+static void paddle_collision_check(MovingObject* const paddle) {
+    if (paddle->pos[1] - paddle->h / 2.0f < 0.0f) {
+        paddle->pos[1] = paddle->h / 2.0f;
+    }
+    if (paddle->pos[1] + paddle->h / 2.0f > 1.0f) {
+        paddle->pos[1] = 1.0f - paddle->h / 2.0f;
+    }
+}
+
+static void paddle_r_move(GameState* const state) {
+    const float ball_y = state->ball.pos[1];
+
+    const float target_zone_range = state->paddle_r.h / 2.0f;
+    const float target_zone_top = state->paddle_r.pos[1] - target_zone_range;
+    const float target_zone_middle = state->paddle_r.pos[1];
+    const float target_zone_bottom = state->paddle_r.pos[1] + target_zone_range;
+
+    const bool ball_is_below = ball_y > target_zone_bottom;
+    const bool ball_is_above = ball_y < target_zone_top;
+    const bool ball_is_centered = ball_y <= target_zone_middle + target_zone_range / 2.0f
+        && ball_y >= target_zone_middle - target_zone_range / 2.0f;
+    // printf("%d%d\n", ball_is_below, ball_is_above);
+
+    if (ball_is_below) {
+        state->paddle_r.dirv[1] = 1.0f;
+    } else if (ball_is_above) {
+        state->paddle_r.dirv[1] = -1.0f;
+    } else if (ball_is_centered) {
+        state->paddle_r.dirv[1] = 0.0f;
+    }
+}
+
+static void game_next_round(GameState* const state) {
+    glmc_vec2_copy(state->ball_start_pos, state->ball.pos);
+    glmc_vec2_copy(state->ball_start_dir, state->ball.dirv);
+}
+
+static bool ball_bounds_collision_check(GameState* const state, vec2 new_pos) {
+    MovingObject* const ball = &state->ball;
+
+    const float wd2 = ball->w / 2.0f;
+    const float hd2 = ball->h / 2.0f;
+
+    if (new_pos[0] + wd2 <= 0.0f) {
+        state->score_r++;
+        game_next_round(state);
+        return true;
+    } else if (new_pos[0] - wd2 >= 1.0f) {
+        state->score_l++;
+        game_next_round(state);
+        return true;
+    }
+
+    if (new_pos[1] - hd2 < 0.0f) {
+        new_pos[1] = hd2;
+        glmc_vec2_reflect(ball->dirv, (vec2) { 0.0f, -1.0f }, ball->dirv);
+    }
+    if (new_pos[1] + hd2 > 1.0f) {
+        new_pos[1] = 1.0f - hd2;
+        glmc_vec2_reflect(ball->dirv, (vec2) { 0.0f, 1.0f }, ball->dirv);
+    }
+
+    return false;
+}
+
+static void ball_paddles_collision_check(GameState* const state, vec2 new_pos) {
+    static const float possible_y_directions[] = {
+        0.0f,
+        0.5f,
+        -0.5f
+    };
+
+    MovingObject* const ball = &state->ball;
+    const MovingObject* const paddle_l = &state->paddle_l;
+    const MovingObject* const paddle_r = &state->paddle_r;
+
+    const float ball_wd2 = ball->w / 2.0f;
+    const float ball_hd2 = ball->h / 2.0f;
+    const float paddle_wd2 = paddle_l->w / 2.0f;
+    const float paddle_hd2 = paddle_l->h / 2.0f;
+    vec2 paddle_side_top;
+    vec2 paddle_side_bottom;
+    vec2 ball_side_top;
+    vec2 ball_side_bottom;
+    float paddle_back_x;
+    vec2 ball_pos;
+    vec2 new_pos_adj;
+    paddle_side_top[0] = paddle_l->pos[0] + paddle_wd2;
+    paddle_side_top[1] = paddle_l->pos[1] - paddle_hd2;
+    paddle_side_bottom[0] = paddle_side_top[0];
+    paddle_side_bottom[1] = paddle_l->pos[1] + paddle_hd2;
+    ball_side_top[0] = ball->pos[0] - ball_wd2;
+    ball_side_top[1] = ball->pos[1] - ball_hd2;
+    ball_side_bottom[0] = ball_side_top[0];
+    ball_side_bottom[1] = ball->pos[1] + ball_hd2;
+    paddle_back_x = paddle_l->pos[0] - paddle_wd2;
+    glmc_vec2_copy(ball->pos, ball_pos);
+    glmc_vec2_copy(new_pos, new_pos_adj);
+
+    bool collision = false;
+    bool mirror_ip = false;
+    vec2 ip;
+
+    if (state->ball.dirv[0] > 0.0f) {
+        mirror_ip = true;
+        paddle_side_top[0] = 1.0f - (paddle_r->pos[0] - paddle_r->w / 2.0f);
+        paddle_side_top[1] = (paddle_r->pos[1] - paddle_r->h / 2.0f);
+        paddle_side_bottom[0] = paddle_side_top[0];
+        paddle_side_bottom[1] = paddle_side_top[1] + paddle_r->h;
+        ball_pos[0] = 1.0f - ball_pos[0];
+        ball_side_top[0] = ball_pos[0] - ball_wd2;
+        ball_side_bottom[0] = ball_side_top[0];
+        paddle_back_x = 1.0f - (paddle_r->pos[0] + paddle_r->w / 2.0f);
+        new_pos_adj[0] = 1.0f - new_pos_adj[0];
+    }
+
+    // SEResult res = se_render_line_segment_norm_coord(
+    //     paddle_side_top,
+    //     paddle_side_bottom,
+    //     (vec4) { 0.0f, 1.0f, 0.0f, 1.0f }
+    // );
+    // if (res != SE_SUCCESS) {
+    //     LOG_ERRORF("Failed to draw segment: %d", res);
+    // }
+    // res = se_render_line_segment_norm_coord(
+    //     ball_side_top,
+    //     ball_side_bottom,
+    //     (vec4) { 1.0f, 0.0f, 0.0f, 1.0f }
+    // );
+    // if (res != SE_SUCCESS) {
+    //     LOG_ERRORF("Failed to draw segment: %d", res);
+    // }
+    // res = se_render_line_segment_norm_coord(
+    //     (vec2) { paddle_back_x, 0.0f },
+    //     (vec2) { paddle_back_x, 1.0f },
+    //     (vec4) { 0.0f, 1.0f, 0.0f, 1.0f }
+    // );
+    // if (res != SE_SUCCESS) {
+    //     LOG_ERRORF("Failed to draw segment: %d", res);
+    // }
+
+    const bool inside_paddle = (ball_side_top[0] <= paddle_side_top[0] && ball_side_top[0] >= paddle_back_x)
+        && ((ball_side_top[1] <= paddle_side_bottom[1] && ball_side_top[1] >= paddle_side_top[1])
+            || (ball_side_bottom[1] <= paddle_side_bottom[1] && ball_side_bottom[1] >= paddle_side_top[1]));
+
+    if (inside_paddle) {
+        ball->dirv[0] *= -1.0f;
+        ball->dirv[1] = possible_y_directions[se_rand_int() % 3];
+        return;
+    };
+
+    collision = se_line_segments_intersect(
+        ball_pos,
+        new_pos_adj,
+        paddle_side_top,
+        paddle_side_bottom,
+        ip
+    );
+
+    if (collision) {
+        new_pos[0] = mirror_ip ? 1.0f - ip[0] : ip[0];
+        new_pos[1] = ip[1];
+        state->ball.dirv[0] *= -1.0f;
+        state->ball.dirv[1] = possible_y_directions[se_rand_int() % 3];
+        // state->ball.dirv[1] = 0.0f;
+    }
+}
+
+static void ball_move(GameState* const state, const double delta) {
+    vec2 ball_movement;
+    const float ball_movement_scale = delta * state->ball.speed;
+    // glmc_vec2_normalize(state->ball.dirv);
+    glmc_vec2_scale(state->ball.dirv, ball_movement_scale, ball_movement);
+
+    vec2 new_pos;
+    glmc_vec2_add(state->ball.pos, ball_movement, new_pos);
+
+    if (ball_bounds_collision_check(state, new_pos)) {
+        return;
+    }
+    ball_paddles_collision_check(state, new_pos);
+
+    glmc_vec2_copy(new_pos, state->ball.pos);
+}
+
+void GameState_move_paddle_l(GameState* const state, const SEDirection dir) {
+    switch (dir) {
+    case SE_UP:
+        state->paddle_l.dirv[1] = -1.0f;
+        break;
+    case SE_DOWN:
+        state->paddle_l.dirv[1] = 1.0f;
+        break;
+    case SE_NONE:
+        state->paddle_l.dirv[1] = 0.0f;
+    default:
+        break;
+    }
+}
+
+void GameState_update(GameState* const state, const double delta) {
+    // Paddles
+    vec2 paddle_movement;
+    const float paddle_movement_scale = delta * state->paddle_l.speed;
+    glmc_vec2_scale(state->paddle_l.dirv, paddle_movement_scale, paddle_movement);
+    glmc_vec2_add(state->paddle_l.pos, paddle_movement, state->paddle_l.pos);
+    paddle_collision_check(&state->paddle_l);
+
+    paddle_r_move(state);
+    glmc_vec2_scale(state->paddle_r.dirv, paddle_movement_scale, paddle_movement);
+    glmc_vec2_add(state->paddle_r.pos, paddle_movement, state->paddle_r.pos);
+    paddle_collision_check(&state->paddle_r);
+
+    // Ball
+    ball_move(state, delta);
+}

+ 40 - 0
src/common/game.h

@@ -0,0 +1,40 @@
+#ifndef GAME_H_
+#define GAME_H_
+
+#include <stdint.h>
+
+#include "cglm/types.h"
+
+#include "scufeng/engine.h"
+
+typedef struct {
+    vec2 pos;
+    float w;
+    float h;
+    vec2 dirv;
+    float speed;
+} MovingObject;
+
+typedef struct {
+    MovingObject paddle_l;
+    MovingObject paddle_r;
+    MovingObject ball;
+    vec2 ball_start_pos;
+    vec2 ball_start_dir;
+    uint8_t score_l;
+    uint8_t score_r;
+} GameState;
+
+GameState GameState_new(
+    const float paddles_w,
+    const float paddles_h,
+    const float paddle_l_x,
+    const float paddle_speed,
+    const float ball_w,
+    const vec2 ball_pos,
+    const float ball_speed
+);
+void GameState_move_paddle_l(GameState *const state, const SEDirection dir);
+void GameState_update(GameState *const state, const double delta);
+
+#endif // GAME_H_

+ 61 - 0
src/game/MainMenu.c

@@ -0,0 +1,61 @@
+#include <stdio.h>
+
+#include "scufeng/engine.h"
+#include "scufeng/Shape.h"
+#include "scufeng/Scene.h"
+#include "cglm/vec3.h"
+
+#include "MainMenu.h"
+#include "screen.h"
+
+MainMenu MainMenu_alloc(const unsigned int screen_w, const unsigned int screen_h) {
+    MainMenu mm;
+
+    mm.title_text = SEMesh_string_alloc("Pong", 1.0f, GLM_VEC3_ONE);
+    mm.title_text.transform.pos[0] = screen_w / 2.0f;
+    mm.title_text.transform.pos[1] = screen_h * 0.8f;
+
+    mm.sp_text = SEMesh_string_alloc("Singleplayer", 1.0f, GLM_VEC3_ONE);
+    mm.sp_text.transform.pos[0] = screen_w * 0.25f;
+    mm.sp_text.transform.pos[1] = screen_h * 0.5f;
+
+    mm.mp_text = SEMesh_string_alloc("Multiplayer", 1.0f, GLM_VEC3_ONE);
+    mm.mp_text.transform.pos[0] = screen_w - mm.sp_text.transform.pos[0];
+    mm.mp_text.transform.pos[1] = mm.sp_text.transform.pos[1];
+
+    return mm;
+}
+
+static Screen MainMenu_input_handle(MainMenu *const mm) {
+    if (se_get_mouse_button(SE_MOUSE_BUTTON_LEFT) == SE_KEY_ACTION_PRESS) {
+        double x, y;
+        se_get_mouse_pos(&x, &y);
+
+        if (SEMesh_rect_is_pos_inside(&mm->sp_text, x, y)) {
+            printf("SP\n");
+            return PONG_SINGLEPLAYER;
+        }
+
+        if (SEMesh_rect_is_pos_inside(&mm->mp_text, x, y)) {
+            printf("MP\n");
+            return PONG_MULTIPLAYER;
+        }
+    }
+
+    return PONG_MAIN_MENU;
+}
+
+Screen MainMenu_tick(MainMenu *const mm) {
+    const Screen screen = MainMenu_input_handle(mm);
+    if (screen != PONG_MAIN_MENU) {
+        return screen;
+    }
+    SEMesh *meshes[] = {
+        &mm->title_text,
+        &mm->sp_text,
+        &mm->mp_text
+    };
+    se_render_meshes_2d(meshes, sizeof(meshes) / sizeof(meshes[0]));
+
+    return PONG_MAIN_MENU;
+}

+ 14 - 0
src/game/MainMenu.h

@@ -0,0 +1,14 @@
+#pragma once
+
+#include "scufeng/Shape.h"
+
+#include "screen.h"
+
+typedef struct {
+    SEMesh title_text;
+    SEMesh sp_text;
+    SEMesh mp_text;
+} MainMenu;
+
+MainMenu MainMenu_alloc(const unsigned int screen_w, const unsigned int screen_h);
+Screen MainMenu_tick(MainMenu *const mm);

+ 63 - 0
src/game/MultiPlayer.c

@@ -0,0 +1,63 @@
+#include <stdio.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <errno.h>
+#include <string.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <unistd.h>
+
+#include "scufeng/log.h"
+
+#include "MultiPlayer.h"
+
+MultiPlayer MultiPlayer_alloc(
+    const char *server_host,
+    const uint16_t server_port
+) {
+    MultiPlayer mp;
+
+    struct protoent *proto = getprotobyname("tcp");
+    mp.sock = socket(AF_INET, SOCK_STREAM, proto->p_proto);
+    if (mp.sock == -1) {
+        LOG_FATALF("socket failed: %d %s", errno, strerror(errno));
+    }
+
+    struct in_addr server_addr;
+    int res = inet_aton(server_host, &server_addr);
+    if (res == 0) {
+        LOG_FATAL("inet_aton failed");
+    }
+
+    struct sockaddr_in server_sockaddr = {
+        .sin_family = AF_INET,
+        .sin_addr = server_addr,
+        .sin_port = htons(server_port)
+    };
+
+    mp.conn = connect(
+        mp.sock,
+        (struct sockaddr *) &server_sockaddr,
+        sizeof(server_sockaddr)
+    );
+    if (mp.conn == -1) {
+        LOG_FATALF("connect failed: %d %s", errno, strerror(errno));
+    }
+
+    return mp;
+}
+
+void MultiPlayer_free(MultiPlayer *const mp) {
+    close(mp->conn);
+    mp->conn = 0;
+    close(mp->sock);
+    mp->sock = 0;
+}
+
+void MultiPlayer_tick(MultiPlayer *const mp) {
+    char buf[1024] = { 0 };
+    const size_t buf_size = sizeof(buf) / sizeof(buf[0]);
+    fgets(buf, buf_size, stdin);
+
+    send(mp->conn, buf, strlen(buf), 0);
+}

+ 15 - 0
src/game/MultiPlayer.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <stdint.h>
+
+typedef struct {
+    int sock;
+    int conn;
+} MultiPlayer;
+
+MultiPlayer MultiPlayer_alloc(
+    const char *server_host,
+    const uint16_t server_port
+);
+void MultiPlayer_free(MultiPlayer *const mp);
+void MultiPlayer_tick(MultiPlayer *const mp);

+ 122 - 0
src/game/SinglePlayer.c

@@ -0,0 +1,122 @@
+#include "cglm/vec4.h"
+#include "scufeng/Scene.h"
+
+#include "SinglePlayer.h"
+#include "screen.h"
+
+SinglePlayer SinglePlayer_alloc(
+    unsigned int screen_w,
+    unsigned int screen_h,
+    float paddles_w,
+    float paddles_h,
+    float paddle_l_x,
+    float paddle_speed,
+    float ball_w,
+    vec2 ball_pos,
+    float ball_speed
+) {
+    SinglePlayer sp = {
+        .screen_w = screen_w,
+        .screen_h = screen_h,
+        .state = GameState_new(
+            paddles_w,
+            paddles_h,
+            paddle_l_x,
+            paddle_speed,
+            ball_w,
+            ball_pos,
+            ball_speed
+        ),
+        .paddle_l = SEMesh_rect_color_alloc(GLM_VEC4_ONE),
+        .paddle_r = SEMesh_rect_color_alloc(GLM_VEC4_ONE),
+        .ball = SEMesh_rect_color_alloc(GLM_VEC4_ONE),
+    };
+    sp.paddle_l.transform.scale[0] = screen_w * paddles_w;
+    sp.paddle_l.transform.scale[1] = screen_h * paddles_h;
+    sp.paddle_l.transform.pos[0] = screen_w * paddle_l_x;
+    sp.paddle_l.transform.pos[1] = screen_h / 2.0f;
+
+    sp.paddle_r.transform.scale[0] = screen_w * paddles_w;
+    sp.paddle_r.transform.scale[1] = screen_h * paddles_h;
+    sp.paddle_r.transform.pos[0] = screen_w * (1.0f - paddle_l_x);
+    sp.paddle_r.transform.pos[1] = screen_h / 2.0f;
+
+    sp.ball.transform.scale[0] = screen_w * ball_w;
+    sp.ball.transform.scale[1] = sp.ball.transform.scale[0];
+    sp.ball.transform.pos[0] = screen_w * ball_pos[0];
+    sp.ball.transform.pos[1] = screen_h * ball_pos[1];
+
+    return sp;
+}
+
+void SinglePlayer_free(SinglePlayer *sp) {
+    SEMesh_free(&sp->paddle_l);
+    SEMesh_free(&sp->paddle_r);
+    SEMesh_free(&sp->ball);
+}
+
+static void pos_convert(
+    vec3 render_pos,
+    const vec3 state_pos,
+    const unsigned int screen_w,
+    const unsigned int screen_h
+) {
+    render_pos[0] = state_pos[0] * screen_w;
+    render_pos[1] = (1.0f - state_pos[1]) * screen_h;
+}
+
+static void update_shapes(
+    const GameState *const state,
+    SEMesh *const paddle_l,
+    SEMesh *const paddle_r,
+    SEMesh *const ball,
+    const unsigned int screen_w,
+    const unsigned int screen_h
+) {
+
+    // Paddles
+    pos_convert(paddle_l->transform.pos, state->paddle_l.pos, screen_w, screen_h);
+    pos_convert(paddle_r->transform.pos, state->paddle_r.pos, screen_w, screen_h);
+
+    // Ball
+    pos_convert(ball->transform.pos, state->ball.pos, screen_w, screen_h);
+}
+
+static bool key_input_handle(GameState *const state) {
+    if (se_get_key(SE_KEY_W) == SE_KEY_ACTION_PRESS) {
+        GameState_move_paddle_l(state, SE_UP);
+    } else if (se_get_key(SE_KEY_S) == SE_KEY_ACTION_PRESS) {
+        GameState_move_paddle_l(state, SE_DOWN);
+    } else {
+        GameState_move_paddle_l(state, SE_NONE);
+    }
+
+    if (se_get_key(SE_KEY_ESCAPE) == SE_KEY_ACTION_PRESS) {
+        return true;
+    }
+
+    return false;
+}
+
+Screen SinglePlayer_tick(SinglePlayer *const sp, const double delta) {
+    if (key_input_handle(&sp->state)) {
+        return PONG_MAIN_MENU;
+    }
+    GameState_update(&sp->state, delta);
+    update_shapes(
+        &sp->state,
+        &sp->paddle_l,
+        &sp->paddle_r,
+        &sp->ball,
+        sp->screen_w,
+        sp->screen_h
+    );
+    SEMesh *meshes[] = {
+        &sp->paddle_l,
+        &sp->paddle_r,
+        &sp->ball
+    };
+    se_render_meshes_2d(meshes, sizeof(meshes) / sizeof(meshes[0]));
+
+    return PONG_SINGLEPLAYER;
+}

+ 29 - 0
src/game/SinglePlayer.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include "scufeng/Shape.h"
+
+#include "game.h"
+#include "screen.h"
+
+typedef struct {
+    unsigned int screen_w;
+    unsigned int screen_h;
+    GameState state;
+    SEMesh paddle_l;
+    SEMesh paddle_r;
+    SEMesh ball;
+} SinglePlayer;
+
+SinglePlayer SinglePlayer_alloc(
+    unsigned int screen_w,
+    unsigned int screen_h,
+    float paddles_w,
+    float paddles_h,
+    float paddle_l_x,
+    float paddle_speed,
+    float ball_w,
+    vec2 ball_pos,
+    float ball_speed
+);
+void SinglePlayer_free(SinglePlayer *sp);
+Screen SinglePlayer_tick(SinglePlayer *const sp, const double delta);

+ 78 - 0
src/game/pong.c

@@ -0,0 +1,78 @@
+#include "cglm/types.h"
+#include "scufeng/engine.h"
+#include "scufeng/text.h"
+
+#include "screen.h"
+#include "MainMenu.h"
+#include "SinglePlayer.h"
+#include "MultiPlayer.h"
+
+static const unsigned int screen_w = 1200;
+static const unsigned int screen_h = 600;
+static const float paddles_w = 0.01f;
+static const float paddles_h = 0.10f;
+static const float paddle_l_x = (1.0f / 16);
+static const float ball_w = paddles_w * 0.5f;
+static vec2 ball_pos = { 0.5f, 0.5f };
+static const float paddle_speed = 0.7f;
+static const float ball_speed = 0.7f;
+
+int main() {
+    se_init(true);
+    se_window_create(screen_w, screen_h, "Pong", -1);
+    se_swap_interval(0);
+    se_fps_lock_set_fps(60);
+    se_font_load(FONT_PATH, 0, 48);
+
+    se_clear_color(0.0f, 0.0f, 0.0f, 1.0f);
+
+    MainMenu mm = MainMenu_alloc(screen_w, screen_h);
+    SinglePlayer sp;
+
+    double last_frame_time = 0.0;
+    while (!se_window_should_close()) {
+        const double current_time = se_get_time();
+        const double delta = current_time - last_frame_time;
+        last_frame_time = current_time;
+        se_fps_lock_frame_start_s(current_time);
+
+        se_clear();
+        se_handle_events();
+
+        switch (current_screen) {
+        case PONG_MAIN_MENU:
+            current_screen = MainMenu_tick(&mm);
+            if (current_screen == PONG_SINGLEPLAYER) {
+                sp = SinglePlayer_alloc(
+                    screen_w,
+                    screen_h,
+                    paddles_w,
+                    paddles_h,
+                    paddle_l_x,
+                    paddle_speed,
+                    ball_w,
+                    ball_pos,
+                    ball_speed
+                );
+            }
+            break;
+        case PONG_SINGLEPLAYER:
+            current_screen = SinglePlayer_tick(&sp, delta);
+            if (current_screen == PONG_MAIN_MENU) {
+                SinglePlayer_free(&sp);
+            }
+            break;
+
+        case PONG_MULTIPLAYER:
+            MultiPlayer_tick();
+            break;
+        }
+
+        // printf("%u %u\n", sp.state.score_l, sp.state.score_r);
+        se_fps_lock_wait();
+        se_swap_buffers();
+        // printf("FT: %lf FPS: %lf\n", se_get_time() - current_time, 1.0 / (se_get_time() - current_time));
+    }
+
+    return 0;
+}

+ 3 - 0
src/game/screen.c

@@ -0,0 +1,3 @@
+#include "screen.h"
+
+Screen current_screen = PONG_MAIN_MENU;

+ 9 - 0
src/game/screen.h

@@ -0,0 +1,9 @@
+#pragma once
+
+typedef enum {
+    PONG_MAIN_MENU,
+    PONG_SINGLEPLAYER,
+    PONG_MULTIPLAYER,
+} Screen;
+
+extern Screen current_screen;

+ 70 - 0
src/server/server.c

@@ -0,0 +1,70 @@
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include "scufeng/log.h"
+
+#define SERVER_HOST "127.0.0.1"
+#define SERVER_PORT 8080
+
+int main() {
+    struct protoent *proto = getprotobyname("tcp");
+    if (proto == nullptr) {
+        LOG_FATAL("getprotobyname failed");
+    }
+    const int sock = socket(AF_INET, SOCK_STREAM, proto->p_proto);
+    if (sock == -1) {
+        LOG_FATALF("socket failed: %s", strerror(errno));
+    }
+
+    {
+        struct in_addr addr;
+        int res = inet_aton(SERVER_HOST, &addr);
+        if (res == 0) {
+            LOG_FATAL("inet_aton failed");
+        }
+        struct sockaddr_in own_addr = {
+            .sin_family = AF_INET,
+            .sin_addr = addr,
+            .sin_port = htons(SERVER_PORT)
+        };
+        res = bind(sock, (struct sockaddr*) &own_addr, sizeof(own_addr));
+        if (res == -1) {
+            LOG_FATALF("bind failed: %s", strerror(errno));
+        }
+    }
+
+    int res = listen(sock, 5);
+    if (res == -1) {
+        LOG_FATALF("listen failed: %s", strerror(errno));
+    }
+
+    int conn = -1;
+
+    conn = accept(sock, nullptr, nullptr);
+    if (conn == -1) {
+        LOG_FATALF("accept failed: %s", strerror(errno));
+    }
+
+    char buf[1024] = {0};
+    const size_t buf_size = sizeof(buf) / sizeof(buf[0]);
+
+    while (true) {
+       int received = recv(conn, buf, buf_size, 0); 
+       if (received == 0) {
+           printf("buf: %s\n", buf);
+           buf[0] = '\0';
+           break;
+       } else if (received == -1) {
+           LOG_FATALF("recv failed: %d %s", errno, strerror(errno));
+       } else {
+           buf[received] = '\0';
+       }
+    }
+
+    return 0;
+}

+ 0 - 0
subprojects/.wraplock


+ 1 - 0
subprojects/scufeng/GIT_COMMIT

@@ -0,0 +1 @@
+c505e04113e221188440051de27fad12c0670205

+ 37 - 0
subprojects/scufeng/include/scufeng/Camera.h

@@ -0,0 +1,37 @@
+#ifndef SCUFENG_CAMERA_H_
+#define SCUFENG_CAMERA_H_
+
+#include "cglm/types.h"
+
+#include "scufeng/engine.h"
+
+typedef struct {
+    vec3 pos;
+    float pitch_rad;
+    float yaw_rad;
+    float fov_rad;
+    float near_clip;
+    float far_clip;
+    vec3 front;
+    vec3 right;
+    mat4 view;
+    mat4 projection;
+} SECamera;
+
+SECamera SECamera_new(
+    vec3 pos,
+    const float fov_rad,
+    const float near_clip,
+    const float far_clip
+);
+
+void SECamera_set_perspective(SECamera *const camera);
+void SECamera_pos_set(SECamera *const camera, vec3 pos);
+void SECamera_move_to_direction(SECamera *const camera, SEDirection dir, const float mag);
+void SECamera_look_dir(SECamera *const camera, vec3 dir);
+void SECamera_look_at(SECamera *const camera, vec3 target);
+void SECamera_pitch_add(SECamera *const camera, const float pitch_rad);
+void SECamera_yaw_add(SECamera *const camera, const float yaw_rad);
+void SECamera_view_update(SECamera *const camera);
+
+#endif // SCUFENG_CAMERA_H_

+ 12 - 0
subprojects/scufeng/include/scufeng/Scene.h

@@ -0,0 +1,12 @@
+#ifndef SCUFENG_SCENE_H_
+#define SCUFENG_SCENE_H_
+
+#include "scufeng/Shape.h"
+#include "engine.h"
+
+void se_render_meshes_2d(SEMesh *const *const meshes, const size_t meshes_n);
+SEResult se_render_line_segment(const vec2 p1, const vec2 p2, const vec4 color);
+SEResult se_render_line_segment_norm_coord(const vec2 p1, const vec2 p2, const vec4 color);
+void se_text_render(const char* text, int x, int y, float scale, const vec3 color);
+
+#endif // SCUFENG_SCENE_H_

+ 90 - 0
subprojects/scufeng/include/scufeng/Shape.h

@@ -0,0 +1,90 @@
+#ifndef SCUFENG_SHAPE_H_
+#define SCUFENG_SHAPE_H_
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "cglm/types.h"
+
+#include "scufeng/engine.h"
+
+typedef enum {
+    SE_TRIANGLES,
+    SE_TRIANGLE_STRIP,
+    SE_TRIANGLE_FAN,
+    SE_LINES,
+    SE_LINE_STRIP
+} SEDrawMode;
+
+typedef struct {
+    vec3 pos;
+    vec3 scale;
+    vec3 rotation;
+} SETransform;
+
+typedef struct {
+    float pos[3];
+    float tex_coords[2];
+} SEVertex;
+
+typedef enum {
+    SE_MESH_COLOR,
+    SE_MESH_TEXTURE
+} SEMeshType;
+
+typedef struct {
+    unsigned int id;
+    float w;
+    float h;
+} SEMeshTexture;
+
+typedef struct {
+    unsigned int vao;
+    unsigned int vbo;
+    size_t verts_n;
+    int draw_mode;
+
+    SETransform transform;
+
+    SEMeshType type;
+    union {
+        SEMeshTexture texture;
+        vec4 color;
+    };
+} SEMesh;
+
+SEResult SEShape_alloc(
+    SETransform *const shape,
+    const SEVertex *const vertices,
+    const size_t vertices_n,
+    const vec4 color,
+    const SEDrawMode draw_mode
+);
+void SEMesh_free(SEMesh *const mesh);
+void SEMesh_set_draw_mode(SEMesh *mesh, const SEDrawMode mode);
+void SEMesh_render(const SEMesh *const mesh);
+void SEMesh_bbox_get_2d(
+    const SEMesh *const mesh,
+    float *left,
+    float *right,
+    float *top,
+    float *bottom
+);
+bool SEMesh_rect_is_pos_inside(const SEMesh *const mesh, const float x, const float y);
+void SEShape_rotation_set(SETransform *shape, vec3 rotation);
+void SEShape_rotate_2d_center(SETransform *const shape, const float w, const float h, const float angle_rad);
+void SEShape_scale_set(SETransform *shape, vec3 scale);
+void SEShape_scale(SETransform *const shape, vec3 scale);
+void SEShape_pos_set(SETransform *const shape, vec3 pos);
+void SEShape_translate_2d_center(SETransform *const shape, const float w, const float h, const vec2 pos);
+void SEShape_transform_reset(SETransform *shape);
+
+SEMesh SEMesh_rect_color_alloc(vec4 color);
+SEMesh SEMesh_string_alloc(
+    const char *text,
+    const float scale,
+    const vec3 color
+);
+// void SEShape_transform_apply(SEShape *const shape);
+
+#endif // SCUFENG_SHAPE_H_

+ 74 - 0
subprojects/scufeng/include/scufeng/engine.h

@@ -0,0 +1,74 @@
+#ifndef SCUFENG_H_
+#define SCUFENG_H_
+
+#include <stdbool.h>
+
+#include "keys.h" // IWYU pragma: export
+
+#define SERESULT_X(X) \
+    X(SE_SUCCESS) \
+    X(SE_OUT_OF_MEMORY) \
+    X(SE_FC_FAILURE) \
+    X(SE_NOT_FOUND) \
+    X(SE_FT_FAILURE)
+
+#define X(val) val,
+
+typedef enum {
+    SERESULT_X(X)
+} SEResult;
+
+#undef X
+
+typedef enum {
+    SE_FILL,
+    SE_LINE,
+    SE_POINT
+} SEPolygonMode;
+
+typedef enum {
+    SE_NONE,
+    SE_FORWARD,
+    SE_BACKWARD,
+    SE_LEFT,
+    SE_RIGHT,
+    SE_UP,
+    SE_DOWN
+} SEDirection;
+
+typedef void (*SEKeyCb)(SEKey key, SEKeyAction action, SEKeyMod mods);
+
+void se_init(const bool debug);
+void se_deinit(void);
+void se_window_create(
+    const int w,
+    const int h,
+    const char* title,
+    const int fullscreen_monitor_i
+);
+void se_window_close();
+void se_clear_color(
+    const float r,
+    const float g,
+    const float b,
+    const float a
+);
+void se_clear(void);
+void se_swap_buffers(void);
+bool se_window_should_close(void);
+void se_handle_events(void);
+double se_get_time(void);
+double se_delta_time(const double current_time);
+void se_set_polygon_mode(const SEPolygonMode mode);
+void se_set_key_callback(SEKeyCb cb);
+SEKeyAction se_get_key(const SEKey key);
+SEKeyAction se_get_mouse_button(const SEMouseButton btn);
+void se_get_mouse_pos(double *x, double *y);
+void se_swap_interval(const int interval);
+void se_fps_lock_set_fps(const unsigned int fps);
+void se_fps_lock_frame_start_s(const double frame_start_s);
+void se_fps_lock_wait(void);
+
+const char* SEResult_to_str(const SEResult res);
+
+#endif // SCUFENG_H_

+ 164 - 0
subprojects/scufeng/include/scufeng/keys.h

@@ -0,0 +1,164 @@
+#ifndef SCUFENG_KEYS_H_
+#define SCUFENG_KEYS_H_
+
+#include "GLFW/glfw3.h"
+
+typedef enum {
+    SE_KEY_UNKNOWN = GLFW_KEY_UNKNOWN,
+
+    /* Printable keys */
+    SE_KEY_SPACE = GLFW_KEY_SPACE,
+    SE_KEY_APOSTROPHE = GLFW_KEY_APOSTROPHE,
+    SE_KEY_COMMA = GLFW_KEY_COMMA,
+    SE_KEY_MINUS = GLFW_KEY_MINUS,
+    SE_KEY_PERIOD = GLFW_KEY_PERIOD,
+    SE_KEY_SLASH = GLFW_KEY_SLASH,
+    SE_KEY_0 = GLFW_KEY_0,
+    SE_KEY_1 = GLFW_KEY_1,
+    SE_KEY_2 = GLFW_KEY_2,
+    SE_KEY_3 = GLFW_KEY_3,
+    SE_KEY_4 = GLFW_KEY_4,
+    SE_KEY_5 = GLFW_KEY_5,
+    SE_KEY_6 = GLFW_KEY_6,
+    SE_KEY_7 = GLFW_KEY_7,
+    SE_KEY_8 = GLFW_KEY_8,
+    SE_KEY_9 = GLFW_KEY_9,
+    SE_KEY_SEMICOLON = GLFW_KEY_SEMICOLON,
+    SE_KEY_EQUAL = GLFW_KEY_EQUAL,
+    SE_KEY_A = GLFW_KEY_A,
+    SE_KEY_B = GLFW_KEY_B,
+    SE_KEY_C = GLFW_KEY_C,
+    SE_KEY_D = GLFW_KEY_D,
+    SE_KEY_E = GLFW_KEY_E,
+    SE_KEY_F = GLFW_KEY_F,
+    SE_KEY_G = GLFW_KEY_G,
+    SE_KEY_H = GLFW_KEY_H,
+    SE_KEY_I = GLFW_KEY_I,
+    SE_KEY_J = GLFW_KEY_J,
+    SE_KEY_K = GLFW_KEY_K,
+    SE_KEY_L = GLFW_KEY_L,
+    SE_KEY_M = GLFW_KEY_M,
+    SE_KEY_N = GLFW_KEY_N,
+    SE_KEY_O = GLFW_KEY_O,
+    SE_KEY_P = GLFW_KEY_P,
+    SE_KEY_Q = GLFW_KEY_Q,
+    SE_KEY_R = GLFW_KEY_R,
+    SE_KEY_S = GLFW_KEY_S,
+    SE_KEY_T = GLFW_KEY_T,
+    SE_KEY_U = GLFW_KEY_U,
+    SE_KEY_V = GLFW_KEY_V,
+    SE_KEY_W = GLFW_KEY_W,
+    SE_KEY_X = GLFW_KEY_X,
+    SE_KEY_Y = GLFW_KEY_Y,
+    SE_KEY_Z = GLFW_KEY_Z,
+    SE_KEY_LEFT_BRACKET = GLFW_KEY_LEFT_BRACKET,
+    SE_KEY_BACKSLASH = GLFW_KEY_BACKSLASH,
+    SE_KEY_RIGHT_BRACKET = GLFW_KEY_RIGHT_BRACKET,
+    SE_KEY_GRAVE_ACCENT = GLFW_KEY_GRAVE_ACCENT,
+    SE_KEY_WORLD_1 = GLFW_KEY_WORLD_1,
+    SE_KEY_WORLD_2 = GLFW_KEY_WORLD_2,
+
+    /* Function keys */
+    SE_KEY_ESCAPE = GLFW_KEY_ESCAPE,
+    SE_KEY_ENTER = GLFW_KEY_ENTER,
+    SE_KEY_TAB = GLFW_KEY_TAB,
+    SE_KEY_BACKSPACE = GLFW_KEY_BACKSPACE,
+    SE_KEY_INSERT = GLFW_KEY_INSERT,
+    SE_KEY_DELETE = GLFW_KEY_DELETE,
+    SE_KEY_RIGHT = GLFW_KEY_RIGHT,
+    SE_KEY_LEFT = GLFW_KEY_LEFT,
+    SE_KEY_DOWN = GLFW_KEY_DOWN,
+    SE_KEY_UP = GLFW_KEY_UP,
+    SE_KEY_PAGE_UP = GLFW_KEY_PAGE_UP,
+    SE_KEY_PAGE_DOWN = GLFW_KEY_PAGE_DOWN,
+    SE_KEY_HOME = GLFW_KEY_HOME,
+    SE_KEY_END = GLFW_KEY_END,
+    SE_KEY_CAPS_LOCK = GLFW_KEY_CAPS_LOCK,
+    SE_KEY_SCROLL_LOCK = GLFW_KEY_SCROLL_LOCK,
+    SE_KEY_NUM_LOCK = GLFW_KEY_NUM_LOCK,
+    SE_KEY_PRINT_SCREEN = GLFW_KEY_PRINT_SCREEN,
+    SE_KEY_PAUSE = GLFW_KEY_PAUSE,
+    SE_KEY_F1 = GLFW_KEY_F1,
+    SE_KEY_F2 = GLFW_KEY_F2,
+    SE_KEY_F3 = GLFW_KEY_F3,
+    SE_KEY_F4 = GLFW_KEY_F4,
+    SE_KEY_F5 = GLFW_KEY_F5,
+    SE_KEY_F6 = GLFW_KEY_F6,
+    SE_KEY_F7 = GLFW_KEY_F7,
+    SE_KEY_F8 = GLFW_KEY_F8,
+    SE_KEY_F9 = GLFW_KEY_F9,
+    SE_KEY_F10 = GLFW_KEY_F10,
+    SE_KEY_F11 = GLFW_KEY_F11,
+    SE_KEY_F12 = GLFW_KEY_F12,
+    SE_KEY_F13 = GLFW_KEY_F13,
+    SE_KEY_F14 = GLFW_KEY_F14,
+    SE_KEY_F15 = GLFW_KEY_F15,
+    SE_KEY_F16 = GLFW_KEY_F16,
+    SE_KEY_F17 = GLFW_KEY_F17,
+    SE_KEY_F18 = GLFW_KEY_F18,
+    SE_KEY_F19 = GLFW_KEY_F19,
+    SE_KEY_F20 = GLFW_KEY_F20,
+    SE_KEY_F21 = GLFW_KEY_F21,
+    SE_KEY_F22 = GLFW_KEY_F22,
+    SE_KEY_F23 = GLFW_KEY_F23,
+    SE_KEY_F24 = GLFW_KEY_F24,
+    SE_KEY_F25 = GLFW_KEY_F25,
+    SE_KEY_KP_0 = GLFW_KEY_KP_0,
+    SE_KEY_KP_1 = GLFW_KEY_KP_1,
+    SE_KEY_KP_2 = GLFW_KEY_KP_2,
+    SE_KEY_KP_3 = GLFW_KEY_KP_3,
+    SE_KEY_KP_4 = GLFW_KEY_KP_4,
+    SE_KEY_KP_5 = GLFW_KEY_KP_5,
+    SE_KEY_KP_6 = GLFW_KEY_KP_6,
+    SE_KEY_KP_7 = GLFW_KEY_KP_7,
+    SE_KEY_KP_8 = GLFW_KEY_KP_8,
+    SE_KEY_KP_9 = GLFW_KEY_KP_9,
+    SE_KEY_KP_DECIMAL = GLFW_KEY_KP_DECIMAL,
+    SE_KEY_KP_DIVIDE = GLFW_KEY_KP_DIVIDE,
+    SE_KEY_KP_MULTIPLY = GLFW_KEY_KP_MULTIPLY,
+    SE_KEY_KP_SUBTRACT = GLFW_KEY_KP_SUBTRACT,
+    SE_KEY_KP_ADD = GLFW_KEY_KP_ADD,
+    SE_KEY_KP_ENTER = GLFW_KEY_KP_ENTER,
+    SE_KEY_KP_EQUAL = GLFW_KEY_KP_EQUAL,
+    SE_KEY_LEFT_SHIFT = GLFW_KEY_LEFT_SHIFT,
+    SE_KEY_LEFT_CONTROL = GLFW_KEY_LEFT_CONTROL,
+    SE_KEY_LEFT_ALT = GLFW_KEY_LEFT_ALT,
+    SE_KEY_LEFT_SUPER = GLFW_KEY_LEFT_SUPER,
+    SE_KEY_RIGHT_SHIFT = GLFW_KEY_RIGHT_SHIFT,
+    SE_KEY_RIGHT_CONTROL = GLFW_KEY_RIGHT_CONTROL,
+    SE_KEY_RIGHT_ALT = GLFW_KEY_RIGHT_ALT,
+    SE_KEY_RIGHT_SUPER = GLFW_KEY_RIGHT_SUPER,
+    SE_KEY_MENU = GLFW_KEY_MENU,
+} SEKey;
+
+typedef enum {
+    SE_KEY_ACTION_RELEASE = GLFW_RELEASE,
+    SE_KEY_ACTION_PRESS = GLFW_PRESS,
+    SE_KEY_ACTION_REPEAT = GLFW_REPEAT,
+} SEKeyAction;
+
+typedef enum {
+    SE_KEY_MOD_SHIFT = GLFW_MOD_SHIFT,
+    SE_KEY_MOD_CONTROL = GLFW_MOD_CONTROL,
+    SE_KEY_MOD_ALT = GLFW_MOD_ALT,
+    SE_KEY_MOD_SUPER = GLFW_MOD_SUPER,
+    SE_KEY_MOD_CAPS_LOCK = GLFW_MOD_CAPS_LOCK,
+    SE_KEY_MOD_NUM_LOCK = GLFW_MOD_NUM_LOCK,
+} SEKeyMod;
+
+typedef enum {
+    SE_MOUSE_BUTTON_1 = 0,
+    SE_MOUSE_BUTTON_2 = 1,
+    SE_MOUSE_BUTTON_3 = 2,
+    SE_MOUSE_BUTTON_4 = 3,
+    SE_MOUSE_BUTTON_5 = 4,
+    SE_MOUSE_BUTTON_6 = 5,
+    SE_MOUSE_BUTTON_7 = 6,
+    SE_MOUSE_BUTTON_8 = 7,
+    SE_MOUSE_BUTTON_LAST = GLFW_MOUSE_BUTTON_8,
+    SE_MOUSE_BUTTON_LEFT = GLFW_MOUSE_BUTTON_1,
+    SE_MOUSE_BUTTON_RIGHT = GLFW_MOUSE_BUTTON_2,
+    SE_MOUSE_BUTTON_MIDDLE = GLFW_MOUSE_BUTTON_3,
+} SEMouseButton;
+
+#endif // SCUFENG_KEYS_H_

+ 23 - 0
subprojects/scufeng/include/scufeng/log.h

@@ -0,0 +1,23 @@
+#ifndef LOG_H_
+#define LOG_H_
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#define LOG(str) log_with_tag(stdout, __FILE__, __LINE__, "LOG", (str))
+#define LOGF(fmt, ...) logf_with_tag(stdout, __FILE__, __LINE__, "LOG", (fmt), __VA_ARGS__)
+
+#define LOG_ERROR(str) log_with_tag(stderr, __FILE__, __LINE__, "ERROR", (str))
+#define LOG_ERRORF(fmt, ...) logf_with_tag(stderr, __FILE__, __LINE__, "ERROR", (fmt), __VA_ARGS__)
+
+#define LOG_FATAL(str) LOG_ERROR(str); exit(-1)
+#define LOG_FATALF(fmt, ...) LOG_ERRORF(fmt, __VA_ARGS__); exit(-1)
+
+#define LOG_FATAL_TODO(str) log_with_tag(stderr, __FILE__, __LINE__, "FATAL TODO", (str)); exit(-1)
+#define LOG_FATAL_TODOF(fmt, ...) logf_with_tag(stderr, __FILE__, __LINE__, "FATAL TODO", (fmt), __VA_ARGS__); exit(-1)
+
+void log_with_tag(FILE* out_file, char* file_name, size_t line, const char* tag, char* str);
+void logf_with_tag(FILE* out_file, char* file_name, size_t line, const char* tag, char* fmt, ...);
+
+#endif // LOG_H_

+ 18 - 0
subprojects/scufeng/include/scufeng/model.h

@@ -0,0 +1,18 @@
+#ifndef SCUFENG_MODEL_H_
+#define SCUFENG_MODEL_H_
+
+#include <stddef.h>
+
+typedef struct {
+    float* vertices;
+    size_t vertices_n;
+} Mesh;
+
+typedef struct {
+    Mesh* meshes;
+    size_t meshes_n;
+} Model;
+
+Model model_load_gltf(const char* path);
+
+#endif // SCUFENG_MODEL_H_

+ 9 - 0
subprojects/scufeng/include/scufeng/scufcont_impl.h

@@ -0,0 +1,9 @@
+#ifndef SE_SCUFCONT_IMPL_H_
+#define SE_SCUFCONT_IMPL_H_
+
+#include "scufcont/DArray.h"
+#include "scufeng/Shape.h"
+
+SCDARRAY_HEADER(SETransform*, SEShapep)
+
+#endif // SE_SCUFCONT_IMPL_H_

+ 12 - 0
subprojects/scufeng/include/scufeng/text.h

@@ -0,0 +1,12 @@
+#ifndef SCUFENG_TEXT_H_
+#define SCUFENG_TEXT_H_
+
+#include "scufeng/engine.h"
+
+SEResult se_font_load(
+    const char *path,
+    const unsigned int pixel_w,
+    const unsigned int pixel_h
+);
+
+#endif // SCUFENG_TEXT_H_

+ 15 - 0
subprojects/scufeng/include/scufeng/util.h

@@ -0,0 +1,15 @@
+#ifndef SCUFENG_UTIL_H_
+#define SCUFENG_UTIL_H_
+
+#include "cglm/types.h"
+
+int se_rand_int();
+bool se_line_segments_intersect(
+    const vec2 a1,
+    const vec2 a2,
+    const vec2 b1,
+    const vec2 b2,
+    vec2 ip
+);
+
+#endif // SCUFENG_UTIL_H_

TEMPAT SAMPAH
subprojects/scufeng/libscufeng.a


+ 15 - 0
subprojects/scufeng/meson.build

@@ -0,0 +1,15 @@
+project('scufeng', 'c')
+
+cc = meson.get_compiler('c')
+
+scufeng_lib_dep = cc.find_library(
+  'scufeng',
+  dirs: meson.current_source_dir()
+)
+
+scufeng_dep = declare_dependency(
+  dependencies: scufeng_lib_dep,
+  include_directories: include_directories('include')
+)
+
+meson.override_dependency('scufeng', scufeng_dep)