Moved code and assets for the game to a seperate folder. Moved jansson and raylib to third_party.

This commit is contained in:
2025-12-16 11:47:55 +01:00
parent fd2dbf232d
commit 8cdbd5b162
63 changed files with 14 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1,132 @@
#include "game_instance.h"
#include <assert.h>
#define INPUT_QUEUE_CAPACITY 8
void game_instance_init(Game_Instance *instance) {
instance->local_player_id = 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)); // LEAK
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;
}
uint16_t host_player_id = 0xFFFF;
if(!networking_try_host(&instance->game_networking, instance->game_session, settings, &host_player_id)) {
printf("Failed to host session.\n");
free(instance->game_session);
instance->game_session = NULL;
return false;
}
instance->local_player_id = host_player_id;
}
else {
game_session_init(instance->game_session, settings);
for(uint32_t i = 0; i < settings.max_players; i++) {
// When playing locally/offline, create the player(s) here.
assert(game_session_create_player(instance->game_session, &instance->local_player_id)); // TODO: SS - Fix me! Always assigning the newest player to be the active 'local_player_id' which isn't correct.
}
printf("Created %u local players.\n", settings.max_players);
}
return true;
}
bool game_instance_join_session(Game_Instance *instance, const char *session_id) {
assert(instance != NULL);
printf("Trying to join a session '%s' ...\n", session_id);
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;
}
instance->game_session = (Game_Session *)calloc(1, sizeof(Game_Session)); // LEAK
if(!networking_try_join(&instance->game_networking, instance->game_session, session_id)) {
free(instance->game_session);
return false;
}
return true;
}
void game_instance_stop_session(Game_Instance *instance) {
assert(instance != NULL);
assert(instance->game_session != NULL);
Game_Session *session = instance->game_session;
Game_Networking *networking = &instance->game_networking;
printf("Stopping session ...\n");
if(session->settings.online) { // Are we online? Offline?
printf("Session is online (session-id: '%s', host? %s).\n", networking->session_id, networking->is_host ? "true" : "false");
if(networking->is_host) {
// Stop hosting.
networking_stop_hosting(networking);
}
else {
// Disconnect from the host.
assert(false); // TEMP
}
}
else {
printf("Session is offline/local.\n");
}
game_session_dispose(instance->game_session);
free(instance->game_session);
instance->game_session = NULL;
}
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_id; // TODO: SS - We need to modify this to support local multiplayer.
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);
void game_instance_stop_session(Game_Instance *instance);
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

@@ -0,0 +1,355 @@
#include "state_ingame.h"
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include "third_party/raylib.h"
#include "third_party/raygui.h"
#include "session/game_session.h"
#include "shared/wang_hash.h"
#define GRID_CELL_SIZE 8
#define ENTITY_PRESENTATION_Y_OFFSET -2
static void state_enter(Presentation_State *state) {
Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
(void)ctx;
printf("Entered ingame.\n");
printf("Setting up context ...\n");
ctx->main_camera = (Camera2D) {
.offset = (Vector2) { 0, 0 },
.target = (Vector2) { 0, 0 },
.rotation = 0.0f,
.zoom = 3.0f
};
// TODO: SS - Maybe put the textures in an array and index them using an enum?
// These should probably be loaded at start-up instead of here.
ctx->texture_shadow_basic = LoadTexture("game/assets/sprites/spr_shadow_basic.png");
assert(IsTextureValid(ctx->texture_shadow_basic));
ctx->texture_grass = LoadTexture("game/assets/sprites/spr_floor_grass.png");
assert(IsTextureValid(ctx->texture_grass));
ctx->texture_apple = LoadTexture("game/assets/sprites/spr_food_apple.png");
assert(IsTextureValid(ctx->texture_apple));
ctx->texture_snake_head = LoadTexture("game/assets/sprites/spr_snake_head.png");
assert(IsTextureValid(ctx->texture_snake_head));
ctx->texture_snake_body = LoadTexture("game/assets/sprites/spr_snake_body.png");
assert(IsTextureValid(ctx->texture_snake_body));
}
static void state_tick(Presentation_State *state) {
Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
(void)ctx;
Game_Instance *instance = ctx->game_instance;
assert(instance != NULL);
Game_Session *session = instance->game_session;
assert(session != NULL);
{ // Push local input to queue.
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),
.down = IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S),
.right = IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D),
.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;
}
}
const double delta_time = GetFrameTime();
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_id, input);
}
game_session_tick(session);
ctx->simulation_accumulator -= sim_dt;
}
{ // TEMP: SS
if(IsKeyPressed(KEY_TAB)) {
ctx->debug_draw_session_details = !ctx->debug_draw_session_details;
}
if(IsKeyPressed(KEY_ESCAPE)) {
presentation_state_machine_go_to(&presentation_state_main_menu);
}
}
}
uint32_t floor_texture_variant(uint32_t x, uint32_t y, uint32_t seed, uint32_t num_variants) {
return (wang_hash(x * 73856093u ^ y * 19349663u ^ seed)) % num_variants;
}
#define SHADOW_ALPHA 64
static void state_render(Presentation_State *state) {
Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
(void)ctx;
ClearBackground((Color) { 240, 236, 226, 255 });
Game_Instance *instance = ctx->game_instance;
Game_Session *session = instance->game_session;
Simulation_World *sim_world = &session->simulation_world;
assert(sim_world != NULL);
Game_World *world = sim_world->game_world;
assert(world != NULL);
Grid *grid = &world->grid;
uint32_t grid_total_size = grid->width * grid->height;
BeginMode2D(ctx->main_camera); {
// Render the level.
for(uint32_t i = 0; i < grid_total_size; i++) {
uint32_t x = i % grid->width;
uint32_t y = i / grid->width;
uint32_t pres_x = x * GRID_CELL_SIZE;
uint32_t pres_y = y * GRID_CELL_SIZE;
uint32_t random_floor_index = floor_texture_variant(x, y, world->seed, 4);
DrawTextureRec(
ctx->texture_grass,
(Rectangle) { GRID_CELL_SIZE * random_floor_index, 0, GRID_CELL_SIZE, GRID_CELL_SIZE },
(Vector2) { pres_x, pres_y },
WHITE
);
DrawRectangleLines(pres_x, pres_y, GRID_CELL_SIZE, GRID_CELL_SIZE, (Color) { 0, 0, 0, 2 }); // TODO: SS - Let the user customize the alpha locally.
}
for(uint32_t i = 0; i < grid_total_size; i++) {
uint32_t x = i % grid->width;
uint32_t y = i / grid->width;
uint32_t pres_x = x * GRID_CELL_SIZE;
uint32_t pres_y = y * GRID_CELL_SIZE;
Grid_Cell *cell = &grid->cells[i];
if(cell->entity != NULL) {
Entity *entity = cell->entity;
switch(entity->type) {
case Entity_Type_Snake_Head: {
Color tint = (Color) { 255, 135, 102, 255 }; // TODO: SS - Get tint based on player ID.
Vector2 origin = (Vector2) { GRID_CELL_SIZE/2, GRID_CELL_SIZE/2 };
Entity *e = entity;
uint16_t i = 0;
while(e != NULL) { // Loop over all the body-parts of the snake, including the head.
float sin_frequency = 0.0f;
float sin_amplitude = 0.0f;
uint32_t pres_x = e->x * GRID_CELL_SIZE;
uint32_t pres_y = e->y * GRID_CELL_SIZE;
Texture2D *texture = NULL;
if(e->type == Entity_Type_Snake_Head) {
texture = &ctx->texture_snake_head;
}
else {
texture = &ctx->texture_snake_body;
sin_frequency = 4.0f;
sin_amplitude = 1.0f;
}
// Draw shadow.
DrawTextureEx(
ctx->texture_shadow_basic,
(Vector2) { pres_x, pres_y },
0.0f,
1.0f,
(Color) { 0, 0, 0, SHADOW_ALPHA }
);
float rotation = 0.0f;
switch(e->move_direction){
case Entity_Movement_Direction_None: {
rotation = 0.0f;
break;
}
case Entity_Movement_Direction_Up: {
rotation = -90.0f;
break;
}
case Entity_Movement_Direction_Down: {
rotation = 90.0f;
break;
}
case Entity_Movement_Direction_Right: {
rotation = 0.0f;
break;
}
case Entity_Movement_Direction_Left: {
rotation = 180.0f;
break;
}
}
DrawTexturePro(
*texture,
(Rectangle) { // Source.
0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE
},
(Rectangle) { // Destination.
pres_x + origin.x,
pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - (sin((GetTime() + (float)(i) / 4.0) * sin_frequency)) * sin_amplitude,
GRID_CELL_SIZE,
GRID_CELL_SIZE
},
origin, // Origin.
rotation, // Rotation.
tint // Tint.
);
e = game_world_try_get_entity_by_id(world, e->child);
i += 1;
}
// TODO: SS - Don't draw player-name if playing by yourself.
// TODO: SS - Don't draw your own player-name, only others.
{ // Draw player-name.
const char *player_name = "player"; // NOTE: SS - Hardcoded.
const uint32_t font_size = 8;
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 });
}
break;
}
case Entity_Type_Snake_Body: {
break;
}
case Entity_Type_Food: {
uint32_t flash = ((sin(GetTime() * 8) + 1)/2) * 32;
Color tint = (Color) { 255-flash, 255-flash, 255-flash, 255 };
Vector2 origin = (Vector2) { GRID_CELL_SIZE/2, GRID_CELL_SIZE/2 };
// Draw shadow.
DrawTextureEx(
ctx->texture_shadow_basic,
(Vector2) { pres_x, pres_y },
0.0f,
1.0f,
(Color) { 0, 0, 0, SHADOW_ALPHA }
);
DrawTexturePro(
ctx->texture_apple,
(Rectangle) { // Source.
0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE
},
(Rectangle) { // Destination.
pres_x + origin.x,
pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - ((sin((GetTime() + (x + y)) * 12) + 1)/2) * 1,
GRID_CELL_SIZE,
GRID_CELL_SIZE
},
origin, // Origin.
0, // Rotation.
tint // Tint.
);
break;
}
}
}
}
// TODO: SS - Switch on 'sim_world->match_state'.
if(ctx->debug_draw_session_details) {
char buf[2048];
// Add generic info about the game to the buffer.
uint32_t i = snprintf(&buf[0], sizeof(buf),
"Match-state: %i\n"
"Tick: %lu\n"
"\n"
"Seed: %u\n"
"Level: %ux%u\n"
"Tickrate: %.2f\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
);
// Now, write info specific to whether you're online or offline.
if(session->settings.online) {
Game_Networking *networking = &instance->game_networking;
snprintf(&buf[i], sizeof(buf),
"\n"
"[Online]\n"
"Host: %s\n"
"Session-id: %s\n"
"Client-id: %s\n"
"\n"
"Local player: %u\n"
"Player count: %u / %u\n"
,
networking->is_host ? "true" : "false",
networking->session_id,
networking->local_client_id,
instance->local_player_id,
game_session_get_amount_of_active_players(session), sim_world->max_players
);
}
// Finally, draw the buffer to the screen (with nice shadowing!).
DrawText(buf, 17, 17, 8, BLACK);
DrawText(buf, 16, 16, 8, WHITE);
}
}
EndMode2D();
}
static void state_exit(Presentation_State *state) {
Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
(void)ctx;
printf("Exiting ingame\n");
UnloadTexture(ctx->texture_shadow_basic);
UnloadTexture(ctx->texture_grass);
UnloadTexture(ctx->texture_apple);
UnloadTexture(ctx->texture_snake_head);
UnloadTexture(ctx->texture_snake_body);
game_instance_stop_session(ctx->game_instance);
ctx->game_instance = NULL;
}
Presentation_State presentation_state_ingame;
void presentation_state_ingame_init(Presentation_State_Ingame_Context *ctx) {
presentation_state_ingame = (Presentation_State) {
.name = "Ingame",
.context = (void *)ctx,
.enter = state_enter,
.tick = state_tick,
.render = state_render,
.exit = state_exit
};
}

