Slight reconstruction/abstraction. Getting close to connecting to other sessions etc.

This commit is contained in:
2025-12-14 20:22:29 +01:00
parent 11e91aa01c
commit e9080d7332
17 changed files with 490 additions and 217 deletions

View File

@@ -0,0 +1,87 @@
#include "game_instance.h"
#include <assert.h>
#define INPUT_QUEUE_CAPACITY 8
void game_instance_init(Game_Instance *instance) {
instance->local_player_index = 0xFFFF;
// TODO: SS - Init input-queue.
instance->simulation_accumulator = 0.0f; // TODO: SS - Should probably be moved to the Ingame context.
instance->game_session = NULL;
memset(&instance->game_networking, 0, sizeof(Game_Networking));
squeue_init(&instance->local_input_queue, 4, sizeof(Simulation_Game_Input));
}
void game_instance_dispose(Game_Instance *instance) {
squeue_free(&instance->local_input_queue);
}
bool game_instance_host_session(Game_Instance *instance, const Game_Session_Settings settings) {
assert(instance != NULL);
printf("Trying to host an %s session ...\n", settings.online ? "online" : "offline");
if(instance->game_session != NULL) {
printf("Failed. A session is already active.\n");
return false;
}
instance->game_session = (Game_Session *)calloc(1, sizeof(Game_Session));
if(settings.online) {
if(instance->game_networking.api != NULL) {
printf("Failed. Network is already active.\n");
free(instance->game_session);
instance->game_session = NULL;
return false;
}
if(!networking_try_host(&instance->game_networking, instance->game_session, settings)) {
printf("Failed to host session.\n");
free(instance->game_session);
instance->game_session = NULL;
return false;
}
}
else {
game_session_init(instance->game_session, settings);
}
assert(game_session_create_player(instance->game_session, &instance->local_player_index)); // Create the host (you!).
return true;
}
bool game_instance_join_session(Game_Instance *instance, const char *session_id) {
assert(instance != NULL);
printf("Trying to join a session ...\n");
if(instance->game_session != NULL) {
printf("Failed. A session is already active.\n");
return false;
}
if(instance->game_networking.api != NULL) {
printf("Failed. Network is already active.\n");
return false;
}
return false;
}
bool game_instance_push_local_input(Game_Instance *instance, Simulation_Game_Input input) {
return squeue_push(&instance->local_input_queue, (void *)&input);
}
bool game_instance_pop_local_input(Game_Instance *instance, Simulation_Game_Input *out_input) {
Simulation_Game_Input input = {0};
if(!squeue_pop(&instance->local_input_queue, (void *)&input)) {
return false;
}
*out_input = input;
return true;
}

View File

@@ -0,0 +1,32 @@
#ifndef GAME_INSTANCE_H
#define GAME_INSTANCE_H
#include <stdint.h>
#include "session/networking.h"
#include "session/game_session.h"
#include "shared/squeue.h"
typedef struct {
uint16_t local_player_index;
SQueue local_input_queue;
double simulation_accumulator;
Game_Networking game_networking;
Game_Session *game_session;
} Game_Instance;
void game_instance_init(Game_Instance *instance);
void game_instance_dispose(Game_Instance *instance);
bool game_instance_host_session(Game_Instance *instance, const Game_Session_Settings settings);
bool game_instance_join_session(Game_Instance *instance, const char *session_id);
// TODO: SS - stop_session()
bool game_instance_push_local_input(Game_Instance *instance, Simulation_Game_Input input);
bool game_instance_pop_local_input(Game_Instance *instance, Simulation_Game_Input *out_input);
#endif

View File

