Moved code and assets for the game to a seperate folder. Moved jansson and raylib to third_party.
This commit is contained in:
BIN
src/game/assets/sprites/spr_floor_grass.png
Normal file
BIN
src/game/assets/sprites/spr_floor_grass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 643 B |
BIN
src/game/assets/sprites/spr_food_apple.png
Normal file
BIN
src/game/assets/sprites/spr_food_apple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 653 B |
BIN
src/game/assets/sprites/spr_shadow_basic.png
Normal file
BIN
src/game/assets/sprites/spr_shadow_basic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 592 B |
BIN
src/game/assets/sprites/spr_snake_body.png
Normal file
BIN
src/game/assets/sprites/spr_snake_body.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 592 B |
BIN
src/game/assets/sprites/spr_snake_head.png
Normal file
BIN
src/game/assets/sprites/spr_snake_head.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 605 B |
132
src/game/instance/game_instance.c
Normal file
132
src/game/instance/game_instance.c
Normal 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;
|
||||
}
|
||||
32
src/game/instance/game_instance.h
Normal file
32
src/game/instance/game_instance.h
Normal 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
|
||||
355
src/game/presentation/states/state_ingame.c
Normal file
355
src/game/presentation/states/state_ingame.c
Normal 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
|
||||
};
|
||||
}
|
||||
31
src/game/presentation/states/state_ingame.h
Normal file
31
src/game/presentation/states/state_ingame.h
Normal 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
|
||||
48
src/game/presentation/states/state_machine.c
Normal file
48
src/game/presentation/states/state_machine.c
Normal 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;
|
||||
}
|
||||
40
src/game/presentation/states/state_machine.h
Normal file
40
src/game/presentation/states/state_machine.h
Normal 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
|
||||
189
src/game/presentation/states/state_main_menu.c
Normal file
189
src/game/presentation/states/state_main_menu.c
Normal 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
|
||||
};
|
||||
}
|
||||
33
src/game/presentation/states/state_main_menu.h
Normal file
33
src/game/presentation/states/state_main_menu.h
Normal 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
|
||||
10
src/game/presentation/states/states.h
Normal file
10
src/game/presentation/states/states.h
Normal 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
|
||||
155
src/game/session/game_session.c
Normal file
155
src/game/session/game_session.c
Normal 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;
|
||||
}
|
||||
58
src/game/session/game_session.h
Normal file
58
src/game/session/game_session.h
Normal 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
|
||||
758
src/game/session/multiplayer_api.c
Normal file
758
src/game/session/multiplayer_api.c
Normal 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;
|
||||
}
|
||||
81
src/game/session/multiplayer_api.h
Normal file
81
src/game/session/multiplayer_api.h
Normal 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;
|
||||
|
||||
/* Callback‑typ för inkommande events från servern. */
|
||||
typedef void (*MultiplayerListener)(
|
||||
const char *event, /* "joined", "leaved", "game" */
|
||||
int64_t messageId, /* sekventiellt meddelande‑ID (från host) */
|
||||
const char *clientId, /* avsändarens klient‑ID (eller NULL) */
|
||||
json_t *data, /* JSON‑objekt 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 sessions‑ID vid join */
|
||||
};
|
||||
|
||||
/* Skapar en ny API‑instans. 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 JSON‑payload 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/protokoll‑fel.
|
||||
*/
|
||||
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 JSON‑data till sessionen. */
|
||||
int mp_api_game(MultiplayerApi *api, json_t *data, const char* destination);
|
||||
|
||||
/* Registrerar en lyssnare för inkommande events.
|
||||
Returnerar ett positivt listener‑ID, eller −1 vid fel. */
|
||||
int mp_api_listen(MultiplayerApi *api,
|
||||
MultiplayerListener cb,
|
||||
void *context);
|
||||
|
||||
/* Avregistrerar lyssnare. Listener‑ID är värdet från mp_api_listen. */
|
||||
void mp_api_unlisten(MultiplayerApi *api, int listener_id);
|
||||
|
||||
#endif /* MULTIPLAYER_API_H */
|
||||
257
src/game/session/networking.c
Normal file
257
src/game/session/networking.c
Normal 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 meddelande‑ID (från host) */
|
||||
const char *client_id, /* avsändarens klient‑ID (eller NULL) */
|
||||
json_t *data, /* JSON‑objekt 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;
|
||||
}
|
||||
28
src/game/session/networking.h
Normal file
28
src/game/session/networking.h
Normal 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
43
src/game/shared/entity.h
Normal 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
|
||||
118
src/game/shared/game_world.c
Normal file
118
src/game/shared/game_world.c
Normal 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];
|
||||
}
|
||||
32
src/game/shared/game_world.h
Normal file
32
src/game/shared/game_world.h
Normal 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
126
src/game/shared/grid.c
Normal 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
33
src/game/shared/grid.h
Normal 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
193
src/game/shared/map.c
Normal 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
78
src/game/shared/map.h
Normal 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
25
src/game/shared/random.c
Normal 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
17
src/game/shared/random.h
Normal 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
57
src/game/shared/squeue.c
Normal 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
27
src/game/shared/squeue.h
Normal 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
|
||||
13
src/game/shared/wang_hash.h
Normal file
13
src/game/shared/wang_hash.h
Normal 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
|
||||
19
src/game/simulation/command.h
Normal file
19
src/game/simulation/command.h
Normal 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
|
||||
29
src/game/simulation/input.h
Normal file
29
src/game/simulation/input.h
Normal 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
|
||||
15
src/game/simulation/player.h
Normal file
15
src/game/simulation/player.h
Normal 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
|
||||
365
src/game/simulation/simulation_world.c
Normal file
365
src/game/simulation/simulation_world.c
Normal 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;
|
||||
}
|
||||
45
src/game/simulation/simulation_world.h
Normal file
45
src/game/simulation/simulation_world.h
Normal 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
|
||||
Reference in New Issue
Block a user