View File

@@ -0,0 +1,31 @@
#ifndef PRES_STATE_INGAME_H
#define PRES_STATE_INGAME_H
#include "states.h"
#include "simulation/simulation_world.h"
#include "third_party/raylib.h"
#include "instance/game_instance.h"
typedef struct {
Game_Instance *game_instance;
double simulation_accumulator;
Simulation_Game_Input prev_local_input;
Camera2D main_camera;
// Textures.
Texture2D texture_shadow_basic;
Texture2D texture_grass;
Texture2D texture_apple;
Texture2D texture_snake_head;
Texture2D texture_snake_body;
// Debug.
bool debug_draw_session_details;
} Presentation_State_Ingame_Context;
void presentation_state_ingame_init(Presentation_State_Ingame_Context *ctx);
#endif

View File

@@ -0,0 +1,48 @@
#include "states.h"
#include <stdio.h>
#include <assert.h>
Presentation_State_Machine presentation_state_machine = (Presentation_State_Machine) {
.current = NULL
};
Presentation_State presentation_state_create(
const char *name,
void *context,
Presentation_State_Enter_Callback enter,
Presentation_State_Tick_Callback tick,
Presentation_State_Render_Callback render,
Presentation_State_Exit_Callback exit
) {
return (Presentation_State) {
.name = name,
.context = context,
.enter = enter,
.tick = tick,
.render = render,
.exit = exit,
};
}
bool presentation_state_machine_go_to(Presentation_State *target) {
if(presentation_state_machine.current == target) {
return false;
}
if(presentation_state_machine.current != NULL) {
assert(presentation_state_machine.current->exit != NULL);
presentation_state_machine.current->exit(presentation_state_machine.current);
}
presentation_state_machine.current = target;
if(presentation_state_machine.current == NULL) {
return true; // Return true here even though it's NULL. This is nice for when we want to exit the program and the current state.
}
assert(presentation_state_machine.current->enter != NULL);
presentation_state_machine.current->enter(presentation_state_machine.current);
return true;
}

View File

@@ -0,0 +1,40 @@
#ifndef PRES_STATE_MACHINE_H
#define PRES_STATE_MACHINE_H
#include <stdbool.h>
typedef struct Presentation_State Presentation_State;
typedef void (*Presentation_State_Enter_Callback)(Presentation_State *state);
typedef void (*Presentation_State_Tick_Callback)(Presentation_State *state);
typedef void (*Presentation_State_Render_Callback)(Presentation_State *state);
typedef void (*Presentation_State_Exit_Callback)(Presentation_State *state);
struct Presentation_State {
const char *name;
void *context;
Presentation_State_Enter_Callback enter;
Presentation_State_Tick_Callback tick;
Presentation_State_Render_Callback render;
Presentation_State_Exit_Callback exit;
};
Presentation_State presentation_state_create(
const char *name,
void *context,
Presentation_State_Enter_Callback enter,
Presentation_State_Tick_Callback tick,
Presentation_State_Render_Callback render,
Presentation_State_Exit_Callback exit
);
typedef struct {
Presentation_State *current;
} Presentation_State_Machine;
extern Presentation_State_Machine presentation_state_machine;
bool presentation_state_machine_go_to(Presentation_State *target);
#endif

View File

@@ -0,0 +1,189 @@
#include "state_main_menu.h"
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include "third_party/raylib.h"
#include "third_party/raygui.h"
#include "session/networking.h"
static void state_enter(Presentation_State *state) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
(void)ctx;
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) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
// { // DEBUG
// if(IsKeyPressed(KEY_P)) {
// game_session_init_default_settings(true, &ctx->session_settings);
// assert(ctx->game_session == NULL);
// ctx->game_session = (Game_Session *)calloc(1, sizeof(Game_Session));
// 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);
// }
// }
}
static void state_render(Presentation_State *state) {
Presentation_State_Main_Menu_Context *ctx = (Presentation_State_Main_Menu_Context *)state->context;
ClearBackground((Color) {
230, 204, 138, 255
});
#define BUTTON_HEIGHT 32
switch(ctx->mode) {
case Main_Menu_Mode_Home: {
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Singleplayer")) {
ctx->mode = Main_Menu_Mode_Singleplayer;
}
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Multiplayer")) {
ctx->mode = Main_Menu_Mode_Multiplayer;
}
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 2), 128, BUTTON_HEIGHT }, "Quit")) {
*(ctx->should_quit_game) = true;
}
break;
}
case Main_Menu_Mode_Singleplayer: {
ctx->mode = Main_Menu_Mode_Game_Setup;
game_session_init_default_settings(false, &ctx->session_settings);
break;
}
case Main_Menu_Mode_Multiplayer: {
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Host")) {
ctx->mode = Main_Menu_Mode_Game_Setup;
game_session_init_default_settings(true, &ctx->session_settings);
}
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 1), 128, BUTTON_HEIGHT }, "Join")) {
ctx->mode = Main_Menu_Mode_Multiplayer_Join;
memset(&ctx->session_id_input, 0, sizeof(ctx->session_id_input));
ctx->session_id_input[0] = 'A';
ctx->session_id_input[1] = 'B';
ctx->session_id_input[2] = 'C';
ctx->session_id_input[3] = 'D';
ctx->session_id_input[4] = 'E';
ctx->session_id_input[5] = 'F';
}
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 2), 128, BUTTON_HEIGHT }, "Back to Main Menu")) {
ctx->mode = Main_Menu_Mode_Home;
}
break;
}
case Main_Menu_Mode_Game_Setup: {
// TODO: SS - Add options for the game here so players can customize..
// - public or locked session (unlock with password?)
// - max-players
// - game-speed (tickrate)
// - match-time
// - map
// - seed
// - obstacles
// - power-ups
// etc..
// Modify 'ctx->session_settings'.
if (GuiButton((Rectangle){ 64, 64 + (BUTTON_HEIGHT * 0), 128, BUTTON_HEIGHT }, "Play")) {
if(game_instance_host_session(ctx->game_instance, ctx->session_settings)) { // Settings indicate whether or not this is an online or offline game.
ctx->ingame_ctx->game_instance = ctx->game_instance;
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")) {
ctx->mode = ctx->session_settings.max_players == 1 ? Main_Menu_Mode_Home : Main_Menu_Mode_Multiplayer;
}
break;
}
case Main_Menu_Mode_Multiplayer_Join: {
// TODO: SS - Session-browser? Shows all available sessions and you can just press it to join? Maybe be prompted with a password if it's locked?
int pressed_button_index = GuiTextInputBox(
(Rectangle) { 64, 64, 256, 128 }, // Bounds
"Session-ID", // Title
NULL, // Message
"Join", // Buttons
&ctx->session_id_input[0], // Text
sizeof(ctx->session_id_input) + 1, // Text max size
NULL // secret view active
);
if(pressed_button_index >= 0) {
if(pressed_button_index == 0) {
ctx->mode = Main_Menu_Mode_Multiplayer;
}
else if(pressed_button_index == 1) {
if(game_instance_join_session(ctx->game_instance, &ctx->session_id_input[0])) {
// Ok!
ctx->ingame_ctx->game_instance = ctx->game_instance;
presentation_state_machine_go_to(&presentation_state_ingame);
}
else {
printf("Failed to join session '%s'.\n", &ctx->session_id_input[0]);
}
}
else {
assert(false);
}
}
break;
}
}
}
static void state_exit(Presentation_State *state) {
(void)state;
printf("Exited main menu\n");
}
Presentation_State presentation_state_main_menu;
void presentation_state_main_menu_init(Presentation_State_Main_Menu_Context *ctx) {
presentation_state_main_menu = (Presentation_State) {
.name = "Main Menu",
.context = (void *)ctx,
.enter = state_enter,
.tick = state_tick,
.render = state_render,
.exit = state_exit
};
}

View File

@@ -0,0 +1,33 @@
#ifndef PRES_STATE_MAIN_MENU_H
#define PRES_STATE_MAIN_MENU_H
#include "states.h"
#include "state_ingame.h"
#include "instance/game_instance.h"
typedef enum {
Main_Menu_Mode_Home,
Main_Menu_Mode_Singleplayer,
Main_Menu_Mode_Multiplayer,
Main_Menu_Mode_Game_Setup,
Main_Menu_Mode_Multiplayer_Join,
} Main_Menu_Mode;
typedef struct {
Main_Menu_Mode mode;
Game_Session_Settings session_settings;
Game_Instance *game_instance;
bool *should_quit_game;
Presentation_State_Ingame_Context *ingame_ctx;
char session_id_input[6];
} Presentation_State_Main_Menu_Context;
void presentation_state_main_menu_init(Presentation_State_Main_Menu_Context *ctx);
#endif