@@ -205,7 +205,7 @@ static int compare_keys(const void *key1, const void *key2)
static int loop_check(hashtable_t *parents, const json_t *json, char *key, size_t key_size) static int loop_check(hashtable_t *parents, const json_t *json, char *key, size_t key_size)
{ {
snprintf(key, key_size, "%p", json); snprintf(key, key_size, "%p", (void *)json);
if (hashtable_get(parents, key)) if (hashtable_get(parents, key))
return -1; return -1;

View File

@@ -6,6 +6,9 @@
#include "presentation/states/state_main_menu.h" #include "presentation/states/state_main_menu.h"
#include "raylib.h" #include "raylib.h"
#include "session/networking.h"
#include "instance/game_instance.h"
int main() { int main() {
InitWindow(1280, 720, "snejk"); InitWindow(1280, 720, "snejk");
@@ -14,10 +17,18 @@ int main() {
bool should_quit_game = false; bool should_quit_game = false;
Presentation_State_Ingame_Context presentation_state_ingame_ctx = (Presentation_State_Ingame_Context) { Game_Instance local_game_instance;
}; memset(&local_game_instance, 0, sizeof(Game_Instance));
game_instance_init(&local_game_instance);
Presentation_State_Ingame_Context presentation_state_ingame_ctx;
memset(&presentation_state_ingame_ctx, 0, sizeof(Presentation_State_Ingame_Context));
Presentation_State_Main_Menu_Context presentation_state_main_menu_ctx = { Presentation_State_Main_Menu_Context presentation_state_main_menu_ctx = {
.should_quit_game = &should_quit_game, .should_quit_game = &should_quit_game,
.game_instance = &local_game_instance,
.ingame_ctx = &presentation_state_ingame_ctx
}; };
presentation_state_ingame_init(&presentation_state_ingame_ctx); presentation_state_ingame_init(&presentation_state_ingame_ctx);
@@ -35,6 +46,10 @@ int main() {
presentation_state_machine.current->tick(presentation_state_machine.current); presentation_state_machine.current->tick(presentation_state_machine.current);
} }
if(IsKeyPressed(KEY_F)) {
ToggleFullscreen();
}
// Render. // Render.
BeginDrawing(); BeginDrawing();
{ {
@@ -48,5 +63,7 @@ int main() {
assert(presentation_state_machine_go_to(NULL)); assert(presentation_state_machine_go_to(NULL));
CloseWindow(); CloseWindow();
game_instance_dispose(&local_game_instance);
return 0; return 0;
} }

View File

@@ -21,24 +21,6 @@ static void state_enter(Presentation_State *state) {
printf("Entered ingame.\n"); printf("Entered ingame.\n");
printf("Setting up session ...\n");
assert(g_current_session != NULL);
Game_Session_Settings *settings = &g_current_session->settings;
printf(
"Singleplayer? %i.\n"
"Settings:\n"
"- Seed: %u\n"
"- Level width: %u, height: %u\n"
"- Max players: %u\n"
,
g_current_session->is_singleplayer,
settings->seed,
settings->level_width, settings->level_height,
settings->max_players
);
printf("Setting up context ...\n"); printf("Setting up context ...\n");
ctx->main_camera = (Camera2D) { ctx->main_camera = (Camera2D) {
.offset = (Vector2) { 0, 0 }, .offset = (Vector2) { 0, 0 },
@@ -65,23 +47,46 @@ static void state_tick(Presentation_State *state) {
Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context; Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
(void)ctx; (void)ctx;
Game_Session *session = g_current_session; Game_Instance *instance = ctx->game_instance;
assert(instance != NULL);
Game_Session *session = instance->game_session;
assert(session != NULL);
{ // Push local input to queue. { // Push local input to queue.
game_session_enqueue_player_input(session, session->local_player_index, (Simulation_Game_Input) { Simulation_Game_Input input = (Simulation_Game_Input) { // NOTE: SS - This needs to be slightly modified to support N local players.
.up = IsKeyDown(KEY_UP) || IsKeyDown(KEY_W), .up = IsKeyDown(KEY_UP) || IsKeyDown(KEY_W),
.down = IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S), .down = IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S),
.right = IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D), .right = IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D),
.left = IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A) .left = IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)
}); };
if(!simulation_input_equal(input, ctx->prev_local_input)) {
game_instance_push_local_input(instance, input);
ctx->prev_local_input = input;
}
} }
double delta_time = GetFrameTime(); const double delta_time = GetFrameTime();
game_session_update(session, delta_time); ctx->simulation_accumulator += delta_time;
const double sim_dt = 1.0f / session->settings.tickrate;
while (ctx->simulation_accumulator >= sim_dt) {
// Pop input from instance's local_input_queue and set the local player's input to it, if we have one.
Simulation_Game_Input input = {0};
if(game_instance_pop_local_input(instance, &input)) {
// TODO: SS - We should probably check if this input is invalid for the snake. (pressing "go right" when going left, for example.)
// If it is invalid, the next input in the queue should be tried.
game_session_set_player_input(session, instance->local_player_index, input);
}
game_session_tick(session);
ctx->simulation_accumulator -= sim_dt;
}
{ // TEMP: SS { // TEMP: SS
if(IsKeyPressed(KEY_TAB)) { if(IsKeyPressed(KEY_TAB)) {
ctx->debug_render_match_state = !ctx->debug_render_match_state; ctx->debug_draw_session_details = !ctx->debug_draw_session_details;
} }
if(IsKeyPressed(KEY_ESCAPE)) { if(IsKeyPressed(KEY_ESCAPE)) {
@@ -102,7 +107,9 @@ static void state_render(Presentation_State *state) {
ClearBackground((Color) { 240, 236, 226, 255 }); ClearBackground((Color) { 240, 236, 226, 255 });
Simulation_World *sim_world = &g_current_session->simulation_world; Game_Instance *instance = ctx->game_instance;
Game_Session *session = instance->game_session;
Simulation_World *sim_world = &session->simulation_world;
assert(sim_world != NULL); assert(sim_world != NULL);
Game_World *world = sim_world->game_world; Game_World *world = sim_world->game_world;
@@ -222,7 +229,7 @@ static void state_render(Presentation_State *state) {
// TODO: SS - Don't draw player-name if playing by yourself. // TODO: SS - Don't draw player-name if playing by yourself.
// TODO: SS - Don't draw your own player-name, only others. // TODO: SS - Don't draw your own player-name, only others.
{ // Draw player-name. { // Draw player-name.
const char *player_name = "mirakel"; // NOTE: SS - Hardcoded. const char *player_name = "player"; // NOTE: SS - Hardcoded.
const uint32_t font_size = 8; const uint32_t font_size = 8;
int text_width = MeasureText(player_name, font_size); int text_width = MeasureText(player_name, font_size);
DrawText(player_name, pres_x - (float)text_width/2.0f + 4, pres_y - 16, font_size, (Color) { 255, 255, 255, 128 }); DrawText(player_name, pres_x - (float)text_width/2.0f + 4, pres_y - 16, font_size, (Color) { 255, 255, 255, 128 });
@@ -255,7 +262,7 @@ static void state_render(Presentation_State *state) {
}, },
(Rectangle) { // Destination. (Rectangle) { // Destination.
pres_x + origin.x, pres_x + origin.x,
pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - ((sin(GetTime() * 12) + 1)/2) * 1, pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - ((sin((GetTime() + (x + y)) * 12) + 1)/2) * 1,
GRID_CELL_SIZE, GRID_CELL_SIZE,
GRID_CELL_SIZE GRID_CELL_SIZE
}, },
@@ -271,11 +278,33 @@ static void state_render(Presentation_State *state) {
// TODO: SS - Switch on 'sim_world->match_state'. // TODO: SS - Switch on 'sim_world->match_state'.
if(ctx->debug_render_match_state) { if(ctx->debug_draw_session_details) {
char buf[512]; char buf[1024];
snprintf(&buf[0], sizeof(buf), "Match-state: %i. Tick: %lu.", sim_world->match_state, sim_world->tick); snprintf(&buf[0], sizeof(buf),
"Match-state: %i\n"
"Tick: %lu\n"
"\n"
"Seed: %u\n"
"Level: %ux%u\n"
"Tickrate: %.2f\n"
"\n"
DrawText(buf, 16, 16, 8, RED); // TODO: SS - This should only be shown when playing online. Other things should be shown in the cases below;
// When playing offline/singleplayer you can only be one player.
// When playing local-multiplayer, you control multiple players from the same computer/client/instance.
"Local player: %u (host? %s)\n"
"Player count: %u / %u\n"
,
sim_world->match_state, sim_world->tick,
sim_world->game_world->seed,
sim_world->game_world->grid.width, sim_world->game_world->grid.height,
session->settings.tickrate,
instance->local_player_index, instance->local_player_index == 0 ? "true" : "false", // NOTE: SS - You're the host if you're player 0.
game_session_get_amount_of_active_players(session), sim_world->max_players
);
DrawText(buf, 17, 17, 8, BLACK);
DrawText(buf, 16, 16, 8, WHITE);
} }
} }
EndMode2D(); EndMode2D();
@@ -292,7 +321,7 @@ static void state_exit(Presentation_State *state) {
UnloadTexture(ctx->texture_snake_head); UnloadTexture(ctx->texture_snake_head);
UnloadTexture(ctx->texture_snake_body); UnloadTexture(ctx->texture_snake_body);
game_session_destroy(); ctx->game_instance = NULL;
} }
Presentation_State presentation_state_ingame; Presentation_State presentation_state_ingame;

View File

@@ -5,8 +5,13 @@
#include "simulation/simulation_world.h" #include "simulation/simulation_world.h"
#include "raylib.h" #include "raylib.h"
#include "instance/game_instance.h"
typedef struct { typedef struct {
Game_Instance *game_instance;
double simulation_accumulator;
Simulation_Game_Input prev_local_input;
Camera2D main_camera; Camera2D main_camera;
// Textures. // Textures.
@@ -17,7 +22,7 @@ typedef struct {
Texture2D texture_snake_body; Texture2D texture_snake_body;
// Debug. // Debug.
bool debug_render_match_state; bool debug_draw_session_details;
} Presentation_State_Ingame_Context; } Presentation_State_Ingame_Context;

View File

@@ -2,39 +2,58 @@
#include <stdio.h> #include <stdio.h>
#include <stdbool.h> #include <stdbool.h>
#include <assert.h>
#include "raylib.h" #include "raylib.h"
#include "raygui.h" #include "raygui.h"
#include "session/networking.h"
static void state_enter(Presentation_State *state) { static void state_enter(Presentation_State *state) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context; Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
(void)ctx; (void)ctx;
ctx->mode = Main_Menu_Mode_Home; ctx->mode = Main_Menu_Mode_Home;
// if(ctx->game_session != NULL) {
// if(ctx->game_session->is_singleplayer) {
// }
// else {
// if(ctx->game_session->is_host) {
// networking_stop_hosting(ctx->game_networking);
// }
// else {
// }
// }
// game_session_dispose(ctx->game_session);
// ctx->game_session = NULL;
// }
} }
static void state_tick(Presentation_State *state) { static void state_tick(Presentation_State *state) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context; Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
{ // DEBUG // { // DEBUG
if(IsKeyPressed(KEY_P)) { // if(IsKeyPressed(KEY_P)) {
ctx->is_singleplayer = true; // game_session_init_default_settings(true, &ctx->session_settings);
ctx->mode = Main_Menu_Mode_Game_Setup;
game_session_init_default_settings(ctx->is_singleplayer, &ctx->session_settings);
game_session_create( // assert(ctx->game_session == NULL);
ctx->is_singleplayer, // ctx->game_session = (Game_Session *)calloc(1, sizeof(Game_Session));
!ctx->is_singleplayer,
ctx->session_settings
);
presentation_state_machine_go_to(&presentation_state_ingame); // game_session_init(
} // ctx->game_session,
} // ctx->session_settings
// );
// ctx->ingame_ctx->game_instance = ctx->game_instance;
// presentation_state_machine_go_to(&presentation_state_ingame);
// }
// }
} }
bool editing_width = false;
static void state_render(Presentation_State *state) { static void state_render(Presentation_State *state) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context; Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
@@ -59,17 +78,15 @@ static void state_render(Presentation_State *state) {
break; break;
} }
case Main_Menu_Mode_Singleplayer: { case Main_Menu_Mode_Singleplayer: {
ctx->is_singleplayer = true;
ctx->mode = Main_Menu_Mode_Game_Setup; ctx->mode = Main_Menu_Mode_Game_Setup;
game_session_init_default_settings(ctx->is_singleplayer, &ctx->session_settings); game_session_init_default_settings(false, &ctx->session_settings);
break; break;
} }
case Main_Menu_Mode_Multiplayer: { case Main_Menu_Mode_Multiplayer: {
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Host")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Host")) {
ctx->is_singleplayer = false;
ctx->mode = Main_Menu_Mode_Game_Setup; ctx->mode = Main_Menu_Mode_Game_Setup;
game_session_init_default_settings(ctx->is_singleplayer, &ctx->session_settings); game_session_init_default_settings(true, &ctx->session_settings);
} }
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Join")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Join")) {
ctx->mode = Main_Menu_Mode_Multiplayer_Join; ctx->mode = Main_Menu_Mode_Multiplayer_Join;
@@ -94,18 +111,16 @@ static void state_render(Presentation_State *state) {
// Modify 'ctx->session_settings'. // Modify 'ctx->session_settings'.
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Play")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Play")) {
// Set up the ingame-context and transition to the ingame-state. if(game_instance_host_session(ctx->game_instance, ctx->session_settings)) { // Settings indicate whether or not this is an online or offline game.
// NOTE: SS - We might need to wait before transitioning when playing multiplayer. ctx->ingame_ctx->game_instance = ctx->game_instance;
game_session_create(
ctx->is_singleplayer,
!ctx->is_singleplayer,
ctx->session_settings
);
presentation_state_machine_go_to(&presentation_state_ingame); presentation_state_machine_go_to(&presentation_state_ingame);
} }
else {
printf("Failed to play.\n");
}
}
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Cancel")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Cancel")) {
ctx->mode = ctx->is_singleplayer ? Main_Menu_Mode_Home : Main_Menu_Mode_Multiplayer; ctx->mode = ctx->session_settings.max_players == 1 ? Main_Menu_Mode_Home : Main_Menu_Mode_Multiplayer;
} }
break; break;
@@ -115,6 +130,8 @@ static void state_render(Presentation_State *state) {
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Connect")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Connect")) {
printf("TODO: SS - Connect to session id.\n"); printf("TODO: SS - Connect to session id.\n");
const char *session_id = "TEMP00"; // TEMP
game_instance_join_session(ctx->game_instance, session_id);
} }
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 2), 128, BUTTON_HEIGHT }, "Cancel")) { if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 2), 128, BUTTON_HEIGHT }, "Cancel")) {

View File

@@ -2,7 +2,8 @@
#define PRES_STATE_MAIN_MENU_H #define PRES_STATE_MAIN_MENU_H
#include "states.h" #include "states.h"
#include "session/game_session.h" #include "state_ingame.h"
#include "instance/game_instance.h"
typedef enum { typedef enum {
Main_Menu_Mode_Home, Main_Menu_Mode_Home,
@@ -16,10 +17,13 @@ typedef enum {
typedef struct { typedef struct {
Main_Menu_Mode mode; Main_Menu_Mode mode;
bool is_singleplayer;
Game_Session_Settings session_settings; Game_Session_Settings session_settings;
Game_Instance *game_instance;
bool *should_quit_game; bool *should_quit_game;
Presentation_State_Ingame_Context *ingame_ctx;
} Presentation_State_Main_Menu_Context; } Presentation_State_Main_Menu_Context;
void presentation_state_main_menu_init(Presentation_State_Main_Menu_Context *ctx); void presentation_state_main_menu_init(Presentation_State_Main_Menu_Context *ctx);

View File

@@ -6,22 +6,14 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include "shared/squeue.h" void game_session_init(Game_Session *session, Game_Session_Settings settings) {
assert(session != NULL);
#define INPUT_QUEUE_CAPACITY 8
Game_Session *g_current_session = NULL;
void game_session_create(bool is_singleplayer, bool is_host, Game_Session_Settings settings) {
assert(g_current_session == NULL);
Game_Session *session = (Game_Session *)calloc(1, sizeof(Game_Session));
session->is_singleplayer = is_singleplayer;
session->is_host = is_host;
session->settings = settings; session->settings = settings;
session->simulation_world = simulation_create_world( session->simulation_world = simulation_create_world(
session->settings.seed, session->settings.seed,
session->settings.online,
session->settings.max_players, session->settings.max_players,
session->settings.level_width, session->settings.level_width,
session->settings.level_height session->settings.level_height
@@ -29,63 +21,33 @@ void game_session_create(bool is_singleplayer, bool is_host, Game_Session_Settin
session->players = (Game_Session_Player *)calloc(session->settings.max_players, sizeof(Game_Session_Player)); session->players = (Game_Session_Player *)calloc(session->settings.max_players, sizeof(Game_Session_Player));
session->players_prev = (Game_Session_Player *)calloc(session->settings.max_players, sizeof(Game_Session_Player)); session->players_prev = (Game_Session_Player *)calloc(session->settings.max_players, sizeof(Game_Session_Player));
for(uint16_t i = 0; i < session->settings.max_players; i++) {
assert(squeue_init(
&session->players[i].input_queue,
INPUT_QUEUE_CAPACITY,
sizeof(Simulation_Game_Input)
));
// Intentionally not initializing players_prev's input_queue.
}
{ // TEMP
session->local_player_index = 0;
assert(game_session_create_player(session, &session->local_player_index));
}
session->simulation_accumulator = 0.0f;
g_current_session = session;
printf("New Game_Session created.\n");
} }
void game_session_destroy() { void game_session_dispose(Game_Session *session) {
Game_Session *session = g_current_session;
if(session == NULL) { if(session == NULL) {
return; return;
} }
simulation_destroy_world(&session->simulation_world); simulation_destroy_world(&session->simulation_world);
for(uint16_t i = 0; i < session->settings.max_players; i++) {
squeue_free(&session->players[i].input_queue);
// Intentionally not freeing players_prev's input_queue.
}
free(session->players); free(session->players);
session->players = NULL; session->players = NULL;
free(session->players_prev); free(session->players_prev);
session->players_prev = NULL; session->players_prev = NULL;
free(session); memset(session, 0, sizeof(Game_Session));
g_current_session = NULL;
} }
void game_session_init_default_settings(bool is_singleplayer, Game_Session_Settings *out_settings) { void game_session_init_default_settings(bool online, Game_Session_Settings *out_settings) {
out_settings->seed = 1337; // TODO: SS - Randomize. out_settings->seed = 1337; // TODO: SS - Randomize.
out_settings->online = online;
out_settings->tickrate = 10.0; out_settings->tickrate = 10.0;
out_settings->level_width = 64; out_settings->level_width = 64;
out_settings->level_height = 32; out_settings->level_height = 32;
out_settings->max_players = is_singleplayer ? 1 : 8; out_settings->max_players = online ? 8 : 2;
} }
static void game_session_tick(Game_Session *session) { void game_session_tick(Game_Session *session) {
// Update input.
for(uint16_t i = 0; i < session->settings.max_players; i++) { for(uint16_t i = 0; i < session->settings.max_players; i++) {
Game_Session_Player *session_player = &session->players[i]; Game_Session_Player *session_player = &session->players[i];
@@ -112,17 +74,13 @@ static void game_session_tick(Game_Session *session) {
continue; continue;
} }
Simulation_Game_Input input = {0}; // Update input.
if(squeue_pop(&session_player->input_queue, &input)) {
// We got an input.
simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) { simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
.type = Simulation_Command_Type_Set_Player_Input, .type = Simulation_Command_Type_Set_Player_Input,
.player_id = i, .player_id = i,
.player_input = input, .player_input = session_player->input,
}); });
} }
}
// Tick. // Tick.
simulation_world_tick(&session->simulation_world); simulation_world_tick(&session->simulation_world);
@@ -131,17 +89,6 @@ static void game_session_tick(Game_Session *session) {
memcpy(session->players_prev, session->players, sizeof(Game_Session_Player) * session->settings.max_players); memcpy(session->players_prev, session->players, sizeof(Game_Session_Player) * session->settings.max_players);
} }
void game_session_update(Game_Session *session, const double delta_time) {
session->simulation_accumulator += delta_time;
const double sim_dt = 1.0 / session->settings.tickrate;
while (session->simulation_accumulator >= sim_dt) {
game_session_tick(session);
session->simulation_accumulator -= sim_dt;
}
}
static Game_Session_Player *game_session_get_player_from_id(Game_Session *session, uint16_t player_index) { static Game_Session_Player *game_session_get_player_from_id(Game_Session *session, uint16_t player_index) {
assert(session != NULL); assert(session != NULL);
if(player_index >= session->settings.max_players) { if(player_index >= session->settings.max_players) {
@@ -151,11 +98,19 @@ static Game_Session_Player *game_session_get_player_from_id(Game_Session *sessio
return &session->players[player_index]; return &session->players[player_index];
} }
Game_Session_Player *game_session_get_local_player(Game_Session *session) { uint16_t game_session_get_amount_of_active_players(Game_Session *session) {
assert(session != NULL); assert(session != NULL);
return game_session_get_player_from_id(session, session->local_player_index); uint16_t amount = 0;
}
for(uint16_t i = 0; i < session->settings.max_players; i++) {
Game_Session_Player *player = &session->players[i];
if(player->active) {
amount += 1;
}
}
return amount;
}
bool game_session_create_player(Game_Session *session, uint16_t *out_player_index) { bool game_session_create_player(Game_Session *session, uint16_t *out_player_index) {
assert(session != NULL); assert(session != NULL);
@@ -191,23 +146,10 @@ void game_session_destroy_player(Game_Session *session, uint16_t player_index) {
memset(player, 0, sizeof(Game_Session_Player)); memset(player, 0, sizeof(Game_Session_Player));
} }
bool game_session_enqueue_player_input(Game_Session *session, uint16_t player_index, Simulation_Game_Input input) { bool game_session_set_player_input(Game_Session *session, uint16_t player_index, Simulation_Game_Input input) {
Game_Session_Player *player = game_session_get_player_from_id(session, player_index); Game_Session_Player *player = game_session_get_player_from_id(session, player_index);
assert(player != NULL); assert(player != NULL);
if(simulation_input_equal(player->most_recent_input, input)) { player->input = input;
// Ignore 'input' if it's equal to the most recent input. This way we avoid duplicates.
return false;
}
// TODO: SS - Check if 'input' is a valid "snake-move".
// Inputting right when going left will sadly be "okay" here which will make the game
// feel less responsive.
if(!squeue_push(&player->input_queue, (void *)&input)) {
return false;
}
player->most_recent_input = input;
return true; return true;
} }

View File

@@ -13,54 +13,46 @@
typedef struct { typedef struct {
bool active; bool active;
SQueue input_queue; Simulation_Game_Input input;
Simulation_Game_Input most_recent_input; // Exclusively used to make sure that the input-queue doesn't get filled with duplicated inputs.
} Game_Session_Player; } Game_Session_Player;
typedef struct { typedef struct {
uint32_t seed; uint32_t seed;
double tickrate; bool online;
uint16_t level_width; uint16_t level_width;
uint16_t level_height; uint16_t level_height;
double tickrate;
uint8_t max_players; uint8_t max_players;
// .. // ..
} Game_Session_Settings; } Game_Session_Settings;
typedef struct { typedef struct {
bool is_singleplayer;
bool is_host;
Game_Session_Settings settings; Game_Session_Settings settings;
Simulation_World simulation_world;
double simulation_accumulator;
Game_Session_Player *players; Game_Session_Player *players;
Game_Session_Player *players_prev; Game_Session_Player *players_prev;
uint16_t local_player_index;
// TODO: SS - Local + remote input-queue. Simulation_World simulation_world;
} Game_Session; } Game_Session;
extern Game_Session *g_current_session; void game_session_init(Game_Session *session, Game_Session_Settings settings);
void game_session_dispose(Game_Session *session);
void game_session_create(bool is_singleplayer, bool is_host, Game_Session_Settings settings); void game_session_init_default_settings(bool online, Game_Session_Settings *out_settings);
void game_session_destroy();
void game_session_init_default_settings(bool is_singleplayer, Game_Session_Settings *out_settings); void game_session_tick(Game_Session *session);
void game_session_update(Game_Session *session, const double delta_time); uint16_t game_session_get_amount_of_active_players(Game_Session *session);
Game_Session_Player *game_session_get_local_player(Game_Session *session);
bool game_session_create_player(Game_Session *session, uint16_t *out_player_index); bool game_session_create_player(Game_Session *session, uint16_t *out_player_index);
void game_session_destroy_player(Game_Session *session, uint16_t player_index); void game_session_destroy_player(Game_Session *session, uint16_t player_index);
bool game_session_enqueue_player_input(Game_Session *session, uint16_t player_index, Simulation_Game_Input input); bool game_session_set_player_input(Game_Session *session, uint16_t player_index, Simulation_Game_Input input);
#endif #endif

View File

@@ -1,4 +1,4 @@
#include "MultiplayerApi.h" #include "multiplayer_api.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -15,18 +15,19 @@
typedef struct ListenerNode { typedef struct ListenerNode {
int id; int id;
MultiplayerListener cb; MultiplayerListener cb;
void *user_data; void *context;
struct ListenerNode *next; struct ListenerNode *next;
} ListenerNode; } ListenerNode;
typedef struct ListenerSnapshot { typedef struct ListenerSnapshot {
MultiplayerListener cb; MultiplayerListener cb;
void *user_data; void *context;
} ListenerSnapshot; } ListenerSnapshot;
struct MultiplayerApi { struct MultiplayerApi {
char *server_host; char *server_host;
uint16_t server_port; uint16_t server_port;
char identifier[37];
int sockfd; int sockfd;
char *session_id; char *session_id;
@@ -48,12 +49,19 @@ static void *recv_thread_main(void *arg);
static void process_line(MultiplayerApi *api, const char *line); static void process_line(MultiplayerApi *api, const char *line);
static int start_recv_thread(MultiplayerApi *api); static int start_recv_thread(MultiplayerApi *api);
MultiplayerApi *mp_api_create(const char *server_host, uint16_t server_port) { MultiplayerApi *mp_api_create(const char *server_host, uint16_t server_port, const char *identifier)
{
int len = strlen(identifier);
if (len != 36) {
return NULL;
}
MultiplayerApi *api = (MultiplayerApi *)calloc(1, sizeof(MultiplayerApi)); MultiplayerApi *api = (MultiplayerApi *)calloc(1, sizeof(MultiplayerApi));
if (!api) { if (!api) {
return NULL; return NULL;
} }
if (server_host) { if (server_host) {
api->server_host = strdup(server_host); api->server_host = strdup(server_host);
} else { } else {
@@ -66,6 +74,9 @@ MultiplayerApi *mp_api_create(const char *server_host, uint16_t server_port) {
} }
api->server_port = server_port; api->server_port = server_port;
strncpy(api->identifier, identifier, 37);
api->sockfd = -1; api->sockfd = -1;
api->session_id = NULL; api->session_id = NULL;
api->recv_thread_started = 0; api->recv_thread_started = 0;
@@ -117,6 +128,7 @@ void mp_api_destroy(MultiplayerApi *api) {
} }
int mp_api_host(MultiplayerApi *api, int mp_api_host(MultiplayerApi *api,
json_t *data,
char **out_session, char **out_session,
char **out_clientId, char **out_clientId,
json_t **out_data) { json_t **out_data) {
@@ -129,9 +141,16 @@ int mp_api_host(MultiplayerApi *api,
json_t *root = json_object(); json_t *root = json_object();
if (!root) return MP_API_ERR_IO; if (!root) return MP_API_ERR_IO;
json_object_set_new(root, "session", json_null()); json_object_set_new(root, "identifier", json_string(api->identifier));
json_object_set_new(root, "cmd", json_string("host")); json_object_set_new(root, "cmd", json_string("host"));
json_object_set_new(root, "data", json_object());
json_t *data_copy;
if (data && json_is_object(data)) {
data_copy = json_deep_copy(data);
} else {
data_copy = json_object();
}
json_object_set_new(root, "data", data_copy);
rc = send_json_line(api, root); rc = send_json_line(api, root);
if (rc != MP_API_OK) { if (rc != MP_API_OK) {
@@ -214,6 +233,7 @@ int mp_api_list(MultiplayerApi *api, json_t **out_list)
json_t *root = json_object(); json_t *root = json_object();
if (!root) return MP_API_ERR_IO; if (!root) return MP_API_ERR_IO;
json_object_set_new(root, "identifier", json_string(api->identifier));
json_object_set_new(root, "cmd", json_string("list")); json_object_set_new(root, "cmd", json_string("list"));
rc = send_json_line(api, root); rc = send_json_line(api, root);
@@ -277,6 +297,7 @@ int mp_api_join(MultiplayerApi *api,
json_t *root = json_object(); json_t *root = json_object();
if (!root) return MP_API_ERR_IO; if (!root) return MP_API_ERR_IO;
json_object_set_new(root, "identifier", json_string(api->identifier));
json_object_set_new(root, "session", json_string(sessionId)); json_object_set_new(root, "session", json_string(sessionId));
json_object_set_new(root, "cmd", json_string("join")); json_object_set_new(root, "cmd", json_string("join"));
@@ -371,16 +392,20 @@ int mp_api_join(MultiplayerApi *api,
return joinAccepted ? MP_API_OK : MP_API_ERR_REJECTED; return joinAccepted ? MP_API_OK : MP_API_ERR_REJECTED;
} }
int mp_api_game(MultiplayerApi *api, json_t *data) { int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination) {
if (!api || !data) return MP_API_ERR_ARGUMENT; if (!api || !data) return MP_API_ERR_ARGUMENT;
if (api->sockfd < 0 || !api->session_id) return MP_API_ERR_STATE; if (api->sockfd < 0 || !api->session_id) return MP_API_ERR_STATE;
json_t *root = json_object(); json_t *root = json_object();
if (!root) return MP_API_ERR_IO; if (!root) return MP_API_ERR_IO;
json_object_set_new(root, "identifier", json_string(api->identifier));
json_object_set_new(root, "session", json_string(api->session_id)); json_object_set_new(root, "session", json_string(api->session_id));
json_object_set_new(root, "cmd", json_string("game")); json_object_set_new(root, "cmd", json_string("game"));
if(destination)
json_object_set_new(root, "destination", json_string(destination));
json_t *data_copy; json_t *data_copy;
if (json_is_object(data)) { if (json_is_object(data)) {
data_copy = json_deep_copy(data); data_copy = json_deep_copy(data);
@@ -394,14 +419,14 @@ int mp_api_game(MultiplayerApi *api, json_t *data) {
int mp_api_listen(MultiplayerApi *api, int mp_api_listen(MultiplayerApi *api,
MultiplayerListener cb, MultiplayerListener cb,
void *user_data) { void *context) {
if (!api || !cb) return -1; if (!api || !cb) return -1;
ListenerNode *node = (ListenerNode *)malloc(sizeof(ListenerNode)); ListenerNode *node = (ListenerNode *)malloc(sizeof(ListenerNode));
if (!node) return -1; if (!node) return -1;
node->cb = cb; node->cb = cb;
node->user_data = user_data; node->context = context;
pthread_mutex_lock(&api->lock); pthread_mutex_lock(&api->lock);
node->id = api->next_listener_id++; node->id = api->next_listener_id++;
@@ -505,6 +530,8 @@ static int send_json_line(MultiplayerApi *api, json_t *obj) {
return MP_API_ERR_IO; return MP_API_ERR_IO;
} }
printf("Sending JSON: %s\n", text); // Debug print
size_t len = strlen(text); size_t len = strlen(text);
int fd = api->sockfd; int fd = api->sockfd;
@@ -640,7 +667,7 @@ static void process_line(MultiplayerApi *api, const char *line) {
while (node) { while (node) {
if (node->cb) { if (node->cb) {
snapshot[idx].cb = node->cb; snapshot[idx].cb = node->cb;
snapshot[idx].user_data = node->user_data; snapshot[idx].context = node->context;
idx++; idx++;
} }
node = node->next; node = node->next;
@@ -648,7 +675,7 @@ static void process_line(MultiplayerApi *api, const char *line) {
pthread_mutex_unlock(&api->lock); pthread_mutex_unlock(&api->lock);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
snapshot[i].cb(cmd, (int64_t)msgId, clientId, data_obj, snapshot[i].user_data); snapshot[i].cb(cmd, (int64_t)msgId, clientId, data_obj, snapshot[i].context);
} }
free(snapshot); free(snapshot);

View File

@@ -4,10 +4,6 @@
#include <stdint.h> #include <stdint.h>
#include "jansson/jansson.h" #include "jansson/jansson.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct MultiplayerApi MultiplayerApi; typedef struct MultiplayerApi MultiplayerApi;
/* Callbacktyp för inkommande events från servern. */ /* Callbacktyp för inkommande events från servern. */
@@ -16,7 +12,7 @@ typedef void (*MultiplayerListener)(
int64_t messageId, /* sekventiellt meddelandeID (från host) */ int64_t messageId, /* sekventiellt meddelandeID (från host) */
const char *clientId, /* avsändarens klientID (eller NULL) */ const char *clientId, /* avsändarens klientID (eller NULL) */
json_t *data, /* JSONobjekt med godtycklig speldata */ json_t *data, /* JSONobjekt med godtycklig speldata */
void *user_data /* godtycklig pekare som skickas vidare */ void *context /* godtycklig pekare som skickas vidare */
); );
/* Returkoder */ /* Returkoder */
@@ -31,7 +27,7 @@ enum {
}; };
/* Skapar en ny APIinstans. Returnerar NULL vid fel. */ /* Skapar en ny APIinstans. Returnerar NULL vid fel. */
MultiplayerApi *mp_api_create(const char *server_host, uint16_t server_port); MultiplayerApi *mp_api_create(const char *server_host, uint16_t server_port, const char *identifier);
/* Stänger ner anslutning, stoppar mottagartråd och frigör minne. */ /* Stänger ner anslutning, stoppar mottagartråd och frigör minne. */
void mp_api_destroy(MultiplayerApi *api); void mp_api_destroy(MultiplayerApi *api);
@@ -41,6 +37,7 @@ void mp_api_destroy(MultiplayerApi *api);
anroparen ansvarar för att free:a. out_data (om ej NULL) får ett json_t* anroparen ansvarar för att free:a. out_data (om ej NULL) får ett json_t*
med extra data från servern (anroparen ska json_decref när klart). */ med extra data från servern (anroparen ska json_decref när klart). */
int mp_api_host(MultiplayerApi *api, int mp_api_host(MultiplayerApi *api,
json_t *data,
char **out_session, char **out_session,
char **out_clientId, char **out_clientId,
json_t **out_data); json_t **out_data);
@@ -70,19 +67,15 @@ int mp_api_join(MultiplayerApi *api,
json_t **out_data); json_t **out_data);
/* Skickar ett "game"meddelande med godtycklig JSONdata till sessionen. */ /* Skickar ett "game"meddelande med godtycklig JSONdata till sessionen. */
int mp_api_game(MultiplayerApi *api, json_t *data); int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination);
/* Registrerar en lyssnare för inkommande events. /* Registrerar en lyssnare för inkommande events.
Returnerar ett positivt listenerID, eller 1 vid fel. */ Returnerar ett positivt listenerID, eller 1 vid fel. */
int mp_api_listen(MultiplayerApi *api, int mp_api_listen(MultiplayerApi *api,
MultiplayerListener cb, MultiplayerListener cb,
void *user_data); void *context);
/* Avregistrerar lyssnare. ListenerID är värdet från mp_api_listen. */ /* Avregistrerar lyssnare. ListenerID är värdet från mp_api_listen. */
void mp_api_unlisten(MultiplayerApi *api, int listener_id); void mp_api_unlisten(MultiplayerApi *api, int listener_id);
#ifdef __cplusplus
}
#endif
#endif /* MULTIPLAYER_API_H */ #endif /* MULTIPLAYER_API_H */

104
src/session/networking.c Normal file
View File

@@ -0,0 +1,104 @@
#include "networking.h"
#include <assert.h>
#define ONVO_IDENTIFIER "c2438167-831b-4bf7-8bdc-abcdefabcd00"
static inline bool setup_api(Game_Networking *networking) {
assert(networking != NULL);
if(networking->api != NULL) {
printf("Failed to set up API. Already active.\n");
return false;
}
MultiplayerApi *mp_api = mp_api_create("kontoret.onvo.se", 9001, ONVO_IDENTIFIER);
if(mp_api == NULL) {
printf("Failed to set up API. Could not initialize.\n");
return false;
}
networking->api = mp_api;
return true;
}
bool networking_try_host(Game_Networking *networking, Game_Session *session, Game_Session_Settings settings) {
assert(networking != NULL);
if(!settings.online) {
printf("Failed to host; Game_Session_Settings.online == false, expected it to be true.\n");
return false;
}
if(!setup_api(networking)) {
printf("Failed to host; API is already set up.\n");
return false;
}
char *session_id = NULL;
char *local_client_id = NULL;
json_t *response_data = NULL;
int host_result = mp_api_host(
networking->api,
NULL, // TODO: SS - Send data to server that contains the game-session's settings.
&session_id,
&local_client_id,
&response_data
);
if(host_result != MP_API_OK) {
printf("Failed to host; Got result: %i.\n", host_result);
return false;
}
printf("Started hosting session '%s', local_client_id is '%s'.\n", session_id, local_client_id);
networking->session_id = session_id;
networking->local_client_id = local_client_id;
// TODO: SS - Start listening.
// Set up session.
game_session_init(
session,
settings
);
if(response_data != NULL) {
// TODO: SS - Handle JSON-response.
json_decref(response_data);
}
return true;
}
bool networking_try_join(Game_Networking *networking, const char *session_id) {
assert(networking != NULL);
if(!setup_api(networking)) {
printf("Failed to join; API is already set up.\n");
return false;
}
printf("Trying to join session with id: '%s' ...\n", session_id);
return true;
}
bool networking_stop_hosting(Game_Networking *networking) {
assert(networking != NULL);
if(networking->api == NULL) {
printf("Failed to stop hosting; API is not set up.\n");
return false;
}
mp_api_destroy(networking->api);
networking->api = NULL;
if(networking->session_id != NULL) {
free(networking->session_id);
}
if(networking->local_client_id != NULL) {
free(networking->local_client_id);
}
printf("Stopped hosting.\n");
return true;
}

19
src/session/networking.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef NETWORKING_H
#define NETWORKING_H
#include "multiplayer_api.h"
#include "game_session.h"
typedef struct {
MultiplayerApi *api;
char *session_id;
char *local_client_id;
} Game_Networking;
bool networking_try_host(Game_Networking *networking, Game_Session *session, Game_Session_Settings settings);
bool networking_try_join(Game_Networking *networking, const char *session_id);
bool networking_stop_hosting(Game_Networking *networking);
#endif

View File

@@ -25,6 +25,8 @@ void game_world_destroy(Game_World *world);
bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id); bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id);
void game_world_destroy_entity(Game_World *world, Entity_ID entity_id); void game_world_destroy_entity(Game_World *world, Entity_ID entity_id);
// TODO: SS - "void game_world_spawn_player(Game_World *world, ..)"
Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id); Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id);
#endif #endif

View File

@@ -6,12 +6,14 @@
#define SIM_COMMANDS_PER_PLAYER 4 #define SIM_COMMANDS_PER_PLAYER 4
Simulation_World simulation_create_world(uint32_t seed, uint8_t max_players, uint16_t width, uint16_t height) { Simulation_World simulation_create_world(uint32_t seed, bool online, uint8_t max_players, uint16_t width, uint16_t height) {
Simulation_World w; Simulation_World w;
memset(&w, 0, sizeof(Simulation_World)); memset(&w, 0, sizeof(Simulation_World));
w.tick = 0; w.tick = 0;
w.online = online;
w.game_world = game_world_create(seed, width, height); w.game_world = game_world_create(seed, width, height);
assert(w.game_world != NULL); assert(w.game_world != NULL);
@@ -97,9 +99,7 @@ void simulation_world_tick(Simulation_World *simulation_world) {
switch(simulation_world->match_state) { switch(simulation_world->match_state) {
case Simulation_Match_State_Waiting_To_Start: { case Simulation_Match_State_Waiting_To_Start: {
bool is_singleplayer = simulation_world->max_players == 1; if(!simulation_world->online) { // If not online, just go to Counting_Down.
if(is_singleplayer) {
simulation_world->match_state = Simulation_Match_State_Counting_Down; simulation_world->match_state = Simulation_Match_State_Counting_Down;
} }
else { else {
@@ -230,7 +230,6 @@ void simulation_world_tick(Simulation_World *simulation_world) {
// Figure out what cell we're on and what cell we want to go to. // Figure out what cell we're on and what cell we want to go to.
Grid_Cell *current_cell = grid_get_cell(&gw->grid, entity->x, entity->y); Grid_Cell *current_cell = grid_get_cell(&gw->grid, entity->x, entity->y);
Grid_Cell *start_cell = current_cell;
assert(current_cell != NULL); assert(current_cell != NULL);
Grid_Cell *target_cell = grid_get_cell(&gw->grid, entity->x + dx, entity->y + dy); Grid_Cell *target_cell = grid_get_cell(&gw->grid, entity->x + dx, entity->y + dy);

View File

@@ -2,8 +2,10 @@
#define SIMULATION_WORLD_H #define SIMULATION_WORLD_H
#include <stdlib.h> #include <stdlib.h>
#include <stdbool.h>
#include "shared/game_world.h" #include "shared/game_world.h"
#include "shared/random.h" // Deterministic random.
#include "player.h" #include "player.h"
#include "command.h" #include "command.h"
@@ -17,6 +19,8 @@ typedef enum {
typedef struct { typedef struct {
uint64_t tick; uint64_t tick;
bool online;
Game_World *game_world; Game_World *game_world;
Simulation_Match_State match_state; Simulation_Match_State match_state;
@@ -29,7 +33,7 @@ typedef struct {
uint16_t max_commands; uint16_t max_commands;
} Simulation_World; } Simulation_World;
Simulation_World simulation_create_world(uint32_t seed, uint8_t max_players, uint16_t width, uint16_t height); Simulation_World simulation_create_world(uint32_t seed, bool online, uint8_t max_players, uint16_t width, uint16_t height);
void simulation_destroy_world(Simulation_World *simulation_world); void simulation_destroy_world(Simulation_World *simulation_world);
void simulation_world_tick(Simulation_World *simulation_world); void simulation_world_tick(Simulation_World *simulation_world);