View File

@@ -0,0 +1,10 @@
#ifndef PRES_STATES_H
#define PRES_STATES_H
#include "state_machine.h"
extern Presentation_State presentation_state_main_menu;
extern Presentation_State presentation_state_ingame;
#endif

View File

@@ -0,0 +1,155 @@
#include "game_session.h"
#include <stddef.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
void game_session_init(Game_Session *session, Game_Session_Settings settings) {
assert(session != NULL);
session->settings = settings;
session->simulation_world = simulation_create_world(
session->settings.seed,
session->settings.online,
session->settings.max_players,
session->settings.level_width,
session->settings.level_height
);
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));
}
void game_session_dispose(Game_Session *session) {
if(session == NULL) {
return;
}
simulation_destroy_world(&session->simulation_world);
free(session->players);
session->players = NULL;
free(session->players_prev);
session->players_prev = NULL;
memset(session, 0, sizeof(Game_Session));
}
void game_session_init_default_settings(bool online, Game_Session_Settings *out_settings) {
out_settings->seed = 1337; // TODO: SS - Randomize.
out_settings->online = online;
out_settings->tickrate = 10.0;
out_settings->level_width = 64;
out_settings->level_height = 32;
out_settings->max_players = online ? 8 : 2;
}
void game_session_tick(Game_Session *session) {
for(uint16_t i = 0; i < session->settings.max_players; i++) {
Game_Session_Player *session_player = &session->players[i];
{ // Check if any players have joined/disconnected by comparing against the previous session_player.
Game_Session_Player *session_player_prev = &session->players_prev[i];
bool was_active = session_player_prev->active;
bool is_active = session_player->active;
if (!was_active && is_active) {
simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
.type = Simulation_Command_Type_Add_Player,
.player_id = i
});
} else if (was_active && !is_active) {
simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
.type = Simulation_Command_Type_Remove_Player,
.player_id = i
});
}
}
if(!session_player->active) {
continue;
}
// Update input.
simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
.type = Simulation_Command_Type_Set_Player_Input,
.player_id = i,
.player_input = session_player->input,
});
}
// Tick.
simulation_world_tick(&session->simulation_world);
// Copy 'players' from session to 'players_prev'.
memcpy(session->players_prev, session->players, sizeof(Game_Session_Player) * session->settings.max_players);
}
static Game_Session_Player *game_session_get_player_from_id(Game_Session *session, uint16_t player_index) {
assert(session != NULL);
if(player_index >= session->settings.max_players) {
return NULL;
}
return &session->players[player_index];
}
uint16_t game_session_get_amount_of_active_players(Game_Session *session) {
assert(session != NULL);
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) {
assert(session != NULL);
int32_t found_index = -1;
for(uint16_t i = 0; i < session->settings.max_players; i++) {
Game_Session_Player *session_player = &session->players[i];
assert(session_player != NULL);
if(!session_player->active) {
found_index = i;
session_player->active = true;
break;
}
}
if(found_index < 0) {
return false;
}
*out_player_index = (uint16_t)found_index;
return true;
}
void game_session_destroy_player(Game_Session *session, uint16_t player_index) {
assert(session != NULL);
assert(player_index < session->settings.max_players);
Game_Session_Player *player = &session->players[player_index];
assert(player != NULL);
assert(player->active);
memset(player, 0, sizeof(Game_Session_Player));
}
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);
assert(player != NULL);
player->input = input;
return true;
}

View File

@@ -0,0 +1,58 @@
#ifndef GAME_SESSION_H
#define GAME_SESSION_H
#include <stdint.h>
#include <stdbool.h>
#include "shared/squeue.h"
#include "simulation/simulation_world.h"
#include "simulation/input.h"
#include "simulation/player.h"
typedef struct {
bool active;
Simulation_Game_Input input;
} Game_Session_Player;
typedef struct {
uint32_t seed;
bool online;
uint16_t level_width;
uint16_t level_height;
double tickrate;
uint8_t max_players;
// ..
} Game_Session_Settings;
typedef struct {
Game_Session_Settings settings;
Game_Session_Player *players;
Game_Session_Player *players_prev;
Simulation_World simulation_world;
} Game_Session;
void game_session_init(Game_Session *session, Game_Session_Settings settings);
void game_session_dispose(Game_Session *session);
void game_session_init_default_settings(bool online, Game_Session_Settings *out_settings);
void game_session_tick(Game_Session *session);
uint16_t game_session_get_amount_of_active_players(Game_Session *session);
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);
bool game_session_set_player_input(Game_Session *session, uint16_t player_index, Simulation_Game_Input input);
#endif

View File

@@ -0,0 +1,758 @@
#include "multiplayer_api.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
typedef struct ListenerNode {
int id;
MultiplayerListener cb;
void *context;
struct ListenerNode *next;
} ListenerNode;
typedef struct ListenerSnapshot {
MultiplayerListener cb;
void *context;
} ListenerSnapshot;
struct MultiplayerApi {
char *server_host;
uint16_t server_port;
char identifier[37];
int sockfd;
char *session_id;
pthread_t recv_thread;
int recv_thread_started;
int running;
pthread_mutex_t lock;
ListenerNode *listeners;
int next_listener_id;
};
static int connect_to_server(const char *host, uint16_t port);
static int ensure_connected(MultiplayerApi *api);
static int send_all(int fd, const char *buf, size_t len);
static int send_json_line(MultiplayerApi *api, json_t *obj); /* tar över ägarskap */
static int read_line(int fd, char **out_line);
static void *recv_thread_main(void *arg);
static void process_line(MultiplayerApi *api, const char *line);
static int start_recv_thread(MultiplayerApi *api);
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));
if (!api) {
return NULL;
}
if (server_host) {
api->server_host = strdup(server_host);
} else {
api->server_host = strdup("127.0.0.1");
}
if (!api->server_host) {
free(api);
return NULL;
}
api->server_port = server_port;
strncpy(api->identifier, identifier, 37);
api->sockfd = -1;
api->session_id = NULL;
api->recv_thread_started = 0;
api->running = 0;
api->listeners = NULL;
api->next_listener_id = 1;
if (pthread_mutex_init(&api->lock, NULL) != 0) {
free(api->server_host);
free(api);
return NULL;
}
return api;
}
void mp_api_destroy(MultiplayerApi *api) {
if (!api) return;
if (api->recv_thread_started && api->sockfd >= 0) {
shutdown(api->sockfd, SHUT_RDWR);
pthread_join(api->recv_thread, NULL);
}
if (api->sockfd >= 0) {
close(api->sockfd);
}
pthread_mutex_lock(&api->lock);
ListenerNode *node = api->listeners;
api->listeners = NULL;
pthread_mutex_unlock(&api->lock);
while (node) {
ListenerNode *next = node->next;
free(node);
node = next;
}
if (api->session_id) {
free(api->session_id);
}
if (api->server_host) {
free(api->server_host);
}
pthread_mutex_destroy(&api->lock);
free(api);
}
int mp_api_host(MultiplayerApi *api,
json_t *data,
char **out_session,
char **out_clientId,
json_t **out_data) {
if (!api) return MP_API_ERR_ARGUMENT;
if (api->session_id) return MP_API_ERR_STATE;
int rc = ensure_connected(api);
if (rc != MP_API_OK) return rc;
json_t *root = json_object();
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("host"));
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);
if (rc != MP_API_OK) {
return rc;
}
char *line = NULL;
rc = read_line(api->sockfd, &line);
if (rc != MP_API_OK) {
return rc;
}
json_error_t jerr;
json_t *resp = json_loads(line, 0, &jerr);
free(line);
if (!resp || !json_is_object(resp)) {
if (resp) json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *cmd_val = json_object_get(resp, "cmd");
if (!json_is_string(cmd_val) || strcmp(json_string_value(cmd_val), "host") != 0) {
json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *sess_val = json_object_get(resp, "session");
if (!json_is_string(sess_val)) {
json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
const char *session = json_string_value(sess_val);
json_t *cid_val = json_object_get(resp, "clientId");
const char *clientId = json_is_string(cid_val) ? json_string_value(cid_val) : NULL;
json_t *data_val = json_object_get(resp, "data");
json_t *data_obj = NULL;
if (json_is_object(data_val)) {
data_obj = data_val;
json_incref(data_obj);
}
api->session_id = strdup(session);
if (!api->session_id) {
if (data_obj) json_decref(data_obj);
json_decref(resp);
return MP_API_ERR_IO;
}
if (out_session) {
*out_session = strdup(session);
}
if (out_clientId && clientId) {
*out_clientId = strdup(clientId);
}
if (out_data) {
*out_data = data_obj;
} else if (data_obj) {
json_decref(data_obj);
}
json_decref(resp);
rc = start_recv_thread(api);
if (rc != MP_API_OK) {
return rc;
}
return MP_API_OK;
}
int mp_api_list(MultiplayerApi *api, json_t **out_list)
{
if (!api || !out_list) return MP_API_ERR_ARGUMENT;
int rc = ensure_connected(api);
if (rc != MP_API_OK) return rc;
json_t *root = json_object();
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"));
rc = send_json_line(api, root);
if (rc != MP_API_OK) {
return rc;
}
char *line = NULL;
rc = read_line(api->sockfd, &line);
if (rc != MP_API_OK) {
return rc;
}
printf("Received line: %s\n", line); // Debug print
json_error_t jerr;
json_t *resp = json_loads(line, 0, &jerr);
free(line);
if (!resp || !json_is_object(resp)) {
if (resp) json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *cmd_val = json_object_get(resp, "cmd");
if (!json_is_string(cmd_val) || strcmp(json_string_value(cmd_val), "list") != 0) {
json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *list_val = json_object_get(resp, "data");
if (!json_is_object(list_val)) {
json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *list_obj = json_object_get(list_val, "list");
if (!json_is_array(list_obj)) {
json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
*out_list = list_obj;
json_incref(*out_list);
json_decref(resp);
return MP_API_OK;
}
int mp_api_join(MultiplayerApi *api,
const char *sessionId,
json_t *data,
char **out_session,
char **out_clientId,
json_t **out_data) {
if (!api || !sessionId) return MP_API_ERR_ARGUMENT;
if (api->session_id) return MP_API_ERR_STATE;
int rc = ensure_connected(api);
if (rc != MP_API_OK) return rc;
json_t *root = json_object();
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, "cmd", json_string("join"));
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);
if (rc != MP_API_OK) {
return rc;
}
char *line = NULL;
rc = read_line(api->sockfd, &line);
if (rc != MP_API_OK) {
return rc;
}
json_error_t jerr;
json_t *resp = json_loads(line, 0, &jerr);
free(line);
if (!resp || !json_is_object(resp)) {
if (resp) json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *cmd_val = json_object_get(resp, "cmd");
if (!json_is_string(cmd_val) || strcmp(json_string_value(cmd_val), "join") != 0) {
json_decref(resp);
return MP_API_ERR_PROTOCOL;
}
json_t *sess_val = json_object_get(resp, "session");
const char *session = NULL;
if (json_is_string(sess_val)) {
session = json_string_value(sess_val);
}
json_t *cid_val = json_object_get(resp, "clientId");
const char *clientId = json_is_string(cid_val) ? json_string_value(cid_val) : NULL;
json_t *data_val = json_object_get(resp, "data");
json_t *data_obj = NULL;
if (json_is_object(data_val)) {
data_obj = data_val;
json_incref(data_obj);
}
int joinAccepted = 1;
if (data_obj) {
json_t *status_val = json_object_get(data_obj, "status");
if (json_is_string(status_val) &&
strcmp(json_string_value(status_val), "error") == 0) {
joinAccepted = 0;
}
}
if (joinAccepted && session) {
api->session_id = strdup(session);
if (!api->session_id) {
if (data_obj) json_decref(data_obj);
json_decref(resp);
return MP_API_ERR_IO;
}
}
if (out_session && session) {
*out_session = strdup(session);
}
if (out_clientId && clientId) {
*out_clientId = strdup(clientId);
}
if (out_data) {
*out_data = data_obj;
} else if (data_obj) {
json_decref(data_obj);
}
json_decref(resp);
if (joinAccepted && api->session_id) {
rc = start_recv_thread(api);
if (rc != MP_API_OK) {
return rc;
}
}
return joinAccepted ? MP_API_OK : MP_API_ERR_REJECTED;
}
int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination) {
if (!api || !data) return MP_API_ERR_ARGUMENT;
if (api->sockfd < 0 || !api->session_id) return MP_API_ERR_STATE;
json_t *root = json_object();
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, "cmd", json_string("game"));
if(destination)
json_object_set_new(root, "destination", json_string(destination));
json_t *data_copy;
if (json_is_object(data)) {
data_copy = json_deep_copy(data);
} else {
data_copy = json_object();
}
json_object_set_new(root, "data", data_copy);
return send_json_line(api, root);
}
int mp_api_listen(MultiplayerApi *api,
MultiplayerListener cb,
void *context) {
if (!api || !cb) return -1;
ListenerNode *node = (ListenerNode *)malloc(sizeof(ListenerNode));
if (!node) return -1;
node->cb = cb;
node->context = context;
pthread_mutex_lock(&api->lock);
node->id = api->next_listener_id++;
node->next = api->listeners;
api->listeners = node;
pthread_mutex_unlock(&api->lock);
return node->id;
}
void mp_api_unlisten(MultiplayerApi *api, int listener_id) {
if (!api || listener_id <= 0) return;
pthread_mutex_lock(&api->lock);
ListenerNode *prev = NULL;
ListenerNode *cur = api->listeners;
while (cur) {
if (cur->id == listener_id) {
if (prev) {
prev->next = cur->next;
} else {
api->listeners = cur->next;
}
free(cur);
break;
}
prev = cur;
cur = cur->next;
}
pthread_mutex_unlock(&api->lock);
}
/* --- Interna hjälpfunktioner --- */
static int connect_to_server(const char *host, uint16_t port) {
if (!host) host = "127.0.0.1";
char port_str[16];
snprintf(port_str, sizeof(port_str), "%u", (unsigned int)port);
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* IPv4 eller IPv6 */
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *res = NULL;
int err = getaddrinfo(host, port_str, &hints, &res);
if (err != 0) {
return -1;
}
int fd = -1;
for (struct addrinfo *rp = res; rp != NULL; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd == -1) continue;
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
break;
}
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd;
}
static int ensure_connected(MultiplayerApi *api) {
if (!api) return MP_API_ERR_ARGUMENT;
if (api->sockfd >= 0) return MP_API_OK;
int fd = connect_to_server(api->server_host, api->server_port);
if (fd < 0) {
return MP_API_ERR_CONNECT;
}
api->sockfd = fd;
return MP_API_OK;
}
static int send_all(int fd, const char *buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, buf + sent, len - sent, 0);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (n == 0) {
return -1;
}
sent += (size_t)n;
}
return 0;
}
static int send_json_line(MultiplayerApi *api, json_t *obj) {
if (!api || api->sockfd < 0 || !obj) return MP_API_ERR_ARGUMENT;
char *text = json_dumps(obj, JSON_COMPACT);
if (!text) {
json_decref(obj);
return MP_API_ERR_IO;
}
printf("Sending JSON: %s\n", text); // Debug print
size_t len = strlen(text);
int fd = api->sockfd;
int rc = 0;
if (send_all(fd, text, len) != 0 || send_all(fd, "\n", 1) != 0) {
rc = MP_API_ERR_IO;
}
free(text);
json_decref(obj);
return rc;
}
static int read_line(int fd, char **out_line) {
if (!out_line) return MP_API_ERR_ARGUMENT;
size_t cap = 256;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) return MP_API_ERR_IO;
for (;;) {
char c;
ssize_t n = recv(fd, &c, 1, 0);
if (n < 0) {
if (errno == EINTR) continue;
free(buf);
return MP_API_ERR_IO;
}
if (n == 0) {
free(buf);
return MP_API_ERR_IO;
}
if (c == '\n') {
break;
}
if (len + 1 >= cap) {
cap *= 2;
char *tmp = (char *)realloc(buf, cap);
if (!tmp) {
free(buf);
return MP_API_ERR_IO;
}
buf = tmp;
}
buf[len++] = c;
}
buf[len] = '\0';
*out_line = buf;
return MP_API_OK;
}
static void process_line(MultiplayerApi *api, const char *line) {
if (!api || !line || !*line) return;
json_error_t jerr;
json_t *root = json_loads(line, 0, &jerr);
if (!root || !json_is_object(root)) {
if (root) json_decref(root);
return;
}
json_t *cmd_val = json_object_get(root, "cmd");
if (!json_is_string(cmd_val)) {
json_decref(root);
return;
}
const char *cmd = json_string_value(cmd_val);
if (!cmd) {
json_decref(root);
return;
}
if (strcmp(cmd, "joined") != 0 &&
strcmp(cmd, "leaved") != 0 &&
strcmp(cmd, "game") != 0) {
json_decref(root);
return;
}
json_int_t msgId = 0;
json_t *mid_val = json_object_get(root, "messageId");
if (json_is_integer(mid_val)) {
msgId = json_integer_value(mid_val);
}
const char *clientId = NULL;
json_t *cid_val = json_object_get(root, "clientId");
if (json_is_string(cid_val)) {
clientId = json_string_value(cid_val);
}
json_t *data_val = json_object_get(root, "data");
json_t *data_obj;
if (json_is_object(data_val)) {
data_obj = data_val;
json_incref(data_obj);
} else {
data_obj = json_object();
}
pthread_mutex_lock(&api->lock);
int count = 0;
ListenerNode *node = api->listeners;
while (node) {
if (node->cb) count++;
node = node->next;
}
if (count == 0) {
pthread_mutex_unlock(&api->lock);
json_decref(data_obj);
json_decref(root);
return;
}
ListenerSnapshot *snapshot = (ListenerSnapshot *)malloc(sizeof(ListenerSnapshot) * count);
if (!snapshot) {
pthread_mutex_unlock(&api->lock);
json_decref(data_obj);
json_decref(root);
return;
}
int idx = 0;
node = api->listeners;
while (node) {
if (node->cb) {
snapshot[idx].cb = node->cb;
snapshot[idx].context = node->context;
idx++;
}
node = node->next;
}
pthread_mutex_unlock(&api->lock);
for (int i = 0; i < count; ++i) {
snapshot[i].cb(cmd, (int64_t)msgId, clientId, data_obj, snapshot[i].context);
}
free(snapshot);
json_decref(data_obj);
json_decref(root);
}
static void *recv_thread_main(void *arg) {
MultiplayerApi *api = (MultiplayerApi *)arg;
char buffer[1024];
char *acc = NULL;
size_t acc_len = 0;
size_t acc_cap = 0;
while (1) {
ssize_t n = recv(api->sockfd, buffer, sizeof(buffer), 0);
if (n <= 0) {
break;
}
for (ssize_t i = 0; i < n; ++i) {
char ch = buffer[i];
if (ch == '\n') {
if (acc_len > 0) {
char *line = (char *)malloc(acc_len + 1);
if (!line) {
acc_len = 0;
continue;
}
memcpy(line, acc, acc_len);
line[acc_len] = '\0';
acc_len = 0;
process_line(api, line);
free(line);
} else {
acc_len = 0;
}
} else {
if (acc_len + 1 >= acc_cap) {
size_t new_cap = acc_cap == 0 ? 256 : acc_cap * 2;
char *tmp = (char *)realloc(acc, new_cap);
if (!tmp) {
free(acc);
acc = NULL;
acc_len = 0;
acc_cap = 0;
break;
}
acc = tmp;
acc_cap = new_cap;
}
acc[acc_len++] = ch;
}
}
}
if (acc) {
free(acc);
}
return NULL;
}
static int start_recv_thread(MultiplayerApi *api) {
if (!api) return MP_API_ERR_ARGUMENT;
if (api->recv_thread_started) {
return MP_API_OK;
}
api->running = 1;
int rc = pthread_create(&api->recv_thread, NULL, recv_thread_main, api);
if (rc != 0) {
api->running = 0;
return MP_API_ERR_IO;
}
api->recv_thread_started = 1;
return MP_API_OK;
}

View File

@@ -0,0 +1,81 @@
#ifndef MULTIPLAYER_API_H
#define MULTIPLAYER_API_H
#include <stdint.h>
#include "third_party/jansson/jansson.h"
typedef struct MultiplayerApi MultiplayerApi;
/* Callbacktyp för inkommande events från servern. */
typedef void (*MultiplayerListener)(
const char *event, /* "joined", "leaved", "game" */
int64_t messageId, /* sekventiellt meddelandeID (från host) */
const char *clientId, /* avsändarens klientID (eller NULL) */
json_t *data, /* JSONobjekt med godtycklig speldata */
void *context /* godtycklig pekare som skickas vidare */
);
/* Returkoder */
enum {
MP_API_OK = 0,
MP_API_ERR_ARGUMENT = 1,
MP_API_ERR_STATE = 2,
MP_API_ERR_CONNECT = 3,
MP_API_ERR_PROTOCOL = 4,
MP_API_ERR_IO = 5,
MP_API_ERR_REJECTED = 6 /* t.ex. ogiltigt sessionsID vid join */
};
/* Skapar en ny APIinstans. Returnerar NULL vid fel. */
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. */
void mp_api_destroy(MultiplayerApi *api);
/* Hostar en ny session. Blockerar tills svar erhållits eller fel uppstår.
out_session / out_clientId pekar på nyallokerade strängar (malloc) som
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). */
int mp_api_host(MultiplayerApi *api,
json_t *data,
char **out_session,
char **out_clientId,
json_t **out_data);
/*
Hämtar en lista över tillgängliga publika sessioner.
Returnerar MP_API_OK vid framgång, annan felkod vid fel.
Anroparen ansvarar för att json_decref:a out_list när klar. */
int mp_api_list(MultiplayerApi *api,
json_t **out_list);
/* Går med i befintlig session.
sessionId: sessionskod (t.ex. "ABC123").
data: valfri JSONpayload med spelarinformation (kan vara NULL).
out_* fungerar som i mp_api_host.
Returnerar:
- MP_API_OK vid lyckad join
- MP_API_ERR_REJECTED om servern svarar med status:error (t.ex. ogiltigt ID)
- annan felkod vid nätverks/protokollfel.
*/
int mp_api_join(MultiplayerApi *api,
const char *sessionId,
json_t *data,
char **out_session,
char **out_clientId,
json_t **out_data);
/* Skickar ett "game"meddelande med godtycklig JSONdata till sessionen. */
int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination);
/* Registrerar en lyssnare för inkommande events.
Returnerar ett positivt listenerID, eller 1 vid fel. */
int mp_api_listen(MultiplayerApi *api,
MultiplayerListener cb,
void *context);
/* Avregistrerar lyssnare. ListenerID är värdet från mp_api_listen. */
void mp_api_unlisten(MultiplayerApi *api, int listener_id);
#endif /* MULTIPLAYER_API_H */

View File

@@ -0,0 +1,257 @@
#include "networking.h"
#include <assert.h>
#define ONVO_IDENTIFIER "c2438167-831b-4bf7-8bdc-abcdefabcd00"
static void listen_callback(
const char *event, /* "joined", "leaved", "game" */
int64_t messsage_id, /* sekventiellt meddelandeID (från host) */
const char *client_id, /* avsändarens klientID (eller NULL) */
json_t *data, /* JSONobjekt med godtycklig speldata */
void *context /* godtycklig pekare som skickas vidare */
) {
Game_Networking *networking = (Game_Networking *)context;
assert(networking != NULL);
assert(networking->game_session != NULL);
(void)data;
printf("#%li :: Event: '%s' from '%s'.\n", messsage_id, event, client_id);
assert(networking != NULL);
if(strcmp(event, "joined") == 0) {
// Create a new player in the session, get the id and store it as the value in the
// 'client_id_to_player_index' hashmap using 'client_id' as the key.
uint32_t *existing_id = map_get(&networking->client_id_to_player_index, client_id);
assert(existing_id == NULL); // Something has gone terribly wrong if we get a 'joined' event for the same client-id with them still being in the map.
uint16_t player_index = 0xFFFF;
assert(game_session_create_player(networking->game_session, &player_index));
assert(map_set(&networking->client_id_to_player_index, client_id, player_index) == 0); // 0 = OK.
// TODO: SS - Send a 'game' message to the joinee with information about the session?
printf("Added client-id '%s' (player-index: %u) to the map.\n", client_id, player_index);
}
else if(strcmp(event, "leaved") == 0) {
// TODO: SS - Index the hashmap using 'client_id' as the key and attempt to get the
// player-index for that client. Run 'game_session_destroy_player()'.
uint32_t *existing_id = map_get(&networking->client_id_to_player_index, client_id);
assert(existing_id != NULL); // Something has gone terribly wrong if we get a 'leaved' event for the a client-id that isn't in the map.
uint16_t player_id_to_remove = *existing_id;
game_session_destroy_player(networking->game_session, player_id_to_remove); // Returns void but crashes internally if id isn't OK.
map_remove(&networking->client_id_to_player_index, client_id);
printf("Removed client-id '%s' (player-index: %u) from the map.\n", client_id, player_id_to_remove);
}
else if(strcmp(event, "game") == 0) {
// TODO: SS - Check the 'data' to see what type of game-event it is.
// Possible game-events I can think of right now:
// - Player-input.
// - Match-state changed.
// - Chat-message?
// - Session-state?
// - Maybe Player-Joined/Left. (seems like mp_api only sends 'joined'/'leaved' events to the host.)
}
else {
printf("UNKNOWN EVENT-TYPE: '%s'.\n", event);
}
}
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;
}
static bool start_listening(Game_Networking *networking) {
assert(networking != NULL);
assert(networking->api != NULL);
int listener_id = mp_api_listen(networking->api, listen_callback, (void *)networking); // TODO: SS - Change context.
if(listener_id < 0) {
return false;
}
networking->listener_id = (uint32_t)listener_id;
return true;
}
static bool stop_listening(Game_Networking *networking) {
assert(networking != NULL);
assert(networking->api != NULL);
mp_api_unlisten(networking->api, (int)networking->listener_id);
return true;
}
bool networking_try_host(Game_Networking *networking, Game_Session *session, Game_Session_Settings settings, uint16_t *out_host_player_id) {
assert(networking != NULL);
assert(session != 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);
// TODO: SS - Shutdown API.
return false;
}
printf("Started hosting session '%s', local_client_id is '%s'.\n", session_id, local_client_id);
networking->is_host = true;
networking->game_session = session;
networking->session_id = session_id;
networking->local_client_id = local_client_id;
map_init(&networking->client_id_to_player_index);
assert(start_listening(networking));
// Set up session.
game_session_init(
session,
settings
);
// Add the host to the game.
uint16_t host_player_index = 0xFFFF;
assert(game_session_create_player(networking->game_session, &host_player_index));
map_set(&networking->client_id_to_player_index, local_client_id, host_player_index);
*out_host_player_id = host_player_index;
printf("Host-player created (client-id: '%s'). Player-id: %u.\n", local_client_id, host_player_index);
if(response_data != NULL) {
// TODO: SS - Handle JSON-response.
json_decref(response_data);
}
return true;
}
bool networking_try_join(Game_Networking *networking, Game_Session *session, const char *session_id) {
assert(networking != NULL);
assert(session != NULL);
if(strlen(session_id) == 0) {
return false;
}
if(!setup_api(networking)) {
printf("Failed to join; API is already set up.\n");
return false;
}
printf("Trying to join session using code: '%s' ...\n", session_id);
char *result_session_id;
char *local_client_id;
json_t *received_json;
int join_result = mp_api_join(
networking->api, // API.
session_id, // Session-code.
NULL, // JSON-data to send. // TODO: SS - Send information about you as a player (username, ..).
&result_session_id, // Received Session-id. I assume it should be the same as 'session_id'. // TODO: SS - Ask Robin why mp_api_join() returns a session_id. Is it to support 'abcdef' => 'ABCDEF' and/or proxies?
&local_client_id, // Received client-id.
&received_json // Received JSON-data.
);
if(join_result != MP_API_OK) {
// TODO: SS - Shutdown API.
printf("Failed to join; Got result: %i.\n", join_result);
return false;
}
assert(strcmp(result_session_id, session_id) == 0);
printf("Joined session '%s', local_client_id is '%s'.\n", result_session_id, local_client_id);
networking->is_host = true;
networking->game_session = session;
networking->session_id = result_session_id;
networking->local_client_id = local_client_id;
map_init(&networking->client_id_to_player_index);
// TODO: SS - Decide how to network this..
// Should we just receive the session's settings as well as a long list of commands of what has happened throughout the match (so far) - at least one per tick?
// Or, should this client receive the current state of the session?
if(received_json != NULL) {
// TODO: SS - Read this json because it might contain valuable information.
json_decref(received_json);
}
// TODO: SS - Stop listening.
assert(start_listening(networking));
// TODO: SS - Set up session based on what we got in the json.
// game_session_init(
// session,
// settings
// );
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;
}
map_deinit(&networking->client_id_to_player_index);
assert(stop_listening(networking));
mp_api_destroy(networking->api);
networking->api = NULL;
networking->game_session = NULL;
networking->is_host = false;
if(networking->session_id != NULL) {
free(networking->session_id);
networking->session_id = NULL;
}
if(networking->local_client_id != NULL) {
free(networking->local_client_id);
networking->local_client_id = NULL;
}
printf("Stopped hosting.\n");
return true;
}

View File

@@ -0,0 +1,28 @@
#ifndef NETWORKING_H
#define NETWORKING_H
#include "multiplayer_api.h"
#include "game_session.h"
#include "shared/map.h"
typedef struct {
MultiplayerApi *api;
bool is_host;
char *session_id;
char *local_client_id;
uint32_t listener_id;
Game_Session *game_session; // Comes from Game_Instance.
map_uint_t client_id_to_player_index;
} Game_Networking;
bool networking_try_host(Game_Networking *networking, Game_Session *session, Game_Session_Settings settings, uint16_t *out_host_player_id);
bool networking_try_join(Game_Networking *networking, Game_Session *session, const char *session_id);
bool networking_stop_hosting(Game_Networking *networking);
#endif

43
src/game/shared/entity.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef ENTITY_H
#define ENTITY_H
#include <stdint.h>
#include <stdbool.h>
typedef uint16_t Entity_ID;
#define INVALID_ENTITY_ID (Entity_ID)65535
typedef enum {
Entity_Movement_Direction_None,
Entity_Movement_Direction_Up,
Entity_Movement_Direction_Down,
Entity_Movement_Direction_Right,
Entity_Movement_Direction_Left,
} Entity_Movement_Direction;
typedef enum {
Entity_Type_Snake_Head,
Entity_Type_Snake_Body,
Entity_Type_Food,
} Entity_Type;
typedef struct {
bool active;
// TODO: SS - Maybe add an ID here.
Entity_Type type;
uint16_t x;
uint16_t y;
uint16_t prev_x;
uint16_t prev_y;
Entity_Movement_Direction move_direction;
Entity_ID child;
} Entity;
#endif

View File

@@ -0,0 +1,118 @@
#include "game_world.h"
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#define MIN_LEVEL_WIDTH 8 // TODO: SS - Move these out of here.
#define MIN_LEVEL_HEIGHT 8
Game_World *game_world_create(uint32_t seed, uint16_t level_width, uint16_t level_height) {
assert(level_width >= MIN_LEVEL_WIDTH); // We should probably have failed earlier.
assert(level_height >= MIN_LEVEL_HEIGHT);
Game_World *w = (Game_World *)calloc(1, sizeof(Game_World));
assert(w != NULL);
w->seed = seed;
random_init(&w->random_generator, w->seed);
w->max_entities = level_width * level_height;
w->entities = (Entity *)calloc(w->max_entities, sizeof(Entity));
// Set up entity-id queue.
assert(squeue_init(&w->entity_id_queue, w->max_entities, sizeof(Entity_ID)));
for(uint16_t i = 0; i < w->entity_id_queue.capacity; i++) {
Entity_ID id = (Entity_ID)i;
assert(squeue_push(&w->entity_id_queue, &id));
}
grid_initialize(&w->grid, level_width, level_height);
{ // TEMP: SS - Testing ..
for(uint16_t i = 0; i < 8; i++) {
Entity_ID entity_food;
assert(game_world_create_entity(
w,
Entity_Type_Food,
(uint16_t)random_u32_range(&w->random_generator, 0, level_width),
(uint16_t)random_u32_range(&w->random_generator, 0, level_height),
&entity_food
));
}
}
return w;
}
void game_world_destroy(Game_World *world) {
assert(world != NULL);
world->seed = 0;
world->max_entities = 0;
free(world->entities);
grid_dispose(&world->grid);
squeue_free(&world->entity_id_queue);
free(world);
}
bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id) {
assert(world != NULL);
*out_entity_id = INVALID_ENTITY_ID;
if(x >= world->grid.width) {
printf("Failed to create entity; Invalid x-coordinate.\n");
return false;
}
if(y >= world->grid.height) {
printf("Failed to create entity; Invalid y-coordinate.\n");
return false;
}
Entity_ID id = 0;
if(!squeue_pop(&world->entity_id_queue, (void *)&id)) {
printf("No free entity ids.\n");
return false;
}
world->entities[id] = (Entity) {
.active = true,
.type = type,
.child = INVALID_ENTITY_ID
};
Grid_Cell *cell = grid_get_cell(&world->grid, x, y); // TEMP: SS - Hardcoded coordinates. // TODO: SS - Find good coordinate.
assert(cell != NULL);
assert(grid_try_add_entity_to_cell(cell, &world->entities[id]));
*out_entity_id = id;
return true;
}
void game_world_destroy_entity(Game_World *world, Entity_ID entity_id) {
assert(world != NULL);
Entity *entity = game_world_try_get_entity_by_id(
world,
entity_id
);
assert(entity != NULL);
entity->active = false;
Grid_Cell *cell = grid_get_cell(&world->grid, entity->x, entity->y);
assert(cell != NULL);
assert(grid_try_remove_entity_from_cell(cell));
squeue_push(&world->entity_id_queue, (void *)&entity_id);
}
Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id) {
assert(world != NULL);
if(id >= world->max_entities) {
return NULL;
}
return &world->entities[id];
}

View File

@@ -0,0 +1,32 @@
#ifndef WORLD_H
#define WORLD_H
#include <stdint.h>
#include "entity.h"
#include "grid.h"
#include "shared/squeue.h"
#include "shared/random.h"
typedef struct {
uint32_t seed;
Random_Generator random_generator;
SQueue entity_id_queue;
Entity *entities;
uint16_t max_entities;
Grid grid;
} Game_World;
Game_World *game_world_create(uint32_t seed, uint16_t level_width, uint16_t level_height);
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);
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);
#endif

126
src/game/shared/grid.c Normal file
View File

@@ -0,0 +1,126 @@
#include "grid.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#define GRID_MIN_WIDTH 8
#define GRID_MIN_HEIGHT 8
static bool from_grid_index(Grid *grid, uint16_t grid_index, uint16_t *out_x, uint16_t *out_y) {
assert(grid != NULL);
assert(out_x != NULL);
assert(out_y != NULL);
if (!grid || !out_x || !out_y) return false;
if (grid_index >= grid->width * grid->height) return false;
*out_x = grid_index % grid->width;
*out_y = grid_index / grid->width;
return true;
}
bool grid_initialize(Grid *grid, uint16_t width, uint16_t height) {
assert(grid != NULL);
if(width < GRID_MIN_WIDTH || height < GRID_MIN_HEIGHT) {
return false;
}
grid->width = width;
grid->height = height;
grid->cells = calloc(width * height, sizeof(Grid_Cell));
for(uint16_t i = 0; i < (width * height); i++) {
Grid_Cell *cell = &grid->cells[i];
assert(from_grid_index(grid, i, &cell->x, &cell->y));
}
return true;
}
void grid_dispose(Grid *grid) {
assert(grid != NULL);
free(grid->cells);
memset(grid, 0, sizeof(Grid));
}
static bool to_grid_index(Grid *grid, uint16_t x, uint16_t y, uint16_t *out_grid_index) {
assert(grid != NULL);
assert(out_grid_index != NULL);
if(!grid || !out_grid_index) {
return false;
}
if(x >= grid->width || y >= grid->height) {
return false;
}
*out_grid_index = y * grid->width + x;
return true;
}
Grid_Cell *grid_get_cell(Grid *grid, uint16_t x, uint16_t y) {
assert(grid != NULL);
if(x >= grid->width || y >= grid->height) {
return NULL;
}
uint16_t grid_index = 0;
if(!to_grid_index(grid, x, y, &grid_index)) {
return NULL;
}
return &grid->cells[grid_index];
}
bool grid_try_add_entity_to_cell(Grid_Cell *cell, Entity *entity) {
assert(cell != NULL);
assert(entity != NULL);
// TODO: SS - Could check what type of cell it is to determine if
// we're allowed to place an entity here. Is it a wall? Lava?
if(cell->entity != NULL) {
return false;
}
cell->entity = entity;
entity->x = cell->x;
entity->y = cell->y;
return true;
}
bool grid_try_remove_entity_from_cell(Grid_Cell *cell) {
assert(cell != NULL);
if(cell->entity == NULL) {
return false;
}
cell->entity->x = 0;
cell->entity->y = 0;
cell->entity = NULL;
return true;
}
void grid_move_entity_from_cell_to_cell(Grid_Cell *cell_a, Grid_Cell *cell_b) {
assert(cell_a != NULL);
assert(cell_b != NULL);
assert(cell_a->entity != NULL); // We expect cell A to have an Entity to move.
assert(cell_b->entity == NULL); // We expect cell B to NOT have an Entity.
Entity *entity_to_be_moved = cell_a->entity;
uint16_t start_x = cell_a->x;
uint16_t start_y = cell_a->y;
assert(grid_try_remove_entity_from_cell(cell_a));
assert(grid_try_add_entity_to_cell(cell_b, entity_to_be_moved));
entity_to_be_moved->prev_x = start_x;
entity_to_be_moved->prev_y = start_y;
}

33
src/game/shared/grid.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef GRID_H
#define GRID_H
#include <stdint.h>
#include <stdbool.h>
#include "entity.h"
typedef struct {
Entity *entity;
uint16_t x;
uint16_t y;
} Grid_Cell;
typedef struct {
uint16_t width;
uint16_t height;
Grid_Cell *cells;
} Grid;
bool grid_initialize(Grid *grid, uint16_t width, uint16_t height);
void grid_dispose(Grid *grid);
Grid_Cell *grid_get_cell(Grid *grid, uint16_t x, uint16_t y);
bool grid_try_add_entity_to_cell(Grid_Cell *cell, Entity *entity);
bool grid_try_remove_entity_from_cell(Grid_Cell *cell);
void grid_move_entity_from_cell_to_cell(Grid_Cell *cell_a, Grid_Cell *cell_b);
#endif

193
src/game/shared/map.c Normal file
View File

@@ -0,0 +1,193 @@
/**
* Copyright (c) 2014 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See LICENSE for details.
*/
#include <stdlib.h>
#include <string.h>
#include "map.h"
struct map_node_t {
unsigned hash;
void *value;
map_node_t *next;
/* char key[]; */
/* char value[]; */
};
static unsigned map_hash(const char *str) {
unsigned hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) ^ *str++;
}
return hash;
}
static map_node_t *map_newnode(const char *key, void *value, int vsize) {
map_node_t *node;
int ksize = strlen(key) + 1;
int voffset = ksize + ((sizeof(void*) - ksize) % sizeof(void*));
node = malloc(sizeof(*node) + voffset + vsize);
if (!node) return NULL;
memcpy(node + 1, key, ksize);
node->hash = map_hash(key);
node->value = ((char*) (node + 1)) + voffset;
memcpy(node->value, value, vsize);
return node;
}
static int map_bucketidx(map_base_t *m, unsigned hash) {
/* If the implementation is changed to allow a non-power-of-2 bucket count,
* the line below should be changed to use mod instead of AND */
return hash & (m->nbuckets - 1);
}
static void map_addnode(map_base_t *m, map_node_t *node) {
int n = map_bucketidx(m, node->hash);
node->next = m->buckets[n];
m->buckets[n] = node;
}
static int map_resize(map_base_t *m, int nbuckets) {
map_node_t *nodes, *node, *next;
map_node_t **buckets;
int i;
/* Chain all nodes together */
nodes = NULL;
i = m->nbuckets;
while (i--) {
node = (m->buckets)[i];
while (node) {
next = node->next;
node->next = nodes;
nodes = node;
node = next;
}
}
/* Reset buckets */
buckets = realloc(m->buckets, sizeof(*m->buckets) * nbuckets);
if (buckets != NULL) {
m->buckets = buckets;
m->nbuckets = nbuckets;
}
if (m->buckets) {
memset(m->buckets, 0, sizeof(*m->buckets) * m->nbuckets);
/* Re-add nodes to buckets */
node = nodes;
while (node) {
next = node->next;
map_addnode(m, node);
node = next;
}
}
/* Return error code if realloc() failed */
return (buckets == NULL) ? -1 : 0;
}
static map_node_t **map_getref(map_base_t *m, const char *key) {
unsigned hash = map_hash(key);
map_node_t **next;
if (m->nbuckets > 0) {
next = &m->buckets[map_bucketidx(m, hash)];
while (*next) {
if ((*next)->hash == hash && !strcmp((char*) (*next + 1), key)) {
return next;
}
next = &(*next)->next;
}
}
return NULL;
}
void map_deinit_(map_base_t *m) {
map_node_t *next, *node;
int i;
i = m->nbuckets;
while (i--) {
node = m->buckets[i];
while (node) {
next = node->next;
free(node);
node = next;
}
}
free(m->buckets);
}
void *map_get_(map_base_t *m, const char *key) {
map_node_t **next = map_getref(m, key);
return next ? (*next)->value : NULL;
}
int map_set_(map_base_t *m, const char *key, void *value, int vsize) {
int n, err;
map_node_t **next, *node;
/* Find & replace existing node */
next = map_getref(m, key);
if (next) {
memcpy((*next)->value, value, vsize);
return 0;
}
/* Add new node */
node = map_newnode(key, value, vsize);
if (node == NULL) goto fail;
if (m->nnodes >= m->nbuckets) {
n = (m->nbuckets > 0) ? (m->nbuckets << 1) : 1;
err = map_resize(m, n);
if (err) goto fail;
}
map_addnode(m, node);
m->nnodes++;
return 0;
fail:
if (node) free(node);
return -1;
}
void map_remove_(map_base_t *m, const char *key) {
map_node_t *node;
map_node_t **next = map_getref(m, key);
if (next) {
node = *next;
*next = (*next)->next;
free(node);
m->nnodes--;
}
}
map_iter_t map_iter_(void) {
map_iter_t iter;
iter.bucketidx = -1;
iter.node = NULL;
return iter;
}
const char *map_next_(map_base_t *m, map_iter_t *iter) {
if (iter->node) {
iter->node = iter->node->next;
if (iter->node == NULL) goto nextBucket;
} else {
nextBucket:
do {
if (++iter->bucketidx >= m->nbuckets) {
return NULL;
}
iter->node = m->buckets[iter->bucketidx];
} while (iter->node == NULL);
}
return (char*) (iter->node + 1);
}

78
src/game/shared/map.h Normal file
View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2014 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See LICENSE for details.
*/
#ifndef MAP_H
#define MAP_H
#include <string.h>
#define MAP_VERSION "0.1.0"
struct map_node_t;
typedef struct map_node_t map_node_t;
typedef struct {
map_node_t **buckets;
unsigned nbuckets, nnodes;
} map_base_t;
typedef struct {
unsigned bucketidx;
map_node_t *node;
} map_iter_t;
#define map_t(T)\
struct { map_base_t base; T *ref; T tmp; }
#define map_init(m)\
memset(m, 0, sizeof(*(m)))
#define map_deinit(m)\
map_deinit_(&(m)->base)
#define map_get(m, key)\
( (m)->ref = map_get_(&(m)->base, key) )
#define map_set(m, key, value)\
( (m)->tmp = (value),\
map_set_(&(m)->base, key, &(m)->tmp, sizeof((m)->tmp)) )
#define map_remove(m, key)\
map_remove_(&(m)->base, key)
#define map_iter(m)\
map_iter_()
#define map_next(m, iter)\
map_next_(&(m)->base, iter)
void map_deinit_(map_base_t *m);
void *map_get_(map_base_t *m, const char *key);
int map_set_(map_base_t *m, const char *key, void *value, int vsize);
void map_remove_(map_base_t *m, const char *key);
map_iter_t map_iter_(void);
const char *map_next_(map_base_t *m, map_iter_t *iter);
typedef map_t(void*) map_void_t;
typedef map_t(char*) map_str_t;
typedef map_t(int) map_int_t;
typedef map_t(unsigned int) map_uint_t;
typedef map_t(char) map_char_t;
typedef map_t(float) map_float_t;
typedef map_t(double) map_double_t;
#endif

25
src/game/shared/random.c Normal file
View File

@@ -0,0 +1,25 @@
#include "random.h"
void random_init(Random_Generator *random_generator, uint32_t seed) {
if(seed == 0) {
seed = 1;
}
random_generator->seed = seed;
random_generator->state = random_generator->seed;
}
uint32_t random_u32(Random_Generator *random_generator) {
uint32_t x = random_generator->state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
random_generator->state = x;
return x;
}
uint32_t random_u32_range(Random_Generator *random_generator, uint32_t min, uint32_t max) {
return min + (random_u32(random_generator) % (max - min + 1));
}

17
src/game/shared/random.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef RANDOM_H
#define RANDOM_H
#include <stdint.h>
typedef struct {
uint32_t seed;
uint32_t state;
} Random_Generator;
void random_init(Random_Generator *random_generator, uint32_t seed);
uint32_t random_u32(Random_Generator *random_generator);
uint32_t random_u32_range(Random_Generator *random_generator, uint32_t min, uint32_t max);
#endif

57
src/game/shared/squeue.c Normal file
View File

@@ -0,0 +1,57 @@
#include "squeue.h"
bool squeue_init(SQueue *q, uint16_t capacity, size_t element_size) {
q->buffer = malloc(element_size * capacity);
if (!q->buffer) {
return false;
}
q->head = q->tail = q->count = 0;
q->capacity = capacity;
q->element_size = element_size;
return true;
}
void squeue_free(SQueue *q) {
free(q->buffer);
q->buffer = NULL;
q->head = q->tail = q->count = q->capacity = 0;
q->element_size = 0;
}
bool squeue_push(SQueue *q, const void *elem) {
if (q->count == q->capacity) {
return false;
}
void *dest = (uint8_t*)q->buffer + q->tail * q->element_size;
memcpy(dest, elem, q->element_size);
q->tail = (q->tail + 1) % q->capacity;
q->count++;
return true;
}
bool squeue_pop(SQueue *q, void *out) {
if (q->count == 0) {
return false;
}
void *src = (uint8_t*)q->buffer + q->head * q->element_size;
memcpy(out, src, q->element_size);
q->head = (q->head + 1) % q->capacity;
q->count--;
return true;
}
bool squeue_peek(const SQueue *q, void *out) {
if (q->count == 0) {
return false;
}
void *src = (uint8_t*)q->buffer + q->head * q->element_size;
memcpy(out, src, q->element_size);
return true;
}

27
src/game/shared/squeue.h Normal file
View File

@@ -0,0 +1,27 @@
#ifndef SQUEUE_H
#define SQUEUE_H
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
// FIFO.
typedef struct {
void *buffer;
uint16_t head;
uint16_t tail;
uint16_t count;
uint16_t capacity;
size_t element_size;
} SQueue;
bool squeue_init(SQueue *q, uint16_t capacity, size_t element_size);
void squeue_free(SQueue *q);
bool squeue_push(SQueue *q, const void *elem);
bool squeue_pop(SQueue *q, void *out);
bool squeue_peek(const SQueue *q, void *out);
#endif

View File

@@ -0,0 +1,13 @@
#ifndef WANG_HASH_H
#define WANG_HASH_H
static inline uint32_t wang_hash(uint32_t v) {
v = (v ^ 61) ^ (v >> 16);
v = v + (v << 3);
v = v ^ (v >> 4);
v = v * 0x27d4eb2d;
v = v ^ (v >> 15);
return v;
}
#endif

View File

@@ -0,0 +1,19 @@
#ifndef SIM_COMMAND_H
#define SIM_COMMAND_H
#include "input.h"
typedef enum {
Simulation_Command_Type_Add_Player,
Simulation_Command_Type_Remove_Player,
Simulation_Command_Type_Set_Player_Input,
} Simulation_Command_Type;
typedef struct {
Simulation_Command_Type type;
uint16_t player_id;
Simulation_Game_Input player_input;
} Simulation_Command;
#endif

View File

@@ -0,0 +1,29 @@
#ifndef SIM_GAME_INPUT_H
#define SIM_GAME_INPUT_H
typedef struct {
// Movement can probably just be one byte. For a traditional snake-game, only one directional-input is OK.
bool up;
bool down;
bool right;
bool left;
} Simulation_Game_Input;
static inline bool simulation_input_equal(Simulation_Game_Input a, Simulation_Game_Input b) {
if(a.up != b.up) {
return false;
}
if(a.down != b.down) {
return false;
}
if(a.right != b.right) {
return false;
}
if(a.left != b.left) {
return false;
}
return true;
}
#endif

View File

@@ -0,0 +1,15 @@
#ifndef SIM_PLAYER_H
#define SIM_PLAYER_H
#include "simulation/input.h"
typedef struct {
bool active;
Entity_ID entity_id;
Simulation_Game_Input input;
// score, name etc.
} Simulation_Player;
#endif

View File

@@ -0,0 +1,365 @@
#include "simulation_world.h"
#include <string.h>
#include <stdio.h>
#include <assert.h>
#define SIM_COMMANDS_PER_PLAYER 4
Simulation_World simulation_create_world(uint32_t seed, bool online, uint8_t max_players, uint16_t width, uint16_t height) {
Simulation_World w;
memset(&w, 0, sizeof(Simulation_World));
w.tick = 0;
w.online = online;
w.game_world = game_world_create(seed, width, height);
assert(w.game_world != NULL);
w.match_state = Simulation_Match_State_Waiting_To_Start;
w.players = (Simulation_Player *)calloc(max_players, sizeof(Simulation_Player));
w.max_players = max_players;
w.max_commands = w.max_players * SIM_COMMANDS_PER_PLAYER;
w.command_count = 0;
w.commands = (Simulation_Command *)calloc(w.max_commands, sizeof(Simulation_Command));
return w;
}
void simulation_destroy_world(Simulation_World *simulation_world) {
assert(simulation_world != NULL);
game_world_destroy(simulation_world->game_world);
simulation_world->game_world = NULL;
simulation_world->match_state = Simulation_Match_State_Waiting_To_Start;
free(simulation_world->players);
simulation_world->players = NULL;
free(simulation_world->commands);
simulation_world->commands = NULL;
}
void simulation_world_tick(Simulation_World *simulation_world) {
assert(simulation_world != NULL);
// printf("TICK: %lu.\n", simulation_world->tick);
for(uint16_t i = 0; i < simulation_world->command_count; i++) {
Simulation_Command *cmd = &simulation_world->commands[i];
// printf("Command type: %i, player id: %i.\n", cmd->type, cmd->player_id);
Simulation_Player *player = &simulation_world->players[cmd->player_id];
assert(player != NULL);
switch(cmd->type) {
case Simulation_Command_Type_Add_Player: {
// printf("Simulation_Command_Type_Add_Player\n");
assert(!player->active);
player->active = true;
assert(game_world_create_entity(
simulation_world->game_world,
Entity_Type_Snake_Head,
i+6, 15, // NOTE: SS - Hardcoded spawn-position.
&player->entity_id
));
Entity *player_entity = game_world_try_get_entity_by_id(simulation_world->game_world, player->entity_id);
// Set initial move-direction for player-entities.
// The initial direction should probably take the spawn-position in to account.
player_entity->move_direction = Entity_Movement_Direction_Up;
break;
}
case Simulation_Command_Type_Remove_Player: {
// printf("Simulation_Command_Type_Remove_Player\n");
assert(player->active);
player->active = false;
game_world_destroy_entity(simulation_world->game_world, player->entity_id);
break;
}
case Simulation_Command_Type_Set_Player_Input: {
// printf("Simulation_Command_Type_Set_Player_Input\n");
assert(player->active);
player->input = cmd->player_input;
break;
}
}
}
simulation_world->command_count = 0;
switch(simulation_world->match_state) {
case Simulation_Match_State_Waiting_To_Start: {
if(!simulation_world->online) { // If not online, just go to Counting_Down.
simulation_world->match_state = Simulation_Match_State_Counting_Down;
}
else {
if(simulation_world_amount_of_active_players(simulation_world) > 1) {
simulation_world->match_state = Simulation_Match_State_Counting_Down;
}
}
break;
}
case Simulation_Match_State_Counting_Down: {
if(simulation_world->tick > 20) { // TEMP: SS - Hardcoded number and condition to break out of this state.
simulation_world->match_state = Simulation_Match_State_Active;
}
break;
}
case Simulation_Match_State_Active: {
if(simulation_world->tick > 500) { // TEMP: SS - Hardcoded number and condition to break out of this state.
simulation_world->match_state = Simulation_Match_State_Ended;
}
else {
Game_World *gw = simulation_world->game_world;
// Loop over all players in the simulation.
for(uint16_t i = 0; i < simulation_world->max_players; i++) {
Simulation_Player *player = &simulation_world->players[i];
if(!player->active) {
continue;
}
// printf("* Input for player %i - up: %i, down: %i, right: %i, left: %i.\n",
// i,
// player->input.up, player->input.down, player->input.right, player->input.left
// );
Entity *player_entity = game_world_try_get_entity_by_id(gw, player->entity_id);
assert(player_entity != NULL);
// Snakes can't go backwards. They have to turn.
switch(player_entity->move_direction) {
case Entity_Movement_Direction_None: {
break;
}
case Entity_Movement_Direction_Up:
case Entity_Movement_Direction_Down: // Snakes can only go right/left if it's currently moving up/down.
{
if(player->input.right) {
player_entity->move_direction = Entity_Movement_Direction_Right;
}
else if(player->input.left) {
player_entity->move_direction = Entity_Movement_Direction_Left;
}
break;
}
case Entity_Movement_Direction_Right:
case Entity_Movement_Direction_Left: // Snakes can only go up/down if it's currently moving right/left.
{
if(player->input.up) {
player_entity->move_direction = Entity_Movement_Direction_Up;
}
else if(player->input.down) {
player_entity->move_direction = Entity_Movement_Direction_Down;
}
break;
}
}
}
// Game-logic! :)
for(uint16_t i = 0; i < gw->max_entities; i++) {
Entity *entity = &gw->entities[i];
if(!entity->active) {
continue;
}
switch(entity->type) {
case Entity_Type_Snake_Head: {
break;
}
case Entity_Type_Snake_Body: {
// Skip.
break;
}
case Entity_Type_Food: {
// Skip (for now).
break;
}
default: {
printf("Unhandled entity-type %i.\n", entity->type);
break;
}
}
{ // Move entity
int16_t dx = 0;
int16_t dy = 0;
switch(entity->move_direction) {
case Entity_Movement_Direction_None: {
break;
}
case Entity_Movement_Direction_Up: {
dy = -1;
break;
}
case Entity_Movement_Direction_Down: {
dy = 1;
break;
}
case Entity_Movement_Direction_Right: {
dx = 1;
break;
}
case Entity_Movement_Direction_Left: {
dx = -1;
break;
}
}
if(dx != 0 || dy != 0) {
// Try moving.
// 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);
assert(current_cell != NULL);
Grid_Cell *target_cell = grid_get_cell(&gw->grid, entity->x + dx, entity->y + dy);
// { // Debug-logging.
// printf("Current cell = (x: %u, y: %u, entity: %p).\n", current_cell->x, current_cell->y, current_cell->entity);
// if(target_cell != NULL) {
// printf("Target cell = (x: %u, y: %u, entity: %p).\n", target_cell->x, target_cell->y, target_cell->entity);
// }
// else {
// printf("Target cell = NULL!\n");
// }
// }
if(target_cell == NULL) {
// Target cell does not exist.
// TODO: SS - Check the session's settings.
// Maybe each session could decide whether the snake should "loop-around" or not.
// If the snake shouldn't loop around, die.
}
else {
bool should_move_to_target_cell = false;
bool snake_should_grow = false;
if(target_cell->entity != NULL) {
// Target cell is occupied.
// Check what type of entity it is and determine what should happen.
switch(target_cell->entity->type) {
case Entity_Type_Snake_Head: {
// TODO: SS - Die.
break;
}
case Entity_Type_Snake_Body: {
// TODO: SS - Die.
break;
}
case Entity_Type_Food: {
// Eat!
assert(grid_try_remove_entity_from_cell(target_cell));
snake_should_grow = true;
should_move_to_target_cell = true;
break;
}
}
}
else {
// Target cell is unoccupied and free to enter.
should_move_to_target_cell = true;
}
if(should_move_to_target_cell) {
{ // Move snake recursively.
Entity *e = entity;
Grid_Cell *t = target_cell;
while(e != NULL) {
Grid_Cell *a = grid_get_cell(&gw->grid, e->x, e->y);
assert(a != NULL);
grid_move_entity_from_cell_to_cell(a, t);
e = game_world_try_get_entity_by_id(gw, e->child);
t = a;
}
}
if(snake_should_grow) {
Entity *e = entity;
uint16_t child_index = 0;
while(e->child != INVALID_ENTITY_ID) {
e = game_world_try_get_entity_by_id(gw, e->child);
child_index += 1;
}
Entity_ID child_entity = INVALID_ENTITY_ID;
assert(game_world_create_entity(
gw,
Entity_Type_Snake_Body,
e->prev_x,
e->prev_y,
&child_entity
));
assert(e->child == INVALID_ENTITY_ID);
e->child = child_entity;
}
}
}
}
}
}
}
break;
}
case Simulation_Match_State_Ended: {
printf("Match over!\n");
break;
}
}
simulation_world->tick += 1; // TODO: SS - Stop ticking if match has ended.
}
bool simulation_world_enqueue_command(Simulation_World *simulation_world, Simulation_Command command) {
assert(simulation_world != NULL);
if(simulation_world->command_count >= simulation_world->max_commands) {
return false;
}
simulation_world->commands[simulation_world->command_count] = command;
simulation_world->command_count += 1;
return true;
}
uint16_t simulation_world_amount_of_active_players(Simulation_World *simulation_world) {
assert(simulation_world != NULL);
uint16_t count = 0;
for(uint16_t i = 0; i < simulation_world->max_players; i++) {
Simulation_Player *player = &simulation_world->players[i];
if(player->active) {
count += 1;
}
}
return count;
}

View File

@@ -0,0 +1,45 @@
#ifndef SIMULATION_WORLD_H
#define SIMULATION_WORLD_H
#include <stdlib.h>
#include <stdbool.h>
#include "shared/game_world.h"
#include "shared/random.h" // Deterministic random.
#include "player.h"
#include "command.h"
typedef enum {
Simulation_Match_State_Waiting_To_Start,
Simulation_Match_State_Counting_Down,
Simulation_Match_State_Active,
Simulation_Match_State_Ended,
} Simulation_Match_State;
typedef struct {
uint64_t tick;
bool online;
Game_World *game_world;
Simulation_Match_State match_state;
Simulation_Player *players;
uint16_t max_players;
Simulation_Command *commands;
uint16_t command_count;
uint16_t max_commands;
} Simulation_World;
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_world_tick(Simulation_World *simulation_world);
bool simulation_world_enqueue_command(Simulation_World *simulation_world, Simulation_Command command);
uint16_t simulation_world_amount_of_active_players(Simulation_World *simulation_world);
#